Bug 559251 - restored missing validate() events during async computation

Only clear the occurred document events when they were actually consumed
by the filtering. While the async computation is running, filtering is
postponed and events are left in the queue.

This ensures that ICompletionProposalExtension2 can adapt their
replacement length correctly.

Replaced fIsFilterPending with an AtomicBoolean to avoid synchronization
issues when accessed by computation thread.


Change-Id: I92ec82eb43a52301aa0116f00e5d703840348187
Signed-off-by: Julian Honnen <julian.honnen@vector.com>
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/FilteringAsyncContentAssistTests.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/FilteringAsyncContentAssistTests.java
index 73f9826..836cfd3 100644
--- a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/FilteringAsyncContentAssistTests.java
+++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/FilteringAsyncContentAssistTests.java
@@ -15,6 +15,7 @@
 
 import java.lang.reflect.Field;
 import java.util.List;
+import java.util.concurrent.CountDownLatch;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -153,7 +154,7 @@
 
 		((ICompletionProposalExtension) filteredProposals.get(0)).apply(document, (char) 0,
 				viewer.getSelectedRange().x);
-		assertEquals("xxx", document.get());
+		assertEquals("xx", document.get());
 	}
 
 	/**
@@ -312,6 +313,46 @@
 		filteredProposals = getFilteredProposals(ca, p -> p instanceof IncompleteCompletionProposal);
 		assertTrue(filteredProposals == null || filteredProposals.isEmpty());
 	}
+	
+	@Test
+	public void testProposalValidation() throws Exception {
+		IDocument document= viewer.getDocument();
+
+		BlockingProcessor processor= new BlockingProcessor("abcd()");
+		ca.addContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
+		
+		ca.install(viewer);
+		
+		viewer.setSelectedRange(0, 0);
+		
+		ca.showPossibleCompletions();
+		DisplayHelper.sleep(shell.getDisplay(), 50);
+		
+		new InsertEdit(0, "a").apply(document);
+		viewer.setSelectedRange(1, 0);
+		new InsertEdit(1, "b").apply(document);
+		viewer.setSelectedRange(2, 0);
+		
+		processor.blocked.countDown();
+		DisplayHelper.sleep(shell.getDisplay(), 100);
+		
+		new InsertEdit(2, "c").apply(document);
+		viewer.setSelectedRange(3, 0);
+		new InsertEdit(3, "d").apply(document);
+		viewer.setSelectedRange(4, 0);
+
+		DisplayHelper.sleep(shell.getDisplay(), 100);
+		
+		List<ICompletionProposal> filteredProposals= getFilteredProposals(ca,
+				p -> p instanceof CompletionProposal);
+		assertTrue(filteredProposals != null);
+		assertEquals(1, filteredProposals.size());
+		
+		filteredProposals.get(0).apply(document);
+		
+		assertEquals("abcd()", document.get());
+		
+	}
 
 	private class ImmediateContentAssistProcessor implements IContentAssistProcessor {
 
@@ -408,6 +449,26 @@
 			return completionProposals;
 		}
 	}
+	
+	private class BlockingProcessor extends ImmediateContentAssistProcessor {
+
+		final CountDownLatch blocked= new CountDownLatch(1);
+
+		BlockingProcessor(String template) {
+			super(template, false);
+		}
+
+		@Override
+		public ICompletionProposal[] computeCompletionProposals(ITextViewer textViewer, int offset) {
+			try {
+				blocked.await();
+			} catch (InterruptedException e) {
+				throw new IllegalStateException("Cannot generate delayed content assist proposals!");
+			}
+
+			return super.computeCompletionProposals(textViewer, offset);
+		}
+	}
 
 	@SuppressWarnings("unchecked")
 	private static List<ICompletionProposal> getComputedProposals(ContentAssistant ca) throws Exception {
@@ -456,7 +517,7 @@
 		/** The replacement offset. */
 		protected int fReplacementOffset;
 		/** The replacement length. */
-		private int fReplacementLength;
+		protected int fReplacementLength;
 		/** The cursor position after this proposal has been applied. */
 		private int fCursorPosition;
 
@@ -530,6 +591,9 @@
 
 		@Override
 		public boolean validate(IDocument document, int offset, DocumentEvent event) {
+			if (event != null) {
+				fReplacementLength += event.fText.length() - event.fLength;
+			}
 			if (offset > fReplacementOffset) {
 				try {
 					return isSubstringFoundOrderedInString(document.get(fReplacementOffset, offset - fReplacementOffset), fReplacementString);
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 32f7460..1ed5f4f 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
@@ -22,6 +22,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.osgi.util.TextProcessor;
 
@@ -350,11 +351,9 @@
 	private final Runnable fFilterRunnable= new Runnable() {
 		@Override
 		public void run() {
-			if (!fIsFilterPending)
+			if (!fIsFilterPending.compareAndSet(true, false))
 				return;
 
-			fIsFilterPending= false;
-
 			if (!Helper.okToUse(fContentAssistSubjectControlAdapter.getControl()))
 				return;
 
@@ -367,7 +366,6 @@
 					proposals= computeFilteredProposals(offset, event);
 				}
 			} catch (BadLocationException x) {
-			} finally {
 				fDocumentEvents.clear();
 			}
 			fFilterOffset= offset;
@@ -395,7 +393,7 @@
 	 *
 	 * @since 3.1.1
 	 */
-	private boolean fIsFilterPending= false;
+	private final AtomicBoolean fIsFilterPending= new AtomicBoolean(false);
 	/**
 	 * The info message at the bottom of the popup, or <code>null</code> for no popup (if
 	 * ContentAssistant does not provide one).
@@ -936,7 +934,7 @@
 		/* Make sure that there is no filter runnable pending.
 		 * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=31427
 		 */
-		if (fIsFilterPending)
+		if (fIsFilterPending.get())
 			fFilterRunnable.run();
 
 		// filter runnable may have hidden the proposals
@@ -1494,8 +1492,7 @@
 	 * offset of the original invocation of the content assistant.
 	 */
 	void filterProposals() {
-		if (!fIsFilterPending) {
-			fIsFilterPending= true;
+		if (fIsFilterPending.compareAndSet(false, true)) {
 			Control control= fContentAssistSubjectControlAdapter.getControl();
 			control.getDisplay().asyncExec(fFilterRunnable);
 		}
@@ -1511,6 +1508,7 @@
 	 * @since 3.0
 	 */
 	List<ICompletionProposal> computeFilteredProposals(int offset, DocumentEvent event) {
+		fDocumentEvents.clear();
 
 		if (offset == fInvocationOffset && event == null) {
 			fIsFilteredSubset= false;