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;
+	}
+	
+}