Bug 572862: [SourceEditor] Add QuickRefactoringAssistProposal

Change-Id: I36c204b5f447a537e036dbb8825561c90c5e4a7b
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/QuickRefactoring.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/QuickRefactoring.java
new file mode 100644
index 0000000..29ee62f
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/QuickRefactoring.java
@@ -0,0 +1,44 @@
+/*=============================================================================#
+ # 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.refactoring.core;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.ltk.core.refactoring.Refactoring;
+import org.eclipse.ltk.core.refactoring.TextChange;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+
+@NonNullByDefault
+public abstract class QuickRefactoring extends Refactoring {
+	
+	
+	public abstract String getBundleId();
+	
+	public abstract TextChange createTextChange(
+			SubMonitor m) throws CoreException;
+	
+	
+	protected CoreException handleBadLocation(final BadLocationException e) {
+		return new CoreException(new Status(IStatus.ERROR, getBundleId(),
+				"Unexpected error (concurrent change?)", e ));
+	}
+	
+	
+}
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/RefactoringMessages.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/RefactoringMessages.java
index 241627d..7a57710 100644
--- a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/RefactoringMessages.java
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/RefactoringMessages.java
@@ -16,10 +16,14 @@
 
 import org.eclipse.osgi.util.NLS;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
 
 /**
  * Common refactoring messages.
  */
+@NonNullByDefault
+@SuppressWarnings("null")
 public class RefactoringMessages extends NLS {
 	
 	
diff --git a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/TextChangeCompatibility.java b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/TextChangeCompatibility.java
index 6603dd2..68691c9 100644
--- a/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/TextChangeCompatibility.java
+++ b/ltk/org.eclipse.statet.ltk.core/src/org/eclipse/statet/ltk/refactoring/core/TextChangeCompatibility.java
@@ -14,66 +14,64 @@
 
 package org.eclipse.statet.ltk.refactoring.core;
 
-import org.eclipse.core.runtime.Assert;
 import org.eclipse.ltk.core.refactoring.CategorizedTextEditGroup;
 import org.eclipse.ltk.core.refactoring.GroupCategorySet;
 import org.eclipse.ltk.core.refactoring.TextChange;
 import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
 import org.eclipse.text.edits.MalformedTreeException;
 import org.eclipse.text.edits.MultiTextEdit;
+import org.eclipse.text.edits.RangeMarker;
 import org.eclipse.text.edits.TextEdit;
 import org.eclipse.text.edits.TextEditGroup;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
 
 /**
- * A utility class to provide compatibility with the old
- * text change API of adding text edits directly and auto
- * inserting them into the tree.
+ * A utility class to provide compatibility with the old text change API of adding text edits
+ * directly and auto inserting them into the tree.
  */
+@NonNullByDefault
 public class TextChangeCompatibility {
 	
 	
-	public static void addTextEdit(final TextChange change, final String name, final TextEdit edit) throws MalformedTreeException {
+	public static void addTextEdit(final TextChange change, final String name, final TextEdit edit)
+			throws MalformedTreeException {
 		addTextEdit(change, name, edit, true);
 	}
 	
-	public static void addTextEdit(final TextChange change, final String name, final TextEdit edit, final boolean enable) throws MalformedTreeException {
-		Assert.isNotNull(change);
-		Assert.isNotNull(name);
-		Assert.isNotNull(edit);
-		TextEdit root= change.getEdit();
-		if (root == null) {
-			root= new MultiTextEdit();
-			change.setEdit(root);
-		}
-		insert(root, edit);
-		final TextEditChangeGroup group= new TextEditChangeGroup(change, new TextEditGroup(name, edit));
+	public static void addTextEdit(final TextChange change, final String name, final TextEdit edit,
+			final boolean enable)
+			throws MalformedTreeException {
+		insert(getRoot(change), edit);
+		final TextEditChangeGroup group= new TextEditChangeGroup(change,
+				new TextEditGroup(name, edit) );
 		group.setEnabled(enable);
 		change.addTextEditChangeGroup(group);
 	}
 	
-	public static void addTextEdit(final TextChange change, final String name, final TextEdit edit, final GroupCategorySet groupCategories) throws MalformedTreeException {
-		Assert.isNotNull(change);
-		Assert.isNotNull(name);
-		Assert.isNotNull(edit);
-		TextEdit root= change.getEdit();
-		if (root == null) {
-			root= new MultiTextEdit();
-			change.setEdit(root);
-		}
-		insert(root, edit);
-		change.addTextEditChangeGroup(new TextEditChangeGroup(
-			change,
-			new CategorizedTextEditGroup(name, edit, groupCategories)));
+	public static void addTextEdit(final TextChange change, final String name, final TextEdit edit,
+			final GroupCategorySet groupCategories)
+			throws MalformedTreeException {
+		insert(getRoot(change), edit);
+		final TextEditChangeGroup group= new TextEditChangeGroup(change,
+				new CategorizedTextEditGroup(name, edit, groupCategories) );
+		change.addTextEditChangeGroup(group);
+	}
+	
+	public static void addMarker(final TextChange change, final RangeMarker marker)
+			throws MalformedTreeException {
+		insert(getRoot(change), marker);
 	}
 	
 	
-	public static void insert(final TextEdit parent, final TextEdit edit) throws MalformedTreeException {
+	public static void insert(final TextEdit parent, final TextEdit edit)
+			throws MalformedTreeException {
 		if (!parent.hasChildren()) {
 			parent.addChild(edit);
 			return;
 		}
-		final TextEdit[] children= parent.getChildren();
+		final var children= parent.getChildren();
 		// First dive down to find the right parent.
 		for (int i= 0; i < children.length; i++) {
 			final TextEdit child= children[i];
@@ -96,6 +94,15 @@
 	}
 	
 	
+	private static TextEdit getRoot(final TextChange change) {
+		TextEdit root= change.getEdit();
+		if (root == null) {
+			root= new MultiTextEdit();
+			change.setEdit(root);
+		}
+		return root;
+	}
+	
 	private static boolean covers(final TextEdit thisEdit, final TextEdit otherEdit) {
 		if (thisEdit.getLength() == 0) {	// an insertion point can't cover anything
 			return false;
@@ -104,11 +111,12 @@
 		final int thisEnd= thisEdit.getExclusiveEnd();
 		if (otherEdit.getLength() == 0) {
 			final int otherOffset= otherEdit.getOffset();
-			return thisOffset < otherOffset && otherOffset < thisEnd;
-		} else {
+			return (thisOffset < otherOffset && otherOffset < thisEnd);
+		}
+		else {
 			final int otherOffset= otherEdit.getOffset();
 			final int otherEnd= otherEdit.getExclusiveEnd();
-			return thisOffset <= otherOffset && otherEnd <= thisEnd;
+			return (thisOffset <= otherOffset && otherEnd <= thisEnd);
 		}
 	}
 	
diff --git a/ltk/org.eclipse.statet.ltk.ui/plugin.properties b/ltk/org.eclipse.statet.ltk.ui/plugin.properties
index f2a1970..ba881b9 100644
--- a/ltk/org.eclipse.statet.ltk.ui/plugin.properties
+++ b/ltk/org.eclipse.statet.ltk.ui/plugin.properties
@@ -69,6 +69,8 @@
 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_QuickAssistConvertToPipeForward_name= Quick Assist - Convert to Forward Pipe
+commands_QuickAssistConvertToPipeForward_description= Converts an expression to pipe forward syntax
 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 a27d0db..f51472b 100644
--- a/ltk/org.eclipse.statet.ltk.ui/plugin.xml
+++ b/ltk/org.eclipse.statet.ltk.ui/plugin.xml
@@ -225,6 +225,11 @@
             categoryId="org.eclipse.statet.workbench.commandCategorys.Source"
             name="%commands_QuickAssistRenameInFile_name"
             description="%commands_QuickAssistRenameInFile_description"/>
+      <command
+            id="org.eclipse.statet.ltk.commands.QuickAssistConvertToPipeForward"
+            categoryId="org.eclipse.statet.workbench.commandCategorys.Source"
+            name="%commands_QuickAssistConvertToPipeForward_name"
+            description="%commands_QuickAssistConvertToPipeForward_description"/>
       
       <!-- refactor -->
       <command
@@ -428,6 +433,11 @@
             contextId="org.eclipse.statet.workbench.contexts.TextEditor"
             schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
             sequence="M1+2 R"/>
+      <key
+            commandId="org.eclipse.statet.ltk.commands.QuickAssistConvertToPipeForward"
+            contextId="org.eclipse.statet.workbench.contexts.TextEditor"
+            schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"
+            sequence="M1+2 P"/>
       
       <key
             commandId="org.eclipse.statet.ltk.commands.RefactorRenameInWorkspace"
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/refactoring/TextEditAnnotator.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/refactoring/TextEditAnnotator.java
new file mode 100644
index 0000000..5555e84
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/internal/ltk/ui/refactoring/TextEditAnnotator.java
@@ -0,0 +1,182 @@
+/*=============================================================================#
+ # Copyright (c) 2009, 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.internal.ltk.ui.refactoring;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.text.edits.CopyTargetEdit;
+import org.eclipse.text.edits.DeleteEdit;
+import org.eclipse.text.edits.InsertEdit;
+import org.eclipse.text.edits.MoveSourceEdit;
+import org.eclipse.text.edits.MoveTargetEdit;
+import org.eclipse.text.edits.RangeMarker;
+import org.eclipse.text.edits.ReplaceEdit;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.text.edits.TextEditVisitor;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+
+/**
+ * Class to annotate edits made by a quick fix/assist to be shown via the quick fix pop-up preview.
+ * E.g. the added changes are shown in bold.
+ */
+@NonNullByDefault
+public class TextEditAnnotator extends TextEditVisitor {
+	
+	
+	private int writtenToOffset= 0;
+	private int chunks;
+	
+	private final StringBuilder sb;
+	
+	private final IDocument previewDocument;
+	
+	
+	public TextEditAnnotator(final StringBuilder sb, final IDocument previewDoc) {
+		this.sb= sb;
+		this.previewDocument= previewDoc;
+	}
+	
+	
+	public void unchangedUntil(final int offset) {
+		if (offset > this.writtenToOffset) {
+			appendContent(this.previewDocument, this.writtenToOffset, offset, true);
+			this.writtenToOffset= offset;
+		}
+	}
+	
+	
+	@Override
+	public boolean visit(final MoveTargetEdit edit) {
+		return true; //rangeAdded(edit);
+	}
+	
+	@Override
+	public boolean visit(final CopyTargetEdit edit) {
+		return true; //return rangeAdded(edit);
+	}
+	
+	@Override
+	public boolean visit(final InsertEdit edit) {
+		return rangeAdded(edit);
+	}
+	
+	@Override
+	public boolean visit(final ReplaceEdit edit) {
+		if (edit.getLength() > 0) {
+			return rangeAdded(edit);
+		}
+		return rangeRemoved(edit);
+	}
+	
+	@Override
+	public boolean visit(final MoveSourceEdit edit) {
+		return rangeRemoved(edit);
+	}
+	
+	@Override
+	public boolean visit(final DeleteEdit edit) {
+		return rangeRemoved(edit);
+	}
+	
+	@Override
+	public boolean visit(final RangeMarker edit) {
+		unchangedUntil(edit.getOffset());
+		return true;
+	}
+	
+	
+	protected boolean rangeRemoved(final TextEdit edit) {
+		unchangedUntil(edit.getOffset());
+		return false;
+	}
+	
+	protected boolean rangeAdded(final TextEdit edit) {
+		return annotateEdit(edit, "<b>", "</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+	
+	protected boolean annotateEdit(final TextEdit edit, final String startTag, final String endTag) {
+		unchangedUntil(edit.getOffset());
+		this.sb.append(startTag);
+		appendContent(this.previewDocument, edit.getOffset(), edit.getExclusiveEnd(), false);
+		this.sb.append(endTag);
+		this.writtenToOffset= edit.getExclusiveEnd();
+		return false;
+	}
+	
+	private void appendContent(final IDocument text, final int startOffset, final int endOffset,
+			final boolean surroundLinesOnly) {
+		final int surroundLines= 1;
+		try {
+			final boolean firstChunk= (this.chunks++ == 0);
+			int startLine= text.getLineOfOffset(
+					(surroundLinesOnly && firstChunk) ? endOffset : startOffset );
+			final int endLine= text.getLineOfOffset(endOffset);
+			
+			boolean dotsAdded= false;
+			if (surroundLinesOnly && startOffset == 0) { // no surround lines for the top no-change range
+				startLine= endLine;
+				dotsAdded= true;
+			}
+			
+			for (int i= startLine; i <= endLine; i++) {
+				if (surroundLinesOnly) {
+					if ((i - startLine > surroundLines) && (endLine - i > surroundLines)) {
+						if (!dotsAdded) {
+							this.sb.append("...<br/>"); //$NON-NLS-1$
+							dotsAdded= true;
+						}
+						else if (endOffset == text.getLength()) {
+							return; // no surround lines for the bottom no-change range
+						}
+						continue;
+					}
+				}
+				
+				final IRegion lineInfo= text.getLineInformation(i);
+				final int start= lineInfo.getOffset();
+				final int end= start + lineInfo.getLength();
+				
+				final int from= Math.max(start, startOffset);
+				final int to= Math.min(end, endOffset);
+				final String content= text.get(from, to - from);
+				if (surroundLinesOnly && (from == start) && content.isEmpty()) {
+					continue; // ignore empty lines except when range started in the middle of a line
+				}
+				for (int k= 0; k < content.length(); k++) {
+					final char ch= content.charAt(k);
+					switch (ch) {
+					case '<':
+						this.sb.append("&lt;"); //$NON-NLS-1$
+						break;
+					case '>':
+						this.sb.append("&gt;"); //$NON-NLS-1$
+						break;
+					default:
+						this.sb.append(ch);
+						break;
+					}
+				}
+				if (to == end && to != endOffset) { // new line when at the end of the line, and not end of range
+					this.sb.append("<br/>"); //$NON-NLS-1$
+				}
+			}
+		}
+		catch (final BadLocationException e) {}
+	}
+	
+}
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 76981c3..0a19ae1 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
@@ -184,6 +184,14 @@
 	public static final String QUICK_ASSIST_RENAME_IN_FILE=
 			"org.eclipse.statet.ltk.commands.QuickAssistRenameInFile";
 	
+	/**
+	 * ID of command 'Quick Assist - Convert to Pipe Forward Statement'.
+	 * 
+	 * Value: @value
+	 */
+	public static final String QUICK_ASSIST_CONVERT_TO_PIPE_FORWARD=
+			"org.eclipse.statet.ltk.commands.QuickAssistConvertToPipeForward";
+	
 	
 //--Search--
 	
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/refactoring/RefactoringExecutionHelper.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/refactoring/RefactoringExecutionHelper.java
index 49d7431..d5c2781 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/refactoring/RefactoringExecutionHelper.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/refactoring/RefactoringExecutionHelper.java
@@ -14,6 +14,8 @@
 
 package org.eclipse.statet.ltk.ui.refactoring;
 
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
 import java.lang.reflect.InvocationTargetException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
@@ -29,7 +31,6 @@
 import org.eclipse.jface.dialogs.Dialog;
 import org.eclipse.jface.dialogs.IDialogConstants;
 import org.eclipse.jface.dialogs.MessageDialog;
-import org.eclipse.jface.operation.IRunnableWithProgress;
 import org.eclipse.jface.text.Position;
 import org.eclipse.ltk.core.refactoring.Change;
 import org.eclipse.ltk.core.refactoring.CompositeChange;
@@ -44,6 +45,9 @@
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.ui.progress.IProgressService;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
 import org.eclipse.statet.internal.ltk.ui.refactoring.Messages;
 import org.eclipse.statet.ltk.model.core.element.SourceUnit;
 import org.eclipse.statet.ltk.refactoring.core.ScheduledRefactoring;
@@ -55,6 +59,7 @@
  * undo change onto the undo stack and folding editor edits into one editor
  * undo object.
  */
+@NonNullByDefault
 public class RefactoringExecutionHelper {
 	
 	
@@ -62,28 +67,25 @@
 	
 	private final IProgressService execContext;
 	
-	private final Shell parent;
+	private final Shell parentShell;
 	private final int stopSeverity;
 	
-	private SourceUnit insertPositionSourceUnit;
-	private Position insertPosition;
+	private @Nullable SourceUnit insertPositionSourceUnit;
+	private @Nullable Position insertPosition;
 	
 	
 	/**
 	 * @param refactoring
 	 * @param stopSeverity a refactoring status constant from {@link RefactoringStatus}
-	 * @param parent
+	 * @param parentShell
 	 * @param context
 	 */
-	public RefactoringExecutionHelper(final Refactoring refactoring, final int stopSeverity, final Shell parent, final IProgressService context) {
-		assert (refactoring != null);
-		assert (parent != null);
-		assert (context != null);
-		
-		this.refactoring= refactoring;
+	public RefactoringExecutionHelper(final Refactoring refactoring, final int stopSeverity,
+			final Shell parentShell, final IProgressService context) {
+		this.refactoring= nonNullAssert(refactoring);
 		this.stopSeverity= stopSeverity;
-		this.parent= parent;
-		this.execContext= context;
+		this.parentShell= nonNullAssert(parentShell);
+		this.execContext= nonNullAssert(context);
 	}
 	
 	
@@ -91,138 +93,128 @@
 	 * Must be called in the UI thread.<br>
 	 * <strong>Use {@link #perform(boolean, boolean)} unless you know exactly what you are doing!</strong>
 	 * 
-	 * @param forkChangeExecution if the change should not be executed in the UI thread: This may not work in any case 
+	 * @param forkChangeExecution if the change should not be executed in the UI thread: This may not work in any case
 	 * @param cancelable  if set, the operation will be cancelable
 	 * @throws InterruptedException thrown when the operation is cancelled
 	 * @throws InvocationTargetException thrown when the operation failed to execute
 	 */
-	public void perform(final boolean forkChangeExecution, final boolean cancelable) throws InterruptedException, InvocationTargetException {
+	public void perform(final boolean forkChangeExecution, final boolean cancelable)
+			throws InterruptedException, InvocationTargetException {
 		final AtomicReference<PerformChangeOperation> op= new AtomicReference<>();
 		try {
-			this.execContext.busyCursorWhile(new IRunnableWithProgress() {
-				@Override
-				public void run(final IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
-					final SubMonitor m= SubMonitor.convert(monitor, RefactoringExecutionHelper.this.refactoring.getName(), 10);
+			this.execContext.busyCursorWhile((final IProgressMonitor monitor) -> {
+				final SubMonitor m= SubMonitor.convert(monitor, this.refactoring.getName(), 10);
+				
+				final IJobManager manager=  Job.getJobManager();
+				final Thread workingThread= Thread.currentThread();
+				final ISchedulingRule rule= (this.refactoring instanceof ScheduledRefactoring) ?
+						((ScheduledRefactoring)this.refactoring).getSchedulingRule() :
+						ResourcesPlugin.getWorkspace().getRoot();
+				
+				manager.beginRule(rule, m.newChild(1));
+				try {
+					if (cancelable && monitor.isCanceled()) {
+						throw new InterruptedException();
+					}
 					
-					final IJobManager manager=  Job.getJobManager();
-					final Thread workingThread= Thread.currentThread();
-					final ISchedulingRule rule= (RefactoringExecutionHelper.this.refactoring instanceof ScheduledRefactoring) ?
-							((ScheduledRefactoring) RefactoringExecutionHelper.this.refactoring).getSchedulingRule() :
-							ResourcesPlugin.getWorkspace().getRoot();
+					this.refactoring.setValidationContext(this.parentShell);
 					
-					manager.beginRule(rule, m.newChild(1));
-					PerformChangeOperation operation= null;
-					try {
-						if (cancelable && monitor.isCanceled()) {
-							throw new InterruptedException();
+					final RefactoringStatus status= this.refactoring.checkAllConditions(
+							m.newChild(1, SubMonitor.SUPPRESS_NONE));
+					if (status.getSeverity() >= this.stopSeverity) {
+						final AtomicBoolean canceled= new AtomicBoolean();
+						this.parentShell.getDisplay().syncExec(() -> {
+							final Dialog dialog= RefactoringUI.createRefactoringStatusDialog(
+									status, this.parentShell, this.refactoring.getName(), false );
+							final int selection= dialog.open();
+							canceled.set(selection == IDialogConstants.CANCEL_ID);
+						});
+						if (canceled.get()) {
+							throw new OperationCanceledException();
 						}
-						
-						RefactoringExecutionHelper.this.refactoring.setValidationContext(RefactoringExecutionHelper.this.parent);
-						
-						final RefactoringStatus status= RefactoringExecutionHelper.this.refactoring.checkAllConditions(
-								m.newChild(1, SubMonitor.SUPPRESS_NONE));
-						if (status.getSeverity() >= RefactoringExecutionHelper.this.stopSeverity) {
-							final AtomicBoolean canceled= new AtomicBoolean();
-							RefactoringExecutionHelper.this.parent.getDisplay().syncExec(new Runnable() {
-								@Override
-								public void run() {
-									final Dialog dialog= RefactoringUI.createRefactoringStatusDialog(status, RefactoringExecutionHelper.this.parent, RefactoringExecutionHelper.this.refactoring.getName(), false);
-									final int selection= dialog.open();
-									canceled.set(selection == IDialogConstants.CANCEL_ID);
-								}
-							});
-							if (canceled.get()) {
-								throw new OperationCanceledException();
+					}
+					
+					final Change change= this.refactoring.createChange(
+							m.newChild(2, SubMonitor.SUPPRESS_NONE) );
+					change.initializeValidationData(
+							m.newChild(1, SubMonitor.SUPPRESS_NONE) );
+					
+					final SourceUnitChange insertTracker= (this.insertPositionSourceUnit != null) ?
+							search(change) : null;
+					
+					final var operation= new PerformChangeOperation(change);
+					operation.setUndoManager(RefactoringCore.getUndoManager(), this.refactoring.getName());
+					operation.setSchedulingRule(rule);
+					
+					if (cancelable && monitor.isCanceled()) {
+						throw new InterruptedException();
+					}
+					
+					op.set(operation);
+					if (forkChangeExecution) {
+						operation.run(m.newChild(4, SubMonitor.SUPPRESS_NONE));
+					}
+					else {
+						final AtomicReference<Exception> opException= new AtomicReference<>();
+						final Display display= this.parentShell.getDisplay();
+						manager.transferRule(rule, display.getThread());
+						display.syncExec(() ->  {
+							try {
+								operation.run(m.newChild(4, SubMonitor.SUPPRESS_NONE));
+							}
+							catch (final CoreException | RuntimeException e) {
+								opException.set(e);
+							}
+							finally {
+								manager.transferRule(rule, workingThread);
+							}
+						});
+						if (opException.get() != null) {
+							final Exception e= opException.get();
+							if (e instanceof CoreException) {
+								throw (CoreException)e;
+							}
+							if (e instanceof RuntimeException) {
+								throw (RuntimeException)e;
 							}
 						}
-						
-						final Change change= RefactoringExecutionHelper.this.refactoring.createChange(
-								m.newChild(2, SubMonitor.SUPPRESS_NONE) );
-						change.initializeValidationData(
-								m.newChild(1, SubMonitor.SUPPRESS_NONE) );
-						
-						final SourceUnitChange insertTracker= (RefactoringExecutionHelper.this.insertPositionSourceUnit != null) ?
-								search(change) : null;
-						
-						operation= new PerformChangeOperation(change);
-						operation.setUndoManager(RefactoringCore.getUndoManager(), RefactoringExecutionHelper.this.refactoring.getName());
-						operation.setSchedulingRule(rule);
-						
-						if (cancelable && monitor.isCanceled()) {
-							throw new InterruptedException();
-						}
-						op.set(operation);
-						
-						if (forkChangeExecution) {
-							operation.run(m.newChild(4, SubMonitor.SUPPRESS_NONE));
-						}
-						else {
-							final AtomicReference<Exception> opException= new AtomicReference<>();
-							final Runnable runnable= new Runnable() {
-								@Override
-								public void run() {
-									try {
-										final PerformChangeOperation operation= op.get();
-										operation.run(m.newChild(4, SubMonitor.SUPPRESS_NONE));
-									}
-									catch (final CoreException | RuntimeException e) {
-										opException.set(e);
-									}
-									finally {
-										manager.transferRule(rule, workingThread);
-									}
-								}
-							};
-							final Display display= RefactoringExecutionHelper.this.parent.getDisplay();
-							manager.transferRule(rule, display.getThread());
-							display.syncExec(runnable);
-							if (opException.get() != null) {
-								final Exception e= opException.get();
-								if (e instanceof CoreException) {
-									throw (CoreException) e;
-								}
-								if (e instanceof RuntimeException) {
-									throw (RuntimeException) e;
-								}
-							}
-						}
-						
-						final RefactoringStatus validationStatus= operation.getValidationStatus();
-						if (validationStatus != null && validationStatus.hasFatalError()) {
-							MessageDialog.openError(RefactoringExecutionHelper.this.parent, RefactoringExecutionHelper.this.refactoring.getName(), NLS.bind(
-									Messages.ExecutionHelper_CannotExecute_message, 
-									validationStatus.getMessageMatchingSeverity(RefactoringStatus.FATAL) ));
-							return;
-						}
-						
-						if (insertTracker != null) {
-							RefactoringExecutionHelper.this.insertPosition= insertTracker.getInsertPosition();
-						}
 					}
-					catch (final OperationCanceledException e) {
-						throw new InterruptedException(e.getMessage());
+					
+					final RefactoringStatus validationStatus= operation.getValidationStatus();
+					if (validationStatus != null && validationStatus.hasFatalError()) {
+						MessageDialog.openError(this.parentShell, this.refactoring.getName(), NLS.bind(
+								Messages.ExecutionHelper_CannotExecute_message,
+								validationStatus.getMessageMatchingSeverity(RefactoringStatus.FATAL) ));
+						return;
 					}
-					catch (final CoreException | RuntimeException e) {
-						throw new InvocationTargetException(e);
-					}
-					finally {
-						manager.endRule(rule);
-						RefactoringExecutionHelper.this.refactoring.setValidationContext(null);
+					
+					if (insertTracker != null) {
+						this.insertPosition= insertTracker.getInsertPosition();
 					}
 				}
-				
+				catch (final OperationCanceledException e) {
+					throw new InterruptedException(e.getMessage());
+				}
+				catch (final CoreException | RuntimeException e) {
+					throw new InvocationTargetException(e);
+				}
+				finally {
+					manager.endRule(rule);
+					this.refactoring.setValidationContext(null);
+				}
 			});
 		}
 		catch (final InvocationTargetException e) {
 			final PerformChangeOperation operation= op.get();
 			if (operation != null && operation.changeExecutionFailed()) {
-				final ChangeExceptionHandler handler= new ChangeExceptionHandler(this.parent, this.refactoring);
+				final ChangeExceptionHandler handler= new ChangeExceptionHandler(this.parentShell,
+						this.refactoring );
 				final Throwable inner= e.getTargetException();
 				if (inner instanceof RuntimeException) {
-					handler.handle(operation.getChange(), (RuntimeException) inner);
+					handler.handle(operation.getChange(), (RuntimeException)inner);
 				}
 				else if (inner instanceof CoreException) {
-					handler.handle(operation.getChange(), (CoreException) inner);
+					handler.handle(operation.getChange(), (CoreException)inner);
 				}
 				else {
 					throw e;
@@ -240,18 +232,18 @@
 		this.insertPositionSourceUnit= su;
 	}
 	
-	public Position getInsertPosition() {
+	public @Nullable Position getInsertPosition() {
 		return this.insertPosition;
 	}
 	
-	private SourceUnitChange search(final Change change) {
+	private @Nullable SourceUnitChange search(final Change change) {
 		if (change instanceof SourceUnitChange) {
-			if (((SourceUnitChange) change).getSourceUnit() == this.insertPositionSourceUnit) {
-				return (SourceUnitChange) change;
+			if (((SourceUnitChange)change).getSourceUnit() == this.insertPositionSourceUnit) {
+				return (SourceUnitChange)change;
 			}
 		}
 		if (change instanceof CompositeChange) {
-			final Change[] children= ((CompositeChange) change).getChildren();
+			final var children= ((CompositeChange)change).getChildren();
 			for (int i= 0; i < children.length; i++) {
 				final SourceUnitChange child= search(children[i]);
 				if (child != null) {
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickRefactoringAssistProposal.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickRefactoringAssistProposal.java
new file mode 100644
index 0000000..d4f5512
--- /dev/null
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/QuickRefactoringAssistProposal.java
@@ -0,0 +1,154 @@
+/*=============================================================================#
+ # 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 static org.eclipse.statet.ltk.ui.LtkUI.BUNDLE_ID;
+
+import java.lang.reflect.InvocationTargetException;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Position;
+import org.eclipse.ltk.core.refactoring.TextChange;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.ui.progress.IProgressService;
+import org.eclipse.ui.statushandlers.StatusManager;
+
+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.ui.DefaultBrowserInformationInput;
+
+import org.eclipse.statet.internal.ltk.ui.refactoring.TextEditAnnotator;
+import org.eclipse.statet.ltk.model.core.element.SourceUnit;
+import org.eclipse.statet.ltk.refactoring.core.QuickRefactoring;
+import org.eclipse.statet.ltk.ui.refactoring.RefactoringExecutionHelper;
+import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor;
+
+
+@NonNullByDefault
+public class QuickRefactoringAssistProposal<TContext extends AssistInvocationContext>
+		extends CommandAssistProposal<TContext> {
+	
+	
+	private final QuickRefactoring refactoring;
+	
+	
+	public QuickRefactoringAssistProposal(final ProposalParameters<TContext> parameters,
+			final QuickRefactoring refactoring) {
+		super(parameters);
+		this.refactoring= refactoring;
+		
+		check();
+	}
+	
+	public QuickRefactoringAssistProposal(final TContext invocationContext, final String commandId,
+			final String label,
+			final QuickRefactoring refactoring) {
+		super(invocationContext, commandId, label, null);
+		this.refactoring= refactoring;
+		
+		check();
+	}
+	
+	protected void check() {
+		if (getInvocationContext().getSourceUnit() == null) {
+			throw new IllegalArgumentException();
+		}
+	}
+	
+	
+	protected QuickRefactoring getRefactoring() {
+		return this.refactoring;
+	}
+	
+	
+	@Override
+	public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
+		final SubMonitor m= SubMonitor.convert(monitor, 2 + 2);
+		try {
+			final StringBuilder sb= new StringBuilder();
+			final TextChange change= this.refactoring.createTextChange(m.newChild(2));
+			change.setKeepPreviewEdits(true);
+			final IDocument previewDocument= change.getPreviewDocument(m.newChild(1));
+			final TextEdit rootEdit= change.getPreviewEdit(change.getEdit());
+			final TextEditAnnotator ea= new TextEditAnnotator(sb, previewDocument);
+			rootEdit.accept(ea); m.worked(1);
+			return new DefaultBrowserInformationInput(getDisplayString(),
+					sb.toString(), DefaultBrowserInformationInput.FORMAT_HTMLSOURCE_INPUT,
+					getInvocationContext().getTabSize() );
+		}
+		catch (final CoreException e) {
+			StatusManager.getManager().handle(new Status(IStatus.ERROR, BUNDLE_ID, -1,
+					String.format("An error occured when generating the preview for quick assist '%1$s'.",
+							getDisplayString() ),
+					e ));
+			return null;
+		}
+		finally {
+			m.done();
+		}
+	}
+	
+	
+	@Override
+	public void apply(final ITextViewer viewer,
+			final char trigger, final int stateMask, final int offset) {
+		final var context= getInvocationContext();
+		@SuppressWarnings("null")
+		final @NonNull SourceUnit sourceUnit= context.getSourceUnit();
+		final SourceEditor editor= context.getEditor();
+		final var textWidget= editor.getViewer().getTextWidget();
+		if (textWidget == null) {
+			return;
+		}
+		
+		IProgressService execContext= null;
+		final var serviceLocator= editor.getServiceLocator();
+		if (serviceLocator != null) {
+			execContext= serviceLocator.getService(IProgressService.class);
+		}
+		if (execContext == null) {
+			return;
+		}
+		final var applyData= getApplyData();
+		try {
+			final var refactoring= this.refactoring;
+			final var helper= new RefactoringExecutionHelper(refactoring,
+					offset, textWidget.getShell(), execContext );
+			helper.enableInsertPosition(sourceUnit);
+			helper.perform(false, false);
+			
+			final Position position= helper.getInsertPosition();
+			if (position != null) {
+				applyData.setSelection(position.getOffset());
+			}
+		}
+		catch (final InterruptedException e) {}
+		catch (final InvocationTargetException e) {
+			StatusManager.getManager().handle(new Status(IStatus.ERROR, BUNDLE_ID, -1,
+					String.format("An error occured when executing quick assist '%1$s'.",
+							getDisplayString() ),
+					e ));
+		}
+	}
+	
+}