Bug 558027: [Ltx-Editor] Add support for context information assist for
tex commands
Change-Id: I4dfc25980e8ae1dda4decb465e25f0041c1995d4
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/ControlNode.java b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/ControlNode.java
index e3f0f54..e5a4075 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/ControlNode.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/ControlNode.java
@@ -60,6 +60,11 @@
return -1;
}
+ @Override
+ public int getArgsStartOffset() {
+ return AstNode.NA_OFFSET;
+ }
+
@Override
public void acceptInChildren(final AstVisitor visitor) throws InvocationTargetException {
@@ -107,6 +112,11 @@
return -1;
}
+ @Override
+ public int getArgsStartOffset() {
+ return getStartOffset() + 1 + getText().length();
+ }
+
@Override
public void acceptInChildren(final AstVisitor visitor) throws InvocationTargetException {
@@ -156,5 +166,10 @@
return this.command;
}
+ /**
+ * Returns the first possible offset the arguments can start (end of control word).
+ * @return the start offset
+ */
+ public abstract int getArgsStartOffset();
}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/LtxParser.java b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/LtxParser.java
index 2b15843..7d0c87f 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/LtxParser.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/LtxParser.java
@@ -20,6 +20,7 @@
import static org.eclipse.statet.docmlet.tex.core.ast.TexAstStatusConstants.STATUS2_GROUP_NOT_OPENED;
import static org.eclipse.statet.docmlet.tex.core.ast.TexAstStatusConstants.STATUS2_VERBATIM_INLINE_C_MISSING;
import static org.eclipse.statet.docmlet.tex.core.ast.TexAstStatusConstants.STATUS2_VERBATIM_INLINE_NOT_CLOSED;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import java.util.ArrayList;
import java.util.HashMap;
@@ -27,6 +28,7 @@
import java.util.Map;
import org.eclipse.statet.jcommons.collections.ImCollections;
+import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.string.BasicStringFactory;
import org.eclipse.statet.jcommons.string.StringFactory;
import org.eclipse.statet.jcommons.text.core.input.TextParserInput;
@@ -59,12 +61,28 @@
private static final String NUM_TEXT= new String("NUM"); //$NON-NLS-1$
+ public static class TaskConfig {
+
+ public TexCommandSet commandSet;
+
+ public @Nullable Map<String, TexCommand> additionalCommands;
+ public @Nullable Map<String, TexCommand> additionalEnvs;
+
+ public TaskConfig(final TexCommandSet commandSet) {
+ this.commandSet= nonNullAssert(commandSet);
+ }
+
+ }
+
+
private final LtxLexer lexer;
private TexCommandSet commandSet;
private Map<String, TexCommand> customCommands;
private Map<String, TexCommand> customEnvs;
+ private Map<String, TexCommand> additionalCommands;
+ private Map<String, TexCommand> additionalEnvs;
private byte[] stackTypes= new byte[32];
private String[] stackEndKeys= new String[32];
@@ -117,7 +135,16 @@
return command;
}
if (this.customCommands != null) {
- return this.customCommands.get(controlWord);
+ command= this.customCommands.get(controlWord);
+ if (command != null) {
+ return command;
+ }
+ }
+ if (this.additionalCommands != null) {
+ command= this.additionalCommands.get(controlWord);
+ if (command != null) {
+ return command;
+ }
}
return null;
}
@@ -137,7 +164,16 @@
return command;
}
if (this.customEnvs != null) {
- return this.customEnvs.get(name);
+ command= this.customEnvs.get(name);
+ if (command != null) {
+ return command;
+ }
+ }
+ if (this.additionalEnvs != null) {
+ command= this.additionalEnvs.get(name);
+ if (command != null) {
+ return command;
+ }
}
return null;
}
@@ -160,13 +196,15 @@
}
- public SourceComponent parse(final TextParserInput input, final TexCommandSet commandSet) {
+ private void initTask(final TextParserInput input, final TaskConfig config) {
if (this.embeddedList != null) {
this.embeddedList.clear();
}
+ this.commandSet= config.commandSet;
+ this.additionalCommands= config.additionalCommands;
+ this.additionalEnvs= config.additionalEnvs;
this.customCommands= null;
this.customEnvs= null;
- this.commandSet= commandSet;
this.lexer.reset(input);
this.foundEndStackPos= -1;
this.stackPos= -1;
@@ -175,6 +213,10 @@
this.lexer.setReport$$(true);
this.lexer.setReportAsterisk(false);
this.lexer.setReportSquaredBrackets(true);
+ }
+
+ public SourceComponent parse(final TextParserInput input, final TexCommandSet commandSet) {
+ initTask(input, new TaskConfig(commandSet));
final SourceComponent node= new SourceComponent();
if (this.lexer.next() != LtxLexer.EOF) {
@@ -189,20 +231,7 @@
public SourceComponent parse(final TextParserInput input, final AstNode parent,
final TexCommandSet commandSet) {
- if (this.embeddedList != null) {
- this.embeddedList.clear();
- }
- this.customCommands= null;
- this.customEnvs= null;
- this.commandSet= commandSet;
- this.lexer.reset(input);
- this.foundEndStackPos= -1;
- this.stackPos= -1;
- this.inMath= false;
-
- this.lexer.setReport$$(true);
- this.lexer.setReportAsterisk(false);
- this.lexer.setReportSquaredBrackets(true);
+ initTask(input, new TaskConfig(commandSet));
final SourceComponent node= new SourceComponent(parent,
input.getStartIndex(), input.getStopIndex() );
@@ -216,6 +245,25 @@
return node;
}
+ public ControlNode scanControlWordArgs(final TextParserInput input, final TexCommand command,
+ final boolean expand,
+ final TaskConfig config) {
+ initTask(input, config);
+
+ this.wasLinebreak= false;
+ final ControlNode.Word controlNode= new ControlNode.Word(command.getControlWord());
+ controlNode.startOffset= input.getStartIndex();
+ controlNode.endOffset= input.getStartIndex();
+ this.lexer.consume();
+
+ parseWord(controlNode, command, false);
+
+ if (expand && this.lexer.getOffset() > controlNode.endOffset) {
+ controlNode.endOffset= this.lexer.getOffset();
+ }
+ return controlNode;
+ }
+
private void putToStack(final byte type, final String key) {
if (++this.stackPos >= this.stackTypes.length) {
@@ -598,15 +646,18 @@
}
private TexAstNode createAndParseWord() {
- String label= null;
this.wasLinebreak= false;
- final ControlNode.Word controlNode= new ControlNode.Word(
- label= this.lexer.getText(this.symbolTextFactory) );
+ final String label= this.lexer.getText(this.symbolTextFactory);
+ final ControlNode.Word controlNode= new ControlNode.Word(label);
controlNode.startOffset= this.lexer.getOffset();
controlNode.endOffset= this.lexer.getStopOffset();
this.lexer.consume();
- TexCommand command= getCommand(label);
+ return parseWord(controlNode, getCommand(label), true);
+ }
+
+ private TexAstNode parseWord(final ControlNode.Word controlNode,
+ TexCommand command, final boolean exec) {
if (command == null) {
if (this.lexer.pop() == LtxLexer.WHITESPACE) {
this.lexer.consume();
@@ -614,6 +665,8 @@
return controlNode;
}
+ String label= controlNode.getText();
+
if (command.supportAsterisk()) {
this.lexer.setReportAsterisk(true);
ASTERISK: while (true) {
@@ -722,7 +775,7 @@
controlNode.command= command;
- if (this.foundEndStackPos >= 0) {
+ if (!exec || this.foundEndStackPos >= 0) {
return controlNode;
}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/TexAst.java b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/TexAst.java
index 0122ce2..3e2b89a 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/TexAst.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/ast/TexAst.java
@@ -95,24 +95,26 @@
}
/**
- * Returns the index in the array for the node at the specified offset
+ * Returns the index in the array for the node at the specified offset.
*
- * @param nodes array with nodes (items can be <code>null</code>)
+ * @param nodes array with nodes (items can be <code>null</code>) ordered by offset
* @param offset the offset of the searched node
- * @return the index in the array if found, otherwise -1
+ * @return the index in the array if found, otherwise, <tt>(-(<i>insertion index</i>) - 1)</tt>
*/
public static int getIndexAt(final @Nullable TexAstNode[] nodes, final int offset) {
+ int insert= 0;
for (int i= 0; i < nodes.length; i++) {
if (nodes[i] != null) {
if (offset < nodes[i].getStartOffset()) {
- return -1;
+ break;
}
if (offset <= nodes[i].getEndOffset()) {
return i;
}
+ insert= i + 1;
}
}
- return -1;
+ return -(insert + 1);
}
/**
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/commands/Argument.java b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/commands/Argument.java
index 678d057..bf06e4d 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/commands/Argument.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/commands/Argument.java
@@ -14,7 +14,11 @@
package org.eclipse.statet.docmlet.tex.core.commands;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+@NonNullByDefault
public final class Argument {
@@ -50,7 +54,7 @@
public static final byte EMBEDDED= (byte) 0xf1;
- private final String label;
+ private final @Nullable String label;
private final byte type;
private final byte content;
@@ -68,6 +72,10 @@
}
+ public @Nullable String getLabel() {
+ return this.label;
+ }
+
public byte getType() {
return this.type;
}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/source/LtxPartitionNodeScanner.java b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/source/LtxPartitionNodeScanner.java
index 86f2766..6522025 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/source/LtxPartitionNodeScanner.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.core/src/org/eclipse/statet/docmlet/tex/core/source/LtxPartitionNodeScanner.java
@@ -23,6 +23,8 @@
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.rules.ICharacterScanner;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
import org.eclipse.statet.ecommons.text.CharacterScannerReader;
import org.eclipse.statet.ecommons.text.core.rules.BufferedDocumentScanner;
import org.eclipse.statet.ecommons.text.core.rules.OperatorRule;
@@ -61,23 +63,24 @@
public class LtxPartitionNodeScanner implements ITreePartitionNodeScanner {
- public static final ITreePartitionNode findLtxRootNode(ITreePartitionNode node) {
+ public static final @Nullable ITreePartitionNode findLtxRootNode(@Nullable ITreePartitionNode node) {
+ ITreePartitionNode ltxNode;
while (true) {
if (node == null) {
return null;
}
if (node.getType() instanceof LtxPartitionNodeType) {
+ ltxNode= node;
break;
}
node= node.getParent();
}
- // (node.getType() instanceof LtxPartitionNodeType)
ITreePartitionNode parentNode;
- while ((parentNode= node.getParent()) != null
+ while ((parentNode= ltxNode.getParent()) != null
&& parentNode.getType() instanceof LtxPartitionNodeType) {
- node= parentNode;
+ ltxNode= parentNode;
}
- return node;
+ return ltxNode;
}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxCommandCompletionProposal.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxCommandCompletionProposal.java
index c623801..2856d74 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxCommandCompletionProposal.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxCommandCompletionProposal.java
@@ -54,6 +54,7 @@
import org.eclipse.statet.docmlet.tex.core.commands.TexCommand;
import org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner;
import org.eclipse.statet.docmlet.tex.ui.TexUIResources;
+import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxArgumentListContextInformation;
import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxAssistInvocationContext;
import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.TexBracketLevel;
import org.eclipse.statet.ltk.ui.sourceediting.assist.SourceProposal;
@@ -81,13 +82,20 @@
super(context, replacementOffset, namePattern);
}
- public LtxCommandProposalParameters(final TexCommand command,
- final LtxAssistInvocationContext context, final int replacementOffset) {
- super(context, replacementOffset, null);
+ public LtxCommandProposalParameters(
+ final LtxAssistInvocationContext context,
+ final TexCommand command) {
+ super(context, context.getInvocationOffset(), 0);
this.command= command;
}
+ /** Only for context information */
+ public LtxCommandProposalParameters(
+ final LtxAssistInvocationContext context, final int replacementOffset) {
+ super(context, replacementOffset, 0);
+ }
+
}
@@ -110,6 +118,34 @@
}
+ public static class ContextInformationProposal extends LtxCommandCompletionProposal {
+
+
+ public ContextInformationProposal(final LtxCommandProposalParameters parameters) {
+ super(parameters);
+ }
+
+
+ @Override
+ public boolean isAutoInsertable() {
+ return true;
+ }
+
+ @Override
+ protected void doApply(final char trigger, final int stateMask,
+ final int caretOffset, final int replacementOffset, final int replacementLength)
+ throws BadLocationException {
+ final ApplyData applyData= getApplyData();
+
+ applyData.clearSelection();
+ applyData.setContextInformation(new LtxArgumentListContextInformation(
+ getReplacementOffset(), // allow negative offsets
+ this.command ));
+ }
+
+ }
+
+
private static class LinkedSepMode implements IDocumentListener, VerifyKeyListener {
private final SourceViewer viewer;
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxElementCompletionComputer.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxElementCompletionComputer.java
index 21012a2..d741f94 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxElementCompletionComputer.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxElementCompletionComputer.java
@@ -48,6 +48,7 @@
import org.eclipse.statet.internal.docmlet.tex.ui.editors.LtxCommandCompletionProposal.LtxCommandProposalParameters;
import org.eclipse.statet.internal.docmlet.tex.ui.editors.TexLabelCompletionProposal.TexLabelProposalParameters;
import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxAssistInvocationContext;
+import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxAssistInvocationContext.CommandCall;
import org.eclipse.statet.ltk.core.LTKUtils;
import org.eclipse.statet.ltk.model.core.elements.IModelElement;
import org.eclipse.statet.ltk.model.core.elements.ISourceStructElement;
@@ -188,11 +189,12 @@
}
else if (context instanceof LtxAssistInvocationContext) {
final LtxAssistInvocationContext texContext= context;
- final int argIdx= texContext.getInvocationArgIdx();
- if (argIdx >= 0) {
- final TexCommand command= texContext.getInvocationControlNode().getCommand();
+ final CommandCall commandCall= texContext.getCommandCall(true);
+ final int argIdx;
+ if (commandCall != null && (argIdx= commandCall.getInvocationArgIdx()) >= 0) {
+ final TexCommand command= commandCall.getCommand();
final Argument argDef= command.getArguments().get(argIdx);
- final TexAstNode argNode= texContext.getInvocationArgNodes()[argIdx];
+ final TexAstNode argNode= commandCall.getArgNode(argIdx);
final int offset= texContext.getInvocationOffset() - prefix.length();
final TextRegion region= TexAst.getInnerRegion(argNode);
if (region != null
@@ -202,7 +204,7 @@
|| (command.getType() & TexCommand.MASK_MAIN) == TexCommand.ENV )) {
final List<String> prefered= new ArrayList<>();
if (command == EnvDefinitions.GENERICENV_end_COMMAND) {
- TexAstNode node= texContext.getInvocationControlNode();
+ TexAstNode node= commandCall.getControlNode();
while (node != null) {
if (node.getNodeType() == NodeType.ENVIRONMENT
&& (prefered.isEmpty()
@@ -240,6 +242,24 @@
@Override
public void computeInformationProposals(final AssistInvocationContext context,
final AssistProposalCollector tenders, final IProgressMonitor monitor) {
+ if (context instanceof LtxAssistInvocationContext) {
+ computeInformationProposals0((LtxAssistInvocationContext)context, tenders, monitor);
+ }
+ }
+
+ protected void computeInformationProposals0(final LtxAssistInvocationContext context,
+ final AssistProposalCollector tenders, final IProgressMonitor monitor) {
+ if (context.getModelInfo() == null) {
+ return;
+ }
+
+ final CommandCall commandCall= context.getCommandCall(false);
+ if (commandCall != null) {
+ final LtxCommandProposalParameters parameters= new LtxCommandProposalParameters(
+ context, commandCall.getControlNode().getArgsStartOffset() );
+ parameters.command= commandCall.getCommand();
+ tenders.add(new LtxCommandCompletionProposal.ContextInformationProposal(parameters));
+ }
}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxSymbolsMenuContributions.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxSymbolsMenuContributions.java
index 556f1b5..c5567a1 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxSymbolsMenuContributions.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/LtxSymbolsMenuContributions.java
@@ -126,7 +126,7 @@
selection.x, contentType,
true, new NullProgressMonitor() );
final LtxCommandCompletionProposal proposal= new LtxCommandCompletionProposal(
- new LtxCommandProposalParameters(command, context, context.getInvocationOffset()) );
+ new LtxCommandProposalParameters(context, command) );
proposal.apply(viewer, (char) 0, 1, context.getInvocationOffset());
selection= proposal.getSelection(viewer.getDocument());
if (selection != null) {
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/TexPathCompletionComputer.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/TexPathCompletionComputer.java
index 417eb26..0969082 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/TexPathCompletionComputer.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/editors/TexPathCompletionComputer.java
@@ -31,6 +31,7 @@
import org.eclipse.statet.docmlet.tex.core.commands.Argument;
import org.eclipse.statet.docmlet.tex.core.commands.TexCommand;
import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxAssistInvocationContext;
+import org.eclipse.statet.internal.docmlet.tex.ui.sourceediting.LtxAssistInvocationContext.CommandCall;
import org.eclipse.statet.ltk.model.core.elements.ISourceUnit;
import org.eclipse.statet.ltk.model.core.elements.IWorkspaceSourceUnit;
import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor;
@@ -95,12 +96,13 @@
protected @Nullable TextRegion getContentRange(final AssistInvocationContext context, final int mode)
throws BadLocationException {
if (context instanceof LtxAssistInvocationContext) {
- final LtxAssistInvocationContext texContext= (LtxAssistInvocationContext) context;
- final int argIdx= texContext.getInvocationArgIdx();
- if (argIdx >= 0) {
- final TexCommand command= texContext.getInvocationControlNode().getCommand();
+ final LtxAssistInvocationContext texContext= (LtxAssistInvocationContext)context;
+ final CommandCall commandCall= texContext.getCommandCall(true);
+ final int argIdx;
+ if (commandCall != null && (argIdx= commandCall.getInvocationArgIdx()) >= 0) {
+ final TexCommand command= commandCall.getCommand();
final Argument argDef= command.getArguments().get(argIdx);
- final TexAstNode argNode= texContext.getInvocationArgNodes()[argIdx];
+ final TexAstNode argNode= commandCall.getArgNode(argIdx);
final int offset= texContext.getInvocationOffset();
if (mode == ContentAssistComputer.SPECIFIC_MODE
|| (argDef.getContent() & 0xf0) == Argument.RESOURCE ) {
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxArgumentListContextInformation.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxArgumentListContextInformation.java
new file mode 100644
index 0000000..d49953f
--- /dev/null
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxArgumentListContextInformation.java
@@ -0,0 +1,117 @@
+/*=============================================================================#
+ # Copyright (c) 2019 Stephan Wahlbrink and others.
+ #
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ #
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ #
+ # Contributors:
+ # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.internal.docmlet.tex.ui.sourceediting;
+
+import org.eclipse.jface.text.contentassist.IContextInformationExtension;
+import org.eclipse.swt.graphics.Image;
+
+import org.eclipse.statet.jcommons.collections.IntArrayList;
+import org.eclipse.statet.jcommons.collections.IntList;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.docmlet.tex.core.commands.Argument;
+import org.eclipse.statet.docmlet.tex.core.commands.TexCommand;
+import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistInformationProposal;
+
+
+@NonNullByDefault
+public class LtxArgumentListContextInformation implements AssistInformationProposal,
+ IContextInformationExtension {
+
+
+ private final int offset;
+
+ private final TexCommand command;
+
+ private final String information;
+ private final int[] informationArgumentIdxs;
+
+
+ public LtxArgumentListContextInformation(final int offset, final TexCommand command) {
+ this.offset= offset;
+ this.command= command;
+
+ final StringBuilder sb= new StringBuilder();
+ final IntList idxs= new IntArrayList();
+ appendArgumentList(sb, idxs, this.command);
+ this.information= sb.toString();
+ this.informationArgumentIdxs= idxs.toArray();
+ }
+
+
+ public int getCallArgsOffset() {
+ return this.offset;
+ }
+
+ public TexCommand getCommand() {
+ return this.command;
+
+ }
+
+
+ @Override
+ public String getContextDisplayString() {
+ return getInformationDisplayString();
+ }
+
+ @Override
+ public @Nullable Image getImage() {
+ return null;
+ }
+
+ @Override
+ public int getContextInformationPosition() {
+ return Math.max(this.offset, 0);
+ }
+
+ @Override
+ public String getInformationDisplayString() {
+ return this.information;
+ }
+
+ public int[] getInformationDisplayStringArgumentIdxs() {
+ return this.informationArgumentIdxs;
+ }
+
+
+ @Override
+ public boolean equals(final @Nullable Object obj) {
+ // prevent stacking of context information at the same position
+ return true;
+ }
+
+
+ private static void appendArgumentList(final StringBuilder text, final IntList idxs, final TexCommand command) {
+ for (final Argument arg : command.getArguments()) {
+ idxs.add(text.length());
+ if ((arg.getType() & Argument.OPTIONAL) != 0) {
+ text.append('[');
+ if (arg.getLabel() != null) {
+ text.append(arg.getLabel());
+ }
+ text.append(']');
+ }
+ else {
+ text.append('{');
+ if (arg.getLabel() != null) {
+ text.append(arg.getLabel());
+ }
+ text.append('}');
+ }
+ }
+ }
+
+}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxAssistInvocationContext.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxAssistInvocationContext.java
index af6a771..4f9fe72 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxAssistInvocationContext.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxAssistInvocationContext.java
@@ -14,6 +14,8 @@
package org.eclipse.statet.internal.docmlet.tex.ui.sourceediting;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
@@ -29,6 +31,7 @@
import org.eclipse.statet.docmlet.tex.core.ast.TexAst;
import org.eclipse.statet.docmlet.tex.core.ast.TexAstNode;
import org.eclipse.statet.docmlet.tex.core.ast.Text;
+import org.eclipse.statet.docmlet.tex.core.commands.TexCommand;
import org.eclipse.statet.docmlet.tex.core.model.TexSourceUnit;
import org.eclipse.statet.docmlet.tex.core.source.LtxHeuristicTokenScanner;
import org.eclipse.statet.ltk.model.core.IModelManager;
@@ -41,11 +44,61 @@
public class LtxAssistInvocationContext extends AssistInvocationContext {
+ public class CommandCall {
+
+
+ private final ControlNode controlNode;
+ private final @Nullable TexAstNode[] argNodes;
+
+ private final int invocationArgIdx;
+
+
+ private CommandCall(final ControlNode controlNode) {
+ nonNullAssert(controlNode.getCommand());
+ this.controlNode= controlNode;
+ this.argNodes= TexAst.resolveArguments(controlNode);
+ this.invocationArgIdx= TexAst.getIndexAt(this.argNodes, getInvocationOffset());
+ }
+
+
+ /**
+ * Returns the node of the command call.
+ * @return the ast node
+ */
+ public final ControlNode getControlNode() {
+ return this.controlNode;
+ }
+
+ @SuppressWarnings("null")
+ public final TexCommand getCommand() {
+ return this.controlNode.getCommand();
+ }
+
+ /**
+ * Returns the node of the argument.
+ * @param argIdx the index of the argument
+ * @return the list with ast nodes
+ */
+ public final @Nullable TexAstNode getArgNode(final int argIdx) {
+ return this.argNodes[argIdx];
+ }
+
+ /**
+ * Returns the index of the argument at the {@link LtxAssistInvocationContext#getInvocationOffset() invocation offset}.
+ * @return the index of the argument
+ */
+ public final int getInvocationArgIdx() {
+ return this.invocationArgIdx;
+ }
+
+ }
+
+
private @Nullable LtxHeuristicTokenScanner scanner;
- private @Nullable ControlNode invoControlNode;
- private @Nullable TexAstNode @Nullable [] invoArgNodes;
- private int invoArgIdx= -2;
+ private int invocationChecked;
+ private @Nullable CommandCall invocationDirect;
+ private @Nullable CommandCall invocationAny;
public LtxAssistInvocationContext(final ISourceEditor editor,
@@ -161,40 +214,48 @@
}
- private void computeInvArgInfo() {
- this.invoArgIdx= -1;
+ private @Nullable CommandCall searchCommandCall(final boolean direct) {
if (getAstSelection().getCovering() instanceof TexAstNode) {
- TexAstNode texNode= (TexAstNode) getAstSelection().getCovering();
- if (texNode instanceof Label || texNode instanceof Text) {
- texNode= texNode.getTexParent();
+ TexAstNode texNode= (TexAstNode)getAstSelection().getCovering();
+ if (direct) {
+ if (texNode instanceof Label || texNode instanceof Text) {
+ texNode= texNode.getTexParent();
+ }
}
+ else {
+ while (texNode != null && !(texNode instanceof Group)) {
+ texNode= texNode.getTexParent();
+ }
+ }
+ ControlNode controlNode;
if (texNode instanceof Group && texNode.getParent() instanceof ControlNode
- && (this.invoControlNode= (ControlNode) texNode.getParent()).getCommand() != null) {
- this.invoArgNodes= TexAst.resolveArguments(this.invoControlNode);
- this.invoArgIdx= TexAst.getIndexAt(this.invoArgNodes, getInvocationOffset());
+ && (controlNode= (ControlNode)texNode.getParent()).getCommand() != null) {
+ return new CommandCall(controlNode);
+ }
+ if (texNode instanceof ControlNode
+ && (controlNode= (ControlNode)texNode).getCommand() != null
+ && controlNode.getArgsStartOffset() <= getStartOffset()
+ && controlNode.getEndOffset() >= getEndOffset() ) {
+ return new CommandCall(controlNode);
}
}
+ return null;
}
- public @Nullable ControlNode getInvocationControlNode() {
- if (this.invoArgIdx == -2) {
- computeInvArgInfo();
+ public @Nullable CommandCall getCommandCall(final boolean direct) {
+ if (this.invocationChecked == 0) {
+ this.invocationChecked= 1;
+ this.invocationDirect= searchCommandCall(true);
}
- return this.invoControlNode;
- }
-
- public @Nullable TexAstNode @Nullable [] getInvocationArgNodes() {
- if (this.invoArgIdx == -2) {
- computeInvArgInfo();
+ if (direct) {
+ return this.invocationDirect;
}
- return this.invoArgNodes;
- }
-
- public int getInvocationArgIdx() {
- if (this.invoArgIdx == -2) {
- computeInvArgInfo();
+ if (this.invocationChecked == 1) {
+ this.invocationChecked= 2;
+ this.invocationAny= (this.invocationDirect != null) ? this.invocationDirect :
+ searchCommandCall(false);
}
- return this.invoArgIdx;
+ return this.invocationAny;
}
}
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContentAssistProcessor.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContentAssistProcessor.java
index 522205a..3718da7 100644
--- a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContentAssistProcessor.java
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContentAssistProcessor.java
@@ -17,6 +17,8 @@
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor;
import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistInvocationContext;
import org.eclipse.statet.ltk.ui.sourceediting.assist.ContentAssist;
@@ -24,6 +26,7 @@
import org.eclipse.statet.ltk.ui.sourceediting.assist.ContentAssistProcessor;
+@NonNullByDefault
public class LtxContentAssistProcessor extends ContentAssistProcessor {
@@ -54,7 +57,7 @@
@Override
protected IContextInformationValidator createContextInformationValidator() {
- return null;
+ return new LtxContextInformationValidator(getEditor());
}
@Override
diff --git a/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContextInformationValidator.java b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContextInformationValidator.java
new file mode 100644
index 0000000..647e805
--- /dev/null
+++ b/docmlet/org.eclipse.statet.docmlet.tex.ui/src/org/eclipse/statet/internal/docmlet/tex/ui/sourceediting/LtxContextInformationValidator.java
@@ -0,0 +1,219 @@
+/*=============================================================================#
+ # Copyright (c) 2019 Stephan Wahlbrink and others.
+ #
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ #
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ #
+ # Contributors:
+ # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.internal.docmlet.tex.ui.sourceediting;
+
+import static org.eclipse.statet.docmlet.tex.core.ast.TexAstStatusConstants.STATUS2_GROUP_NOT_CLOSED;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import org.eclipse.jface.text.AbstractDocument;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.TextPresentation;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.contentassist.IContextInformationPresenter;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.text.core.input.OffsetStringParserInput;
+
+import org.eclipse.statet.ecommons.text.core.FragmentDocument;
+import org.eclipse.statet.ecommons.text.core.treepartitioner.ITreePartitionNode;
+import org.eclipse.statet.ecommons.text.core.treepartitioner.TreePartitionUtils;
+
+import org.eclipse.statet.docmlet.tex.core.TexCore;
+import org.eclipse.statet.docmlet.tex.core.TexProblemConstants;
+import org.eclipse.statet.docmlet.tex.core.ast.ControlNode;
+import org.eclipse.statet.docmlet.tex.core.ast.LtxParser;
+import org.eclipse.statet.docmlet.tex.core.ast.TexAst;
+import org.eclipse.statet.docmlet.tex.core.ast.TexAstNode;
+import org.eclipse.statet.docmlet.tex.core.model.ILtxModelInfo;
+import org.eclipse.statet.docmlet.tex.core.model.TexSourceUnit;
+import org.eclipse.statet.docmlet.tex.core.parser.NowebLtxLexer;
+import org.eclipse.statet.docmlet.tex.core.source.LtxPartitionNodeScanner;
+import org.eclipse.statet.ltk.model.core.elements.ISourceUnit;
+import org.eclipse.statet.ltk.model.core.elements.ISourceUnitModelInfo;
+import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor;
+import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistCompletionInformationProposalWrapper;
+
+
+@NonNullByDefault
+public class LtxContextInformationValidator implements IContextInformationValidator, IContextInformationPresenter {
+
+
+ private final ISourceEditor sourceEditor;
+
+ private @Nullable LtxArgumentListContextInformation info;
+
+ private long scannedArgsStamp;
+ private @Nullable ControlNode scannedArgs;
+
+ private int lastPresentation= -2;
+
+
+ public LtxContextInformationValidator(final ISourceEditor editor) {
+ this.sourceEditor= editor;
+ }
+
+
+ @Override
+ public void install(IContextInformation info, final ITextViewer viewer, final int offset) {
+ if (info instanceof AssistCompletionInformationProposalWrapper) {
+ info= ((AssistCompletionInformationProposalWrapper)info).getContextInformation();
+ }
+
+ this.scannedArgs= null;
+ this.lastPresentation= -2;
+
+ if (info instanceof LtxArgumentListContextInformation
+ && viewer == this.sourceEditor.getViewer()) {
+ this.info= (LtxArgumentListContextInformation)info;
+ }
+ else {
+ this.info= null;
+ return;
+ }
+ }
+
+ @Override
+ public boolean isContextInformationValid(final int offset) {
+ final LtxArgumentListContextInformation info= this.info;
+ if (info == null) {
+ return false;
+ }
+ if (offset < info.getContextInformationPosition()
+ || offset > this.sourceEditor.getViewer().getDocument().getLength()) {
+ return false;
+ }
+ final ControlNode args= getScannedArgs();
+ if (args != null) {
+ return (offset <= args.getEndOffset());
+ }
+ return (offset == info.getContextInformationPosition());
+ }
+
+ @Override
+ public boolean updatePresentation(final int offset, final TextPresentation presentation) {
+ final LtxArgumentListContextInformation info= this.info;
+ if (info == null) {
+ return false;
+ }
+ if (info.getCommand().getArguments().size() > 0) {
+ final int argIdx= getCurrentArgInDef(offset);
+ final int[] idxs= info.getInformationDisplayStringArgumentIdxs();
+ if (argIdx >= 0 && argIdx < idxs.length) {
+ if (argIdx == this.lastPresentation) {
+ return false;
+ }
+ final int start= idxs[argIdx];
+ final int stop= (argIdx + 1 < idxs.length) ? idxs[argIdx + 1] : info.getInformationDisplayString().length();
+ presentation.clear();
+ presentation.addStyleRange(new StyleRange(start, stop - start, null, null, SWT.BOLD));
+ this.lastPresentation= argIdx;
+ return true;
+ }
+ }
+ if (this.lastPresentation >= 0) {
+ presentation.clear();
+ this.lastPresentation= -1;
+ return true;
+ }
+ return false;
+ }
+
+
+ private LtxParser.TaskConfig getParserTaskConfig() {
+ final ISourceUnit sourceUnit= this.sourceEditor.getSourceUnit();
+ if (sourceUnit instanceof TexSourceUnit) {
+ final TexSourceUnit texSourceUnit= (TexSourceUnit)sourceUnit;
+ final LtxParser.TaskConfig config= new LtxParser.TaskConfig(
+ texSourceUnit.getTexCoreAccess().getTexCommandSet() );
+ final ISourceUnitModelInfo modelInfo= texSourceUnit.getModelInfo(null, 0, null);
+ if (modelInfo instanceof ILtxModelInfo) {
+ config.additionalCommands= ((ILtxModelInfo)modelInfo).getCustomCommandMap();
+ config.additionalEnvs= ((ILtxModelInfo)modelInfo).getCustomEnvMap();
+ }
+ return config;
+ }
+ return new LtxParser.TaskConfig(TexCore.getWorkbenchAccess().getTexCommandSet());
+ }
+
+ private @Nullable ControlNode getScannedArgs() {
+ final LtxArgumentListContextInformation info= nonNullAssert(this.info);
+ final int startOffset= info.getCallArgsOffset();
+ AbstractDocument document= (AbstractDocument)this.sourceEditor.getViewer().getDocument();
+ int docStartOffset= startOffset;
+ if (document instanceof FragmentDocument) {
+ final FragmentDocument fragmentDoc= (FragmentDocument)document;
+ document= fragmentDoc.getMasterDocument();
+ docStartOffset+= fragmentDoc.getOffsetInMasterDocument();
+ }
+ if (docStartOffset < 0) {
+ docStartOffset= 0;
+ }
+ final long stamp= document.getModificationStamp();
+ if (this.scannedArgs == null || this.scannedArgsStamp != stamp) {
+ try {
+ ControlNode args= null;
+ final ITreePartitionNode rRootNode= LtxPartitionNodeScanner.findLtxRootNode(
+ TreePartitionUtils.getNode(document, this.sourceEditor.getDocumentContentInfo().getPartitioning(),
+ docStartOffset, true));
+ if (rRootNode != null) {
+ final int docEndOffset= Math.min(0x800, rRootNode.getEndOffset() - docStartOffset);
+ final String text= document.get(docStartOffset, docEndOffset);
+ final LtxParser parser= new LtxParser(new NowebLtxLexer(), null);
+ args= parser.scanControlWordArgs(new OffsetStringParserInput(text, startOffset)
+ .init(startOffset, startOffset + text.length()),
+ info.getCommand(), true, getParserTaskConfig());
+ }
+ this.scannedArgs= args;
+ this.scannedArgsStamp= stamp;
+ }
+ catch (final Exception e) {
+ this.scannedArgs= null;
+ }
+ }
+
+ return this.scannedArgs;
+ }
+
+ private int getCurrentArgInDef(final int offset) {
+ final ControlNode args= getScannedArgs();
+ if (args != null) {
+ @Nullable
+ final TexAstNode[] argNodes= TexAst.resolveArguments(args);
+ int idx= TexAst.getIndexAt(argNodes, offset);
+ // correct selection in between groups
+ if (idx < 0) {
+ idx= -idx - 1;
+ }
+ else if (idx >= 0 && idx + 1 < argNodes.length && argNodes[idx].getEndOffset() == offset) {
+ if ((argNodes[idx].getStatusCode() & TexProblemConstants.MASK_12) != STATUS2_GROUP_NOT_CLOSED) {
+ idx++;
+ }
+ }
+ else if (idx > 0 && argNodes[idx].getStartOffset() == offset) {
+ while (idx > 0 && argNodes[idx - 1] == null
+ && this.info.getCommand().getArguments().get(idx - 1).isOptional()) {
+ idx--;
+ }
+ }
+ return idx;
+ }
+ return -1;
+ }
+
+}