Bug 574248: [SourceEditor] Add common command support for quick assist
proposals

  - Add command definition, handler and keybinding for quick assist
    Rename in File
  - Update icon for linked rename actions/proposals

Change-Id: I4bd582dd0db9f04534bd08e6dca230065ff173e2
diff --git a/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/linked_rename.png b/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/linked_rename.png
new file mode 100644
index 0000000..502d932
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/linked_rename.png
Binary files differ
diff --git a/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/linked_rename@2x.png b/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/linked_rename@2x.png
new file mode 100644
index 0000000..1d02a84
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/linked_rename@2x.png
Binary files differ
diff --git a/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/text-linked_rename.png b/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/text-linked_rename.png
deleted file mode 100644
index 083e4cc..0000000
--- a/ltk/org.eclipse.statet.ltk.ui/icons/obj_16/text-linked_rename.png
+++ /dev/null
Binary files differ
diff --git a/ltk/org.eclipse.statet.ltk.ui/plugin.properties b/ltk/org.eclipse.statet.ltk.ui/plugin.properties
index a8f5dfb..f2a1970 100644
--- a/ltk/org.eclipse.statet.ltk.ui/plugin.properties
+++ b/ltk/org.eclipse.statet.ltk.ui/plugin.properties
@@ -67,6 +67,8 @@
 commands_CorrectLineWrap_description= Corrects the line wrap of current line/selected lines
 commands_ToggleReportProblemWhenTyping_name= Toggle Report Problems as you type
 commands_ToggleReportProblemWhenTyping_description= Toggles the activation of reporting problems as you type in editors of current type
+commands_QuickAssistRenameInFile_name= Quick Assist - Rename in File
+commands_QuickAssistRenameInFile_description= Links all references for a local rename in the current file
 commands_RefactorRenameInWorkspace_name= Rename in Workspace...
 commands_RefactorRenameInWorkspace_description= Renames the selected identifier in the workspace
 commands_RefactorRenameInSelectedRegion_name= Rename in Selected Region...
diff --git a/ltk/org.eclipse.statet.ltk.ui/plugin.xml b/ltk/org.eclipse.statet.ltk.ui/plugin.xml
index 0c0d6dd..a27d0db 100644
--- a/ltk/org.eclipse.statet.ltk.ui/plugin.xml
+++ b/ltk/org.eclipse.statet.ltk.ui/plugin.xml
@@ -217,8 +217,15 @@
             id="org.eclipse.statet.ltk.commands.ToggleLiveReportProblems"
             categoryId="org.eclipse.statet.workbench.commandCategorys.Source"
             name="%commands_ToggleReportProblemWhenTyping_name"
-            description="%commands_ToggleReportProblemWhenTyping_description"
-            />
+            description="%commands_ToggleReportProblemWhenTyping_description"/>
+      
+      <!-- quick assist -->
+      <command
+            id="org.eclipse.statet.ltk.commands.QuickAssistRenameInFile"
+            categoryId="org.eclipse.statet.workbench.commandCategorys.Source"
+            name="%commands_QuickAssistRenameInFile_name"
+            description="%commands_QuickAssistRenameInFile_description"/>
+      
       <!-- refactor -->
       <command
             id="org.eclipse.statet.ltk.commands.RefactorRenameInWorkspace"
@@ -249,12 +256,13 @@
             disabledIcon="icons/tool_16_d/mark-occurrences.png" />
       <image
             commandId="org.eclipse.statet.workbench.commands.CopyElementName"
-            icon="icons/tool_16/copy-name.png">
-      </image>
+            icon="icons/tool_16/copy-name.png"/>
       <image
             commandId="org.eclipse.statet.workbench.commands.OpenSearchDialog"
-            icon="icons/tool_16/search.gif">
-      </image>
+            icon="icons/tool_16/search.gif"/>
+      <image
+            commandId="org.eclipse.statet.ltk.commands.QuickAssistRenameInFile"
+            icon="icons/obj_16/linked_rename.png"/>
    </extension>
    
    <extension
@@ -413,8 +421,14 @@
             commandId="org.eclipse.ui.edit.text.showInformation"
             contextId="org.eclipse.statet.workbench.contexts.StructuredElementViewer"
             schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
-            sequence="F2">
-      </key>
+            sequence="F2"/>
+      
+      <key
+            commandId="org.eclipse.statet.ltk.commands.QuickAssistRenameInFile"
+            contextId="org.eclipse.statet.workbench.contexts.TextEditor"
+            schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+            sequence="M1+2 R"/>
+      
       <key
             commandId="org.eclipse.statet.ltk.commands.RefactorRenameInWorkspace"
             contextId="org.eclipse.statet.workbench.contexts.TextEditor"
@@ -508,6 +522,18 @@
             </with>
          </activeWhen>
       </handler>
+      <handler
+            commandId="org.eclipse.statet.ltk.commands.QuickAssistRenameInFile">
+         <class class="org.eclipse.statet.ltk.ui.sourceediting.actions.QuickAssistCommandHandler"/>
+         <activeWhen>
+            <with
+                  variable="activePart">
+               <instanceof
+                     value="org.eclipse.statet.ltk.ui.sourceediting.SourceEditor1">
+               </instanceof>
+            </with>
+         </activeWhen>
+      </handler>
    </extension>
    
    <extension
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/LtkUIPlugin.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/LtkUIPlugin.java
index e9f1c92..9f6ae25 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/LtkUIPlugin.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/LtkUIPlugin.java
@@ -138,7 +138,7 @@
 		
 		util.register(LtkUI.OBJ_TEXT_TEMPLATE_IMAGE_ID, ImageRegistryUtil.T_OBJ, "text-template.png"); //$NON-NLS-1$
 		util.register(LtkUI.OBJ_TEXT_AT_TAG_IMAGE_ID, ImageRegistryUtil.T_OBJ, "text-at_tag.png"); //$NON-NLS-1$
-		util.register(LtkUI.OBJ_TEXT_LINKEDRENAME_IMAGE_ID, ImageRegistryUtil.T_OBJ, "text-linked_rename.png"); //$NON-NLS-1$
+		util.register(LtkUI.OBJ_TEXT_LINKEDRENAME_IMAGE_ID, ImageRegistryUtil.T_OBJ, "linked_rename.png"); //$NON-NLS-1$
 	}
 	
 	
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/LtkActions.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/LtkActions.java
index d7c913c..76981c3 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/LtkActions.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/LtkActions.java
@@ -176,6 +176,15 @@
 			"org.eclipse.statet.ltk.commands.CorrectLineWrap"; //$NON-NLS-1$
 	
 	
+	/**
+	 * ID of command 'Quick Assist - Rename in File'.
+	 * 
+	 * Value: @value
+	 */
+	public static final String QUICK_ASSIST_RENAME_IN_FILE=
+			"org.eclipse.statet.ltk.commands.QuickAssistRenameInFile";
+	
+	
 //--Search--
 	
 	/**
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractOpenDeclarationHandler.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractOpenDeclarationHandler.java
index 2826f8b..7ac4ede 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractOpenDeclarationHandler.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractOpenDeclarationHandler.java
@@ -20,7 +20,6 @@
 import org.eclipse.core.runtime.Status;
 import org.eclipse.jface.text.ITextSelection;
 import org.eclipse.osgi.util.NLS;
-import org.eclipse.swt.widgets.Display;
 import org.eclipse.ui.statushandlers.StatusManager;
 
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
@@ -42,19 +41,14 @@
 	
 	
 	@Override
-	public @Nullable Object execute(final ExecutionEvent event) throws ExecutionException {
-		final SourceEditor editor= getSourceEditor(event.getApplicationContext());
-		if (editor == null || !isSupported(editor)) {
-			return null;
-		}
-		
+	protected @Nullable Object execute(final SourceEditor editor, final ExecutionEvent event)
+			throws ExecutionException {
 		final ITextSelection selection= (ITextSelection)editor.getViewer().getSelection();
 		if (execute(editor,
 				JFaceTextRegion.newByStartLength(selection.getOffset(), selection.getLength())) ) {
 			return null;
 		}
-		Display.getCurrent().beep();
-		return null;
+		return ACTION_NOT_AVAILABLE;
 	}
 	
 	protected abstract boolean execute(SourceEditor editor, TextRegion selection);
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractSourceEditorHandler.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractSourceEditorHandler.java
index 7eca4e9..a6399e2 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractSourceEditorHandler.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/AbstractSourceEditorHandler.java
@@ -17,7 +17,10 @@
 import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
 
 import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
 import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.swt.widgets.Display;
 import org.eclipse.ui.IWorkbenchPart;
 
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
@@ -33,6 +36,10 @@
 public abstract class AbstractSourceEditorHandler extends AbstractHandler {
 	
 	
+	protected static final Object ACTION_NOT_AVAILABLE= new Object();
+	protected static final Object DOCUMENT_NOT_EDITABLE= new Object();
+	
+	
 	private final @Nullable SourceEditor editor;
 	
 	
@@ -63,22 +70,57 @@
 		return null;
 	}
 	
-	protected boolean isSupported(final SourceEditor sourceEditor) {
-		final SourceViewer viewer= sourceEditor.getViewer();
-		return (UIAccess.isOkToUse(viewer) && viewer.getDocument() != null);
+	protected boolean isSupported(final SourceEditor editor) {
+		final SourceViewer viewer= editor.getViewer();
+		return (UIAccess.isOkToUse(viewer)
+				&& viewer.getDocument() != null );
+	}
+	
+	protected boolean isEditAction() {
+		return false;
 	}
 	
 	
 	@Override
 	public void setEnabled(@Nullable final Object evaluationContext) {
 		final SourceEditor editor= getSourceEditor(evaluationContext);
-		if (editor == null || !isSupported(editor)) {
-			setBaseEnabled(false);
-			return;
-		}
-		
-		setBaseEnabled(true);
+		setBaseEnabled(editor != null && isSupported(editor)
+				&& (!isEditAction() || editor.isEditable(false)) );
 	}
 	
 	
+	@Override
+	public @Nullable Object execute(final ExecutionEvent event) throws ExecutionException {
+		final SourceEditor editor= getSourceEditor(event.getApplicationContext());
+		if (!(editor != null && isSupported(editor)
+				&& (!isEditAction() || editor.isEditable(false)) )) {
+			return null;
+		}
+		
+		final Object result= execute(editor, event);
+		if (result == ACTION_NOT_AVAILABLE) {
+			onActionNotAvailable();
+			return null;
+		}
+		if (result == DOCUMENT_NOT_EDITABLE) {
+			onDocumentNotEditable();
+			return null;
+		}
+		return result;
+	}
+	
+	protected @Nullable Object execute(final SourceEditor editor,
+			final ExecutionEvent event) throws ExecutionException {
+		return null;
+	}
+	
+	
+	protected void onActionNotAvailable() {
+		Display.getCurrent().beep();
+	}
+	
+	protected void onDocumentNotEditable() {
+		onActionNotAvailable();
+	}
+	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/QuickAssistCommandHandler.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/QuickAssistCommandHandler.java
new file mode 100644
index 0000000..0f3336d
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/actions/QuickAssistCommandHandler.java
@@ -0,0 +1,105 @@
+/*=============================================================================#
+ # Copyright (c) 2021 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.ltk.ui.sourceediting.actions;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.isNull;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullLateInit;
+
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IConfigurationElement;
+import org.eclipse.core.runtime.IExecutableExtension;
+import org.eclipse.jface.text.quickassist.IQuickAssistAssistant;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+import org.eclipse.swt.graphics.Point;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor;
+import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistProposal;
+import org.eclipse.statet.ltk.ui.sourceediting.assist.QuickAssistProcessorCommandExtension;
+
+
+@NonNullByDefault
+public class QuickAssistCommandHandler extends AbstractSourceEditorHandler
+		implements IExecutableExtension {
+	
+	
+	private String commandId;
+	
+	
+	public QuickAssistCommandHandler(final String commandId) {
+		this.commandId= commandId;
+	}
+	
+	public QuickAssistCommandHandler() {
+		this.commandId= nonNullLateInit();
+	}
+	
+	@Override
+	public void setInitializationData(final IConfigurationElement config,
+			final String propertyName, @Nullable final Object data) throws CoreException {
+		if (isNull(this.commandId)) {
+			this.commandId= nonNullAssert(config.getAttribute("commandId")).intern(); //$NON-NLS-1$
+		}
+	}
+	
+	
+	@Override
+	protected boolean isEditAction() {
+		return true;
+	}
+	
+	
+	@Override
+	protected @Nullable Object execute(final SourceEditor editor,
+			final ExecutionEvent event) throws ExecutionException {
+		final var processor= getProcessor(editor);
+		if (processor == null) {
+			return ACTION_NOT_AVAILABLE;
+		}
+		final var document= nonNullAssert(editor.getViewer().getDocument());
+		final var invocationContext= editor.getViewer().getQuickAssistInvocationContext();
+		final AssistProposal proposal= processor.findQuickAssist(invocationContext, this.commandId);
+		if (proposal == null) {
+			return ACTION_NOT_AVAILABLE;
+		}
+		
+		if (isEditAction() && !editor.isEditable(true)) {
+			return DOCUMENT_NOT_EDITABLE;
+		}
+		proposal.apply(editor.getViewer(), (char)0, 0, invocationContext.getOffset());
+		final Point selection= proposal.getSelection(document);
+		if (selection != null) {
+			editor.selectAndReveal(selection.x, selection.y);
+		}
+		return null;
+	}
+	
+	private @Nullable QuickAssistProcessorCommandExtension getProcessor(final SourceEditor editor) {
+		final IQuickAssistAssistant assistant= editor.getViewer().getQuickAssistAssistant();
+		if (assistant != null) {
+			final IQuickAssistProcessor processor= assistant.getQuickAssistProcessor();
+			if (processor instanceof QuickAssistProcessorCommandExtension) {
+				return (QuickAssistProcessorCommandExtension)processor;
+			}
+		}
+		return null;
+	}
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java
index c8d4526..d5a1cc6 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CommandAssistProposal.java
@@ -14,6 +14,10 @@
 
 package org.eclipse.statet.ltk.ui.sourceediting.assist;
 
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import static org.eclipse.statet.ecommons.ui.actions.UIActions.NO_COMMAND_ID;
+
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.jface.bindings.keys.KeySequence;
 import org.eclipse.jface.text.DocumentEvent;
@@ -27,6 +31,8 @@
 
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.text.core.BasicTextRegion;
+import org.eclipse.statet.jcommons.text.core.TextRegion;
 
 import org.eclipse.statet.ecommons.text.ui.DefaultBrowserInformationInput;
 import org.eclipse.statet.ecommons.ui.workbench.WorkbenchUIUtils;
@@ -35,8 +41,79 @@
 
 
 @NonNullByDefault
-public abstract class CommandAssistProposal implements AssistProposal, CommandAccess,
-		ICompletionProposalExtension5, ICompletionProposalExtension6 {
+public abstract class CommandAssistProposal<TContext extends AssistInvocationContext>
+		implements AssistProposal, CommandAccess, ICompletionProposalExtension5, ICompletionProposalExtension6 {
+	
+	
+	public static class ProposalParameters<TContext extends AssistInvocationContext> {
+		
+		
+		public final TContext context;
+		
+		public final String commandId;
+		
+		public final String label;
+		public final @Nullable String description;
+		
+		public int relevance;
+		
+		
+		public ProposalParameters(final TContext context, final String commandId,
+				final String label, final @Nullable String description,
+				final int relevance) {
+			this.context= nonNullAssert(context);
+			this.commandId= nonNullAssert(commandId);
+			this.label= nonNullAssert(label);
+			this.description= description;
+			this.relevance= relevance;
+		}
+		
+		
+	}
+	
+	
+	protected static class ApplyData {
+		
+		
+		private @Nullable TextRegion selectionToSet;
+		
+		private @Nullable IContextInformation contextInformation;
+		
+		
+		public ApplyData() {
+		}
+		
+		
+		public void setSelection(final TextRegion region) {
+			this.selectionToSet= region;
+		}
+		
+		public void setSelection(final int offset) {
+			this.selectionToSet= new BasicTextRegion(offset, offset);
+		}
+		
+		public void setSelection(final int offset, final int length) {
+			assert (length >= 0);
+			this.selectionToSet= new BasicTextRegion(offset, offset + length);
+		}
+		
+		public void clearSelection() {
+			this.selectionToSet= null;
+		}
+		
+		public @Nullable TextRegion getSelection() {
+			return this.selectionToSet;
+		}
+		
+		public void setContextInformation(final IContextInformation info) {
+			this.contextInformation= info;
+		}
+		
+		public @Nullable IContextInformation getContextInformation() {
+			return this.contextInformation;
+		}
+		
+	}
 	
 	
 	protected static StyledString addAcceleratorStyled(final String message,
@@ -51,29 +128,35 @@
 	}
 	
 	
-	private final AssistInvocationContext context;
+	private final TContext context;
 	
 	private final String commandId;
 	
-	private String label;
-	private String description;
+	private final String label;
+	private final @Nullable String description;
 	
-	private int relevance;
+	private final int relevance;
+	
+	private @Nullable ApplyData applyData;
 	
 	
-	protected CommandAssistProposal(final AssistInvocationContext invocationContext,
-			final String commandId) {
-		this.context= invocationContext;
-		this.commandId= commandId;
+	public CommandAssistProposal(final ProposalParameters<TContext> parameters) {
+		this.context= parameters.context;
+		this.commandId= parameters.commandId;
+		this.label= parameters.label;
+		this.description= parameters.description;
+		this.relevance= parameters.relevance;
 	}
 	
-	public CommandAssistProposal(final AssistInvocationContext invocationContext,
-			final String commandId,
-			final String label, final String description) {
-		this(invocationContext, commandId);
+	@SuppressWarnings("null")
+	protected CommandAssistProposal(final TContext invocationContext, final String commandId,
+			final String label, final @Nullable String description) {
+		this.context= nonNullAssert(invocationContext);
+		this.commandId= nonNullAssert(commandId);
 		
 		this.label= label;
 		this.description= description;
+		this.relevance= 0;
 	}
 	
 	
@@ -82,39 +165,21 @@
 		return this.commandId;
 	}
 	
-	protected AssistInvocationContext getInvocationContext() {
+	protected final TContext getInvocationContext() {
 		return this.context;
 	}
 	
-	
-	protected void setLabel(final String label) {
-		this.label= label;
+	protected final ApplyData getApplyData() {
+		ApplyData applyData= this.applyData;
+		if (applyData == null) {
+			applyData= createApplyData();
+			this.applyData= applyData;
+		}
+		return applyData;
 	}
 	
-	protected void setDescription(final String description) {
-		this.description= description;
-	}
-	
-	protected void setRelevance(final int relevance) {
-		this.relevance= relevance;
-	}
-	
-	
-	@Override
-	public boolean validate(final IDocument document, final int offset,
-			final @Nullable DocumentEvent event) {
-		return false;
-	}
-	
-	
-	@Override
-	public void apply(final IDocument document) {
-		throw new UnsupportedOperationException();
-	}
-	
-	@Override
-	public @Nullable Point getSelection(final IDocument document) {
-		return null;
+	protected ApplyData createApplyData() {
+		return new ApplyData();
 	}
 	
 	
@@ -128,6 +193,12 @@
 		return this.label;
 	}
 	
+	
+	@Override
+	public @Nullable Image getImage() {
+		return null;
+	}
+	
 	@Override
 	public String getDisplayString() {
 		return this.label;
@@ -135,12 +206,16 @@
 	
 	@Override
 	public StyledString getStyledDisplayString() {
-		return addAcceleratorStyled(getDisplayString(), WorkbenchUIUtils.getBestKeyBinding(this.commandId));
+		return addAcceleratorStyled(getDisplayString(), (this.commandId != NO_COMMAND_ID) ?
+				WorkbenchUIUtils.getBestKeyBinding(this.commandId) :
+				null );
 	}
 	
+	
 	@Override
-	public @Nullable Image getImage() {
-		return null;
+	public boolean validate(final IDocument document, final int offset,
+			final @Nullable DocumentEvent event) {
+		return false;
 	}
 	
 	@Override
@@ -160,6 +235,23 @@
 	
 	
 	@Override
+	public void apply(final IDocument document) {
+		throw new UnsupportedOperationException();
+	}
+	
+	@Override
+	public @Nullable Point getSelection(final IDocument document) {
+		final var applyData= this.applyData;
+		if (applyData != null) {
+			final TextRegion selection= applyData.getSelection();
+			if (selection != null) {
+				return new Point(selection.getStartOffset(), selection.getLength());
+			}
+		}
+		return null;
+	}
+	
+	@Override
 	public @Nullable IContextInformation getContextInformation() {
 		return null;
 	}
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java
index b8a993f..0909a87 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/LinkedNamesAssistProposal.java
@@ -14,11 +14,8 @@
 
 package org.eclipse.statet.ltk.ui.sourceediting.assist;
 
-import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
-
 import static org.eclipse.statet.ltk.ui.LtkUI.BUNDLE_ID;
 
-import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.jface.text.BadLocationException;
@@ -26,8 +23,6 @@
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.ITextViewer;
 import org.eclipse.jface.text.Position;
-import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5;
-import org.eclipse.jface.text.contentassist.IContextInformation;
 import org.eclipse.jface.text.link.LinkedModeModel;
 import org.eclipse.jface.text.link.LinkedModeUI;
 import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
@@ -45,16 +40,14 @@
 import org.eclipse.statet.jcommons.lang.Nullable;
 import org.eclipse.statet.jcommons.text.core.TextRegion;
 
-import org.eclipse.statet.ecommons.text.ui.DefaultBrowserInformationInput;
-
 import org.eclipse.statet.ltk.ui.LtkUI;
 import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor;
 import org.eclipse.statet.ltk.ui.sourceediting.TextEditToolSynchronizer;
 
 
 @NonNullByDefault
-public abstract class LinkedNamesAssistProposal implements AssistProposal,
-		ICompletionProposalExtension5 {
+public abstract class LinkedNamesAssistProposal<TContext extends AssistInvocationContext>
+		extends CommandAssistProposal<TContext> {
 	
 	
 	/**
@@ -97,25 +90,17 @@
 	}
 	
 	
-	private final AssistInvocationContext context;
-	
-	private String label;
-	private @Nullable String description;
-	private int relevance;
-	
 	private @Nullable String valueSuggestion;
 	
 	
-	@SuppressWarnings("null")
-	public LinkedNamesAssistProposal(final AssistInvocationContext invocationContext) {
-		this.context= nonNullAssert(invocationContext);
+	public LinkedNamesAssistProposal(final ProposalParameters<TContext> parameters) {
+		super(parameters);
 	}
 	
 	
-	protected void init(final String label, final @Nullable String description, final int relevance) {
-		this.label= nonNullAssert(label);
-		this.description= description;
-		this.relevance= relevance;
+	@Override
+	public Image getImage() {
+		return LtkUI.getUIResources().getImage(LtkUI.OBJ_TEXT_LINKEDRENAME_IMAGE_ID);
 	}
 	
 	
@@ -125,8 +110,10 @@
 		return false;
 	}
 	
+	
 	@Override
-	public void apply(final ITextViewer viewer, final char trigger, final int stateMask, final int offset) {
+	public void apply(final ITextViewer viewer, final char trigger, final int stateMask,
+			final int offset) {
 		try {
 			// by default full word is selected by linked model ui
 			// instead we want to keep the original selection by default
@@ -151,7 +138,7 @@
 			model.addGroup(group);
 			
 			model.forceInstall();
-			{	final SourceEditor editor= this.context.getEditor();
+			{	final SourceEditor editor= getInvocationContext().getEditor();
 				final TextEditToolSynchronizer synchronizer;
 				if (editor != null && (synchronizer= editor.getTextEditToolSynchronizer()) != null) {
 					synchronizer.install(model);
@@ -202,7 +189,7 @@
 			final @Nullable Position position, final int idx) throws BadLocationException {
 		if (position != null) {
 			group.addPosition(new LinkedPosition(document, position.getOffset(), position.getLength(), idx));
-			return idx+1;
+			return idx + 1;
 		}
 		return idx;
 	}
@@ -211,64 +198,10 @@
 			final @Nullable TextRegion position, final int idx) throws BadLocationException {
 		if (position != null) {
 			group.addPosition(new LinkedPosition(document, position.getStartOffset(), position.getLength(), idx));
-			return idx+1;
+			return idx + 1;
 		}
 		return idx;
 	}
 	
-	@Override
-	public void apply(final IDocument document) {
-		throw new UnsupportedOperationException();
-	}
-	
-	@Override
-	public @Nullable Point getSelection(final IDocument document) {
-		return null;
-	}
-	
-	
-	@Override
-	public int getRelevance() {
-		return this.relevance;
-	}
-	
-	@Override
-	public String getSortingString() {
-		return this.label;
-	}
-	
-	
-	@Override
-	public String getDisplayString() {
-		return this.label;
-	}
-	
-	@Override
-	public Image getImage() {
-		return LtkUI.getUIResources().getImage(LtkUI.OBJ_TEXT_LINKEDRENAME_IMAGE_ID);
-	}
-	
-	
-	@Override
-	public @Nullable String getAdditionalProposalInfo() {
-		return this.description;
-	}
-	
-	@Override
-	public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
-		final var description= this.description;
-		if (description == null) {
-			return null;
-		}
-		return new DefaultBrowserInformationInput(getDisplayString(),
-				description, DefaultBrowserInformationInput.FORMAT_TEXT_INPUT );
-	}
-	
-	
-	@Override
-	public @Nullable IContextInformation getContextInformation() {
-		return null;
-	}
-	
 	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/MultiContentSectionQuickAssistProcessor.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/MultiContentSectionQuickAssistProcessor.java
index 5ec7093..7ddc202 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/MultiContentSectionQuickAssistProcessor.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/MultiContentSectionQuickAssistProcessor.java
@@ -14,13 +14,21 @@
 
 package org.eclipse.statet.ltk.ui.sourceediting.assist;
 
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
 import java.util.IdentityHashMap;
 import java.util.Map;
 
+import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.contentassist.ICompletionProposal;
 import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
 import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
 import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.ISourceViewer;
+
+import org.eclipse.statet.jcommons.lang.NonNull;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
 
 import org.eclipse.statet.ecommons.text.core.sections.DocContentSections;
 
@@ -34,7 +42,9 @@
  * 
  * @see DocContentSections
  */
-public class MultiContentSectionQuickAssistProcessor implements IQuickAssistProcessor {
+@NonNullByDefault
+public class MultiContentSectionQuickAssistProcessor
+		implements IQuickAssistProcessor, QuickAssistProcessorCommandExtension {
 	
 	
 	private static final Object NULL= new Object();
@@ -44,14 +54,11 @@
 	
 	private final Map<String, Object> processors= new IdentityHashMap<>(8);
 	
-	private String errorMessage;
+	private @Nullable String errorMessage;
 	
 	
 	public MultiContentSectionQuickAssistProcessor(final DocContentSections sections) {
-		if (sections == null) {
-			throw new NullPointerException("sections"); //$NON-NLS-1$
-		}
-		this.sections= sections;
+		this.sections= nonNullAssert(sections);
 	}
 	
 	
@@ -62,7 +69,7 @@
 		this.processors.put(sectionType, (processor != null) ? processor : NULL);
 	}
 	
-	protected final IQuickAssistProcessor getProcessor(final String sectionType) {
+	protected final @Nullable IQuickAssistProcessor getProcessor(final String sectionType) {
 		if (sectionType == DocContentSections.ERROR) {
 			return null;
 		}
@@ -83,10 +90,19 @@
 		return (processor != NULL) ? (IQuickAssistProcessor) processor : null;
 	}
 	
-	protected IQuickAssistProcessor createProcessor(final String sectionType) {
+	protected @Nullable IQuickAssistProcessor createProcessor(final String sectionType) {
 		return null;
 	}
 	
+	public @Nullable IQuickAssistProcessor getProcessor(final IQuickAssistInvocationContext invocationContext) {
+		final ISourceViewer sourceViewer= invocationContext.getSourceViewer();
+		final IDocument document;
+		if (sourceViewer == null || (document= sourceViewer.getDocument()) == null) {
+			return null;
+		}
+		return getProcessor(this.sections.getType(document, invocationContext.getOffset()));
+	}
+	
 	
 	@Override
 	public boolean canFix(final Annotation annotation) {
@@ -98,11 +114,12 @@
 		return false;
 	}
 	
+	
 	@Override
-	public ICompletionProposal[] computeQuickAssistProposals(final IQuickAssistInvocationContext invocationContext) {
+	public @NonNull ICompletionProposal @Nullable [] computeQuickAssistProposals(
+			final IQuickAssistInvocationContext invocationContext) {
 		this.errorMessage= null;
-		final IQuickAssistProcessor processor= getProcessor(
-				this.sections.getType(invocationContext.getSourceViewer().getDocument(), invocationContext.getOffset() ));
+		final IQuickAssistProcessor processor= getProcessor(invocationContext);
 		if (processor != null) {
 			try {
 				return processor.computeQuickAssistProposals(invocationContext);
@@ -115,8 +132,21 @@
 	}
 	
 	@Override
-	public String getErrorMessage() {
+	public @Nullable String getErrorMessage() {
 		return this.errorMessage;
 	}
 	
+	
+	@Override
+	public @Nullable AssistProposal findQuickAssist(
+			final IQuickAssistInvocationContext invocationContext,
+			final String commandId) {
+		final IQuickAssistProcessor processor= getProcessor(invocationContext);
+		if (processor instanceof QuickAssistProcessorCommandExtension) {
+			return ((QuickAssistProcessorCommandExtension)processor)
+					.findQuickAssist(invocationContext, commandId);
+		}
+		return null;
+	}
+	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessor.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessor.java
index dd0a8b3..5ab21e8 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessor.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessor.java
@@ -35,18 +35,19 @@
 import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
 import org.eclipse.jface.text.source.Annotation;
 import org.eclipse.jface.text.source.IAnnotationModel;
-import org.eclipse.jface.text.source.ISourceViewer;
 import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.graphics.Point;
 import org.eclipse.ui.texteditor.spelling.SpellingAnnotation;
 import org.eclipse.ui.texteditor.spelling.SpellingProblem;
 
+import org.eclipse.statet.jcommons.lang.NonNull;
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 import org.eclipse.statet.jcommons.lang.Nullable;
 
 import org.eclipse.statet.ecommons.text.core.util.TextUtils;
 
 import org.eclipse.statet.ltk.model.core.ModelManager;
+import org.eclipse.statet.ltk.ui.sourceediting.CommandAccess;
 import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor;
 
 
@@ -54,7 +55,8 @@
  * LTK quick assistant processor.
  */
 @NonNullByDefault
-public class QuickAssistProcessor implements IQuickAssistProcessor {
+public class QuickAssistProcessor
+		implements IQuickAssistProcessor, QuickAssistProcessorCommandExtension {
 	
 	
 	private static class SpellingProposal implements AssistProposal {
@@ -74,7 +76,7 @@
 				final Method method= this.proposal.getClass().getMethod("getRelevance"); //$NON-NLS-1$
 				final Object value= method.invoke(this.proposal);
 				if (value instanceof Integer) {
-					return ((Integer) value).intValue();
+					return ((Integer)value).intValue();
 				}
 			}
 			catch (final Exception e) {
@@ -102,14 +104,14 @@
 		@Override
 		public void selected(final ITextViewer viewer, final boolean smartToggle) {
 			if (this.proposal instanceof ICompletionProposalExtension2) {
-				((ICompletionProposalExtension2) this.proposal).selected(viewer, smartToggle);
+				((ICompletionProposalExtension2)this.proposal).selected(viewer, smartToggle);
 			}
 		}
 		
 		@Override
 		public void unselected(final ITextViewer viewer) {
 			if (this.proposal instanceof ICompletionProposalExtension2) {
-				((ICompletionProposalExtension2) this.proposal).unselected(viewer);
+				((ICompletionProposalExtension2)this.proposal).unselected(viewer);
 			}
 		}
 		
@@ -122,7 +124,7 @@
 		@Override
 		public boolean validate(final IDocument document, final int offset, final DocumentEvent event) {
 			if (this.proposal instanceof ICompletionProposalExtension2) {
-				return ((ICompletionProposalExtension2) this.proposal).validate(document, offset, event);
+				return ((ICompletionProposalExtension2)this.proposal).validate(document, offset, event);
 			}
 			return false;
 		}
@@ -136,7 +138,7 @@
 		@Override
 		public void apply(final ITextViewer viewer, final char trigger, final int stateMask, final int offset) {
 			if (this.proposal instanceof ICompletionProposalExtension2) {
-				((ICompletionProposalExtension2) this.proposal).apply(viewer, trigger, stateMask, offset);
+				((ICompletionProposalExtension2)this.proposal).apply(viewer, trigger, stateMask, offset);
 			}
 			else {
 				this.proposal.apply(viewer.getDocument());
@@ -209,7 +211,8 @@
 	
 	
 	@Override
-	public ICompletionProposal @Nullable [] computeQuickAssistProposals(final IQuickAssistInvocationContext invocationContext) {
+	public @NonNull ICompletionProposal @Nullable [] computeQuickAssistProposals(
+			final IQuickAssistInvocationContext invocationContext) {
 		this.errorMessage= null;
 		final SubMonitor m= SubMonitor.convert(null, 3 + 10 + 1);
 		
@@ -223,13 +226,9 @@
 			if (context == null) {
 				return null;
 			}
-			final ISourceViewer viewer= context.getSourceViewer();
-			if (viewer == null) {
-				return null;
-			}
 			final AssistProposalCollector proposals= new AssistProposalCollector();
 			
-			final IAnnotationModel model= viewer.getAnnotationModel();
+			final IAnnotationModel model= context.getSourceViewer().getAnnotationModel();
 			if (model != null) {
 				addAnnotationProposals(context, proposals, model);
 				m.worked(5);
@@ -260,7 +259,7 @@
 	 * @param monitor a progress monitor
 	 * @return the list of filtered and sorted proposals, ready for display
 	 */
-	protected AssistProposal[] filterAndSortCompletionProposals(
+	protected @NonNull AssistProposal[] filterAndSortCompletionProposals(
 			final AssistProposalCollector proposals,
 			final AssistInvocationContext context, final IProgressMonitor monitor) {
 		final AssistProposal[] array= proposals.toArray();
@@ -271,10 +270,10 @@
 	}
 	
 	
-	protected boolean isMatchingPosition(final Position pos, final int offset) {
+	protected boolean isMatchingPosition(final @Nullable Position pos, final int offset) {
 		return (pos != null)
 				&& (offset >= pos.getOffset())
-				&& (offset <= pos.getOffset()+pos.getLength());
+				&& (offset <= pos.getOffset() + pos.getLength());
 	}
 	
 	private void addAnnotationProposals(final IQuickAssistInvocationContext invocationContext,
@@ -292,8 +291,8 @@
 					continue;
 				}
 				if (annotation instanceof SpellingAnnotation) {
-					final SpellingProblem problem= ((SpellingAnnotation) annotation).getSpellingProblem();
-					final ICompletionProposal[] annotationProposals= problem.getProposals(invocationContext);
+					final SpellingProblem problem= ((SpellingAnnotation)annotation).getSpellingProblem();
+					final var annotationProposals= problem.getProposals(invocationContext);
 					if (annotationProposals != null && annotationProposals.length > 0) {
 						for (int i= 0; i < annotationProposals.length; i++) {
 							proposals.add(new SpellingProposal(annotationProposals[i]));
@@ -304,7 +303,7 @@
 		}
 	}
 	
-	protected void addModelAssistProposals(final AssistInvocationContext context,
+	protected void addModelAssistProposals(final AssistInvocationContext invocationContext,
 			final AssistProposalCollector proposals, final IProgressMonitor monitor) {
 	}
 	
@@ -313,4 +312,24 @@
 		return this.errorMessage;
 	}
 	
+	
+	@Override
+	public @Nullable AssistProposal findQuickAssist(
+			final IQuickAssistInvocationContext invocationContext,
+			final String commandId) {
+		final var proposals= computeQuickAssistProposals(invocationContext);
+		if (proposals == null) {
+			return null;
+		}
+		for (final var proposal : proposals) {
+			if (proposal instanceof AssistProposal
+					&& proposal instanceof CommandAccess
+					&& ((CommandAccess)proposal).getCommandId().equals(commandId) ) {
+				return (AssistProposal)proposal;
+			}
+		}
+		return null;
+	}
+	
+	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessorCommandExtension.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessorCommandExtension.java
new file mode 100644
index 0000000..626dcbf
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickAssistProcessorCommandExtension.java
@@ -0,0 +1,31 @@
+/*=============================================================================#
+ # Copyright (c) 2021 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.ltk.ui.sourceediting.assist;
+
+import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+
+@NonNullByDefault
+public interface QuickAssistProcessorCommandExtension extends IQuickAssistProcessor {
+	
+	
+	@Nullable AssistProposal findQuickAssist(IQuickAssistInvocationContext context,
+			String commandId);
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/SourceProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/SourceProposal.java
index 1bd71f7..e6faf0f 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/SourceProposal.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/SourceProposal.java
@@ -112,6 +112,7 @@
 		
 	}
 	
+	
 	protected static class ApplyData {