Bug 521484 - Async content-assist can freeze UI Thread

Added async proposal filtering to avoid defaulting to sync proposal
retrieval

Change-Id: Ibdc3767359e7fe6f4ff8d52d8cd895e1ddea04ae
Signed-off-by: Lucas Bullen <lbullen@redhat.com>
Also-By: Mickael Istria <mistria@redhat.com>
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/AsyncCompletionProposalPopup.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/AsyncCompletionProposalPopup.java
index 1dafdf5..61a1164 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/AsyncCompletionProposalPopup.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/AsyncCompletionProposalPopup.java
@@ -32,6 +32,7 @@
 import org.eclipse.jface.contentassist.IContentAssistSubjectControl;
 
 import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.ITextViewer;
 import org.eclipse.jface.text.TextUtilities;
@@ -215,7 +216,37 @@
 
 		return getErrorMessage();
 	}
-	
+
+	@Override
+	List<ICompletionProposal> computeProposals(int offset) {
+		fProposalShell.dispose();
+		showProposals(true);
+		return fComputedProposals;
+	}
+
+	@Override
+	void createProposalSelector() {
+		super.createProposalSelector();
+		fProposalShell.addDisposeListener(e -> hide());
+	}
+
+	@Override
+	protected List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) {
+		if(fComputedProposals.size() > 0 && fComputedProposals.get(0) instanceof ComputingProposal) {
+			Set<CompletableFuture<List<ICompletionProposal>>> remaining = Collections.synchronizedSet(new HashSet<>(fFutures));
+			for (CompletableFuture<List<ICompletionProposal>> future : fFutures) {
+				future.thenRun(() -> {
+					remaining.removeIf(CompletableFuture::isDone);
+					if (remaining.isEmpty()) {
+						filterProposals();
+					}
+				});
+			}
+			return fComputedProposals;
+		}
+		return super.computeFilteredProposals(offset, event);
+	}
+
 	@Override
 	public void hide() {
 		super.hide();
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/CompletionProposalPopup.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/CompletionProposalPopup.java
index c47118d..93fa19b 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/CompletionProposalPopup.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/CompletionProposalPopup.java
@@ -552,7 +552,7 @@
 	 * @param offset the offset
 	 * @return the completion proposals available at this offset, never null
 	 */
-	private List<ICompletionProposal> computeProposals(int offset) {
+	List<ICompletionProposal> computeProposals(int offset) {
 		ICompletionProposal[] completionProposals;
 		if (fContentAssistSubjectControl != null) {
 			completionProposals= fContentAssistant.computeCompletionProposals(fContentAssistSubjectControl, offset);
@@ -1460,7 +1460,7 @@
 	 * Filters the displayed proposal based on the given cursor position and the
 	 * offset of the original invocation of the content assistant.
 	 */
-	private void filterProposals() {
+	void filterProposals() {
 		if (!fIsFilterPending) {
 			fIsFilterPending= true;
 			Control control= fContentAssistSubjectControlAdapter.getControl();
@@ -1477,7 +1477,7 @@
 	 * @return the set of filtered proposals
 	 * @since 3.0
 	 */
-	private List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) {
+	List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) {
 
 		if (offset == fInvocationOffset && event == null) {
 			fIsFilteredSubset= false;
diff --git a/org.eclipse.ui.genericeditor.tests/plugin.xml b/org.eclipse.ui.genericeditor.tests/plugin.xml
index c7ce8e9..ea8d49a 100644
--- a/org.eclipse.ui.genericeditor.tests/plugin.xml
+++ b/org.eclipse.ui.genericeditor.tests/plugin.xml
@@ -20,7 +20,7 @@
 		</contentAssistProcessor>
   <contentAssistProcessor
         class="org.eclipse.ui.genericeditor.tests.contributions.LongRunningBarContentAssistProcessor"
-        contentType="org.eclipse.ui.genericeditor.tests.content-type">
+        contentType="org.eclipse.ui.genericeditor.tests.specialized-content-type">
   </contentAssistProcessor>
  </extension>
  <extension
diff --git a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java
index 97b5ef1..7ed46a8 100644
--- a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java
+++ b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java
@@ -19,8 +19,13 @@
 
 import org.junit.Test;
 
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ST;
+import org.eclipse.swt.custom.StyledText;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
 import org.eclipse.swt.widgets.Shell;
 import org.eclipse.swt.widgets.Table;
 import org.eclipse.swt.widgets.TableItem;
@@ -80,12 +85,55 @@
 		TableItem otherProposalItem = completionProposalList.getItem(1);
 		assertEquals(LongRunningBarContentAssistProcessor.PROPOSAL, ((ICompletionProposal)otherProposalItem.getData()).getDisplayString());
 		assertEquals("Addition of completion proposal should keep selection", completionProposal, completionProposalList.getSelection()[0].getData());
-		
+
 		// TODO find a way to actually trigger completion and verify result against Editor content
 		// Assert.assertEquals("Completion didn't complete", "bars are good for a beer.", ((StyledText)editor.getAdapter(Control.class)).getText());
 		completionShell.close();
 	}
 
+	@Test
+	public void testCompletionFreeze_bug521484() throws Exception {
+		Set<Shell> beforeShell = new HashSet<>(Arrays.asList(Display.getDefault().getShells()));
+		editor.selectAndReveal(3, 0);
+		ContentAssistAction action = (ContentAssistAction) editor.getAction(ITextEditorActionConstants.CONTENT_ASSIST);
+		action.update();
+		action.run();
+		waitAndDispatch(100);
+		Set<Shell> afterShell = new HashSet<>(Arrays.asList(Display.getDefault().getShells()));
+		afterShell.removeAll(beforeShell);
+		assertEquals("No completion", 1, afterShell.size());
+		Shell completionShell= afterShell.iterator().next();
+		final Table completionProposalList = findCompletionSelectionControl(completionShell);
+		// should be instantaneous, but happens to go asynchronous on CI so let's allow a wait
+		new DisplayHelper() {
+			@Override
+			protected boolean condition() {
+				return completionProposalList.getItemCount() == 2;
+			}
+		}.waitForCondition(completionShell.getDisplay(), 200);
+		assertEquals(2, completionProposalList.getItemCount());
+		final TableItem computingItem = completionProposalList.getItem(0);
+		assertTrue("Missing computing info entry", computingItem.getText().contains("Computing")); //$NON-NLS-1$ //$NON-NLS-2$
+		// Some processors are long running, moving cursor can cause freeze (bug 521484)
+		// asynchronous
+		StyledText styledText = (StyledText) editor.getAdapter(Control.class);
+		styledText.setSelection(styledText.getSelectionRange().x - 1);
+		Event e = new Event();
+		e.type = ST.VerifyKey;
+		e.widget = styledText;
+		e.keyCode = SWT.ARROW_LEFT;
+		e.display = styledText.getDisplay();
+		long timestamp = System.currentTimeMillis();
+		styledText.notifyListeners(ST.VerifyKey, e);
+		DisplayHelper.sleep(styledText.getDisplay(), 200); //give time to process events
+		long processingDuration = System.currentTimeMillis() - timestamp;
+		assertTrue("UI Thread frozen for " + processingDuration + "ms", processingDuration < LongRunningBarContentAssistProcessor.DELAY);		
+
+		if (!completionShell.isDisposed()) {
+			completionShell.close();
+		}
+	}
+
 	private Table findCompletionSelectionControl(Widget control) {
 		if (control instanceof Table) {
 			return (Table)control;
diff --git a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/BarContentAssistProcessor.java b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/BarContentAssistProcessor.java
index 7f6a148..745f42d 100644
--- a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/BarContentAssistProcessor.java
+++ b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/BarContentAssistProcessor.java
@@ -24,7 +24,7 @@
 	@Override
 	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
 		String text = viewer.getDocument().get();
-		if (text.length() >= 3 && text.substring(offset - 3, offset).equals("bar")) {
+		if (text.length() >= 3 && offset >= 3 && text.substring(offset - 3, offset).equals("bar")) {
 			String message = PROPOSAL;
 			CompletionProposal proposal = new CompletionProposal(message, offset, 0, message.length());
 			return new ICompletionProposal[] { proposal };
diff --git a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/LongRunningBarContentAssistProcessor.java b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/LongRunningBarContentAssistProcessor.java
index b902524..6f504d7 100644
--- a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/LongRunningBarContentAssistProcessor.java
+++ b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/contributions/LongRunningBarContentAssistProcessor.java
@@ -31,7 +31,7 @@
 			e.printStackTrace();
 		}
 		String text = viewer.getDocument().get();
-		if (text.length() >= 3 && text.substring(offset - 3, offset).equals("bar")) {
+		if (text.length() >= 3 && offset >= 3 && text.substring(offset - 3, offset).equals("bar")) {
 			String message = PROPOSAL;
 			CompletionProposal proposal = new CompletionProposal(message, offset, 0, message.length());
 			return new ICompletionProposal[] { proposal };