diff options
author | BoykoAlex | 2019-09-13 19:46:14 +0000 |
---|---|---|
committer | Mickael Istria | 2019-09-20 07:37:24 +0000 |
commit | d82a4a4095918a66621d6c589379d3ba9b10a03f (patch) | |
tree | 9ccc94b68a8f3d59f1170480e1018a3f07341dfa | |
parent | b4ca5bddb0bb5b94ee079ba332f8d7e79e6b0047 (diff) | |
download | eclipse.platform.text-d82a4a4095918a66621d6c589379d3ba9b10a03f.tar.gz eclipse.platform.text-d82a4a4095918a66621d6c589379d3ba9b10a03f.tar.xz eclipse.platform.text-d82a4a4095918a66621d6c589379d3ba9b10a03f.zip |
Bug 550920 - Recompute proposals if some cannot be filtered out
Change-Id: I51282e29252b8e27d318fceed7d9634cb708f2e0
Signed-off-by: BoykoAlex <aboyko@pivotal.io>
3 files changed, 603 insertions, 15 deletions
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/JFaceTextTestSuite.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/JFaceTextTestSuite.java index 5790030af..1a315f140 100644 --- a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/JFaceTextTestSuite.java +++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/JFaceTextTestSuite.java @@ -20,6 +20,7 @@ import org.junit.runners.Suite.SuiteClasses; import org.eclipse.jface.text.tests.codemining.CodeMiningProjectionViewerTest; import org.eclipse.jface.text.tests.codemining.CodeMiningTest; import org.eclipse.jface.text.tests.contentassist.AsyncContentAssistTest; +import org.eclipse.jface.text.tests.contentassist.FilteringAsyncContentAssistTests; import org.eclipse.jface.text.tests.reconciler.AbstractReconcilerTest; import org.eclipse.jface.text.tests.rules.DefaultPartitionerTest; import org.eclipse.jface.text.tests.rules.DefaultPartitionerZeroLengthTest; @@ -49,6 +50,7 @@ import org.eclipse.jface.text.tests.templates.persistence.TemplatePersistenceDat DefaultPairMatcherTest.class, DefaultPairMatcherTest2.class, AsyncContentAssistTest.class, + FilteringAsyncContentAssistTests.class, AbstractReconcilerTest.class, 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 new file mode 100644 index 000000000..d7e4882d5 --- /dev/null +++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/FilteringAsyncContentAssistTests.java @@ -0,0 +1,581 @@ +/******************************************************************************* + * Copyright (c) 2019 Pivotal, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Pivotal, Inc. - initial API and implementation + *******************************************************************************/ +package org.eclipse.jface.text.tests.contentassist; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Shell; + +import org.eclipse.text.edits.InsertEdit; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.contentassist.ContentAssistant; +import org.eclipse.jface.text.contentassist.ContextInformationValidator; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.ICompletionProposalExtension; +import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; +import org.eclipse.jface.text.contentassist.IContentAssistProcessor; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.jface.text.contentassist.IContextInformationValidator; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.tests.util.DisplayHelper; + +/** + * Tests for Async completion proposal popup proposals filtering mechanics + * + * @author Alex Boyko + * + */ +public class FilteringAsyncContentAssistTests { + + private Shell shell; + private SourceViewer viewer; + private ContentAssistant ca; + + @Before + public void setup() { + tearDown(); + + shell = new Shell(); + shell.setSize(300, 300); + shell.open(); + + viewer = new SourceViewer(shell, null, SWT.NONE); + Document document = new Document(); + viewer.setDocument(document); + ca = new ContentAssistant(true); + } + + @After + public void tearDown() { + if (shell != null) { + ca.uninstall(); + if (!shell.isDisposed()) { + shell.dispose(); + } + shell = null; + } + } + + /** + * Simple CA with 1 immediate CA processor. Empty text, invoke CA, verify 1 + * proposal, apply it, verify the resultant text + * + * @throws Exception exception + */ + @Test + public void testSimpleCa() throws Exception { + + ca.addContentAssistProcessor(new ImmediateContentAssistProcessor("xx"), IDocument.DEFAULT_CONTENT_TYPE); + + ca.install(viewer); + + viewer.setSelectedRange(0, 0); + + ca.showPossibleCompletions(); + + DisplayHelper.sleep(shell.getDisplay(), 300); + + List<ICompletionProposal> computedProposals = getComputedProposals(ca); + + assertEquals(1, computedProposals.size()); + + ICompletionProposal proposal = computedProposals.get(0); + + IDocument document = viewer.getDocument(); + + proposal.apply(document); + + assertEquals("xx", document.get()); + } + + /** + * Simple CA with filtering with 2 immediate CA processors. Empty text + * initially. Invoke CA, verify 2 proposals, type 'x', verify only 1 proposal 1 + * + * @throws Exception exception + */ + @Test + public void testFilteredCa() throws Exception { + IDocument document = viewer.getDocument(); + + ca.addContentAssistProcessor(new ImmediateContentAssistProcessor("xx"), IDocument.DEFAULT_CONTENT_TYPE); + ca.addContentAssistProcessor(new ImmediateContentAssistProcessor("yy"), IDocument.DEFAULT_CONTENT_TYPE); + + ca.install(viewer); + + viewer.setSelectedRange(1, 0); + + ca.showPossibleCompletions(); + + DisplayHelper.sleep(shell.getDisplay(), 300); + + List<ICompletionProposal> computedProposals = getComputedProposals(ca); + assertEquals(2, computedProposals.size()); + List<ICompletionProposal> filteredProposals = getFilteredProposals(ca); + assertEquals(2, filteredProposals.size()); + + new InsertEdit(0, "x").apply(document); + viewer.setSelectedRange(1, 0); + + DisplayHelper.sleep(shell.getDisplay(), 300); + + computedProposals = getComputedProposals(ca); + assertEquals(2, computedProposals.size()); + filteredProposals = getFilteredProposals(ca); + assertEquals(1, filteredProposals.size()); + + ((ICompletionProposalExtension) filteredProposals.get(0)).apply(document, (char) 0, + viewer.getSelectedRange().x); + assertEquals("xxx", document.get()); + } + + /** + * Simple CA with filtering with 1 immediate CA processors. Empty text + * initially. Invoke CA, verify 1 proposal, type 'a', verify no proposals + * + * @throws Exception exception + */ + @Test + public void testFilteredCa_AllFilteredOut() throws Exception { + IDocument document = viewer.getDocument(); + + ca.addContentAssistProcessor(new ImmediateContentAssistProcessor("xx"), IDocument.DEFAULT_CONTENT_TYPE); + + ca.install(viewer); + + viewer.setSelectedRange(1, 0); + + ca.showPossibleCompletions(); + + DisplayHelper.sleep(shell.getDisplay(), 300); + + List<ICompletionProposal> computedProposals = getComputedProposals(ca); + assertEquals(1, computedProposals.size()); + List<ICompletionProposal> filteredProposals = getFilteredProposals(ca); + assertEquals(1, filteredProposals.size()); + + new InsertEdit(0, "a").apply(document); + viewer.setSelectedRange(1, 0); + + DisplayHelper.sleep(shell.getDisplay(), 600); + + filteredProposals = getFilteredProposals(ca); + assertTrue(filteredProposals == null || filteredProposals.isEmpty()); + } + + /** + * CA with 1 immediate and 1 delayed CA processors. Empty text initially. Invoke + * CA, verify 1 proposal shows right away, and then another added later after + * delay + * + * @throws Exception exception + */ + @Test + public void testMultipleCaProcessors() throws Exception { + IDocument document = viewer.getDocument(); + + ca.addContentAssistProcessor(new ImmediateContentAssistProcessor("xx"), IDocument.DEFAULT_CONTENT_TYPE); + ca.addContentAssistProcessor(new DelayedContentAssistProcessor("yy", 3000, false), + IDocument.DEFAULT_CONTENT_TYPE); + + ca.install(viewer); + + viewer.setSelectedRange(0, 0); + + ca.showPossibleCompletions(); + + DisplayHelper.sleep(shell.getDisplay(), 300); + + List<ICompletionProposal> computedProposals = getComputedProposals(ca, + p -> p instanceof IncompleteCompletionProposal); + assertEquals(1, computedProposals.size()); + List<ICompletionProposal> filteredProposals = getFilteredProposals(ca, + p -> p instanceof IncompleteCompletionProposal); + assertEquals(1, filteredProposals.size()); + + DisplayHelper.sleep(shell.getDisplay(), 3000); + + computedProposals = getComputedProposals(ca, p -> p instanceof IncompleteCompletionProposal); + assertEquals(2, computedProposals.size()); + filteredProposals = getFilteredProposals(ca, p -> p instanceof IncompleteCompletionProposal); + assertEquals(2, filteredProposals.size()); + + ((ICompletionProposalExtension) filteredProposals.get(1)).apply(document, (char) 0, + viewer.getSelectedRange().x); + assertEquals("yy", document.get()); + } + + /** + * CA with 1 CA processor for which the first request takes long time and consequent request are + * instant. Invoke CA. and type 'a' such that completions are not ready yet, but while recompute + * was cancelling futures the futures from previous invocation completed and scheduled an async + * UI runnable to show completions. Recompute is immediate. Hence proposals shown right away. + * However the async UI runnable to show old proposals runs after and overwrites the correct + * immediate proposals. Test that this behaviour is fixed + * + * @throws Exception exception + */ + @Test + public void testCA_WithFirstDelayedThenImmediateProposals() throws Exception { + IDocument document = viewer.getDocument(); + + ca.addContentAssistProcessor(new LongInitialContentAssistProcessor("abc", 500, true), + IDocument.DEFAULT_CONTENT_TYPE); + + ca.install(viewer); + + viewer.setSelectedRange(0, 0); + + ca.showPossibleCompletions(); + + DisplayHelper.sleep(shell.getDisplay(), 200); + new InsertEdit(0, "a").apply(document); + viewer.setSelectedRange(1, 0); + + DisplayHelper.sleep(shell.getDisplay(), 3000); + + List<ICompletionProposal> filteredProposals= getFilteredProposals(ca, + p -> p instanceof IncompleteCompletionProposal); + assertTrue(filteredProposals != null); + assertEquals(1, filteredProposals.size()); + + filteredProposals.get(0).apply(document); + + assertEquals("aabc", document.get()); + + } + + /** + * CA with filtering with 1 immediate and 1 delayed CA processors. Empty text + * initially. Invoke CA, verify 1 proposal shows right away, type `a` before + * delayed proposal calculated, verify immediate proposal filtered out + * + * Bug: filtering only applied after all CA processors have completed + * + * @throws Exception exception + */ + @Test @Ignore + public void testFastCompletionsNotFilteredUntilLongComplitionsCalculated() throws Exception { + IDocument document = viewer.getDocument(); + + ca.addContentAssistProcessor(new ImmediateContentAssistProcessor("xxxx"), IDocument.DEFAULT_CONTENT_TYPE); + ca.addContentAssistProcessor(new DelayedContentAssistProcessor("yyyy", 5000, false), + IDocument.DEFAULT_CONTENT_TYPE); + + ca.install(viewer); + + viewer.setSelectedRange(1, 0); + + ca.showPossibleCompletions(); + + DisplayHelper.sleep(shell.getDisplay(), 300); + + List<ICompletionProposal> computedProposals = getComputedProposals(ca, + p -> p instanceof IncompleteCompletionProposal); + assertEquals(1, computedProposals.size()); + List<ICompletionProposal> filteredProposals = getFilteredProposals(ca, + p -> p instanceof IncompleteCompletionProposal); + assertEquals(1, filteredProposals.size()); + + new InsertEdit(0, "a").apply(document); + viewer.setSelectedRange(1, 0); + + DisplayHelper.sleep(shell.getDisplay(), 1000); + + filteredProposals = getFilteredProposals(ca, p -> p instanceof IncompleteCompletionProposal); + assertTrue(filteredProposals == null || filteredProposals.isEmpty()); + } + + private class ImmediateContentAssistProcessor implements IContentAssistProcessor { + + final private String template; + final private boolean incomplete; + + ImmediateContentAssistProcessor(String template) { + this(template, false); + } + + ImmediateContentAssistProcessor(String template, boolean incomplete) { + this.template = template; + this.incomplete = incomplete; + } + + @Override + public ICompletionProposal[] computeCompletionProposals(ITextViewer textViewer, int offset) { + try { + IDocument document= textViewer.getDocument(); + if (document != null && (document.getLength() == 0 || isSubstringFoundOrderedInString(document.get(0, offset), template))) { + if (incomplete) { + return new ICompletionProposal[] { + new IncompleteCompletionProposal(template, offset, 0, offset, template) }; + } else { + CompletionProposal proposal = new CompletionProposal(template, offset, 0, offset, template); + return new ICompletionProposal[] { proposal }; + } + } + } catch (BadLocationException e) { + throw new IllegalStateException("Error computing proposals"); + } + return new ICompletionProposal[0]; + } + + @Override + public IContextInformation[] computeContextInformation(ITextViewer textViewer, int offset) { + return new IContextInformation[0]; + } + + @Override + public char[] getCompletionProposalAutoActivationCharacters() { + return new char[0]; + } + + @Override + public char[] getContextInformationAutoActivationCharacters() { + return new char[0]; + } + + @Override + public String getErrorMessage() { + return "No proposals!"; + } + + @Override + public IContextInformationValidator getContextInformationValidator() { + return new ContextInformationValidator(this); + } + + } + + private class DelayedContentAssistProcessor extends ImmediateContentAssistProcessor { + + protected long delay; + + DelayedContentAssistProcessor(String template, long delay, boolean incomplete) { + super(template, incomplete); + this.delay = delay; + } + + @Override + public ICompletionProposal[] computeCompletionProposals(ITextViewer textViewer, int offset) { + if (delay > 0) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + throw new IllegalStateException("Cannot generate delayed content assist proposals!"); + } + } + return super.computeCompletionProposals(viewer, offset); + } + } + + private class LongInitialContentAssistProcessor extends DelayedContentAssistProcessor { + + LongInitialContentAssistProcessor(String template, long delay, boolean incomplete) { + super(template, delay, incomplete); + } + + @Override + public ICompletionProposal[] computeCompletionProposals(ITextViewer textViewer, int offset) { + ICompletionProposal[] completionProposals= super.computeCompletionProposals(viewer, offset); + delay = 0; + return completionProposals; + } + } + + @SuppressWarnings("unchecked") + private static List<ICompletionProposal> getComputedProposals(ContentAssistant ca) throws Exception { + Field f = ContentAssistant.class.getDeclaredField("fProposalPopup"); + f.setAccessible(true); + Object caPopup = f.get(ca); + assertEquals("org.eclipse.jface.text.contentassist.AsyncCompletionProposalPopup", caPopup.getClass().getName()); + Class<?> caPopupSuperClass = caPopup.getClass().getSuperclass(); + assertEquals("org.eclipse.jface.text.contentassist.CompletionProposalPopup", caPopupSuperClass.getName()); + Field computedProposals = caPopupSuperClass.getDeclaredField("fComputedProposals"); + computedProposals.setAccessible(true); + return (List<ICompletionProposal>) computedProposals.get(caPopup); + } + + @SuppressWarnings("unchecked") + private static List<ICompletionProposal> getFilteredProposals(ContentAssistant ca) throws Exception { + Field f = ContentAssistant.class.getDeclaredField("fProposalPopup"); + f.setAccessible(true); + Object caPopup = f.get(ca); + assertEquals("org.eclipse.jface.text.contentassist.AsyncCompletionProposalPopup", caPopup.getClass().getName()); + Class<?> caPopupSuperClass = caPopup.getClass().getSuperclass(); + assertEquals("org.eclipse.jface.text.contentassist.CompletionProposalPopup", caPopupSuperClass.getName()); + Field computedProposals = caPopupSuperClass.getDeclaredField("fFilteredProposals"); + computedProposals.setAccessible(true); + return (List<ICompletionProposal>) computedProposals.get(caPopup); + } + + private static List<ICompletionProposal> getComputedProposals(ContentAssistant ca, Predicate<ICompletionProposal> p) + throws Exception { + List<ICompletionProposal> computedProposals = getComputedProposals(ca); + return computedProposals == null ? null : computedProposals.stream().filter(p).collect(Collectors.toList()); + } + + private static List<ICompletionProposal> getFilteredProposals(ContentAssistant ca, Predicate<ICompletionProposal> p) + throws Exception { + List<ICompletionProposal> filteredProposals = getFilteredProposals(ca); + return filteredProposals == null ? null : filteredProposals.stream().filter(p).collect(Collectors.toList()); + } + + private static class IncompleteCompletionProposal implements ICompletionProposal { + + /** The string to be displayed in the completion proposal popup. */ + private String fDisplayString; + /** The replacement string. */ + protected String fReplacementString; + /** The replacement offset. */ + protected int fReplacementOffset; + /** The replacement length. */ + private int fReplacementLength; + /** The cursor position after this proposal has been applied. */ + private int fCursorPosition; + + public IncompleteCompletionProposal(String replacementString, int replacementOffset, int replacementLength, int cursorPosition, String displayString) { + fReplacementString= replacementString; + fReplacementOffset= replacementOffset; + fReplacementLength= replacementLength; + fCursorPosition= cursorPosition; + fDisplayString= displayString; + } + + @Override + public void apply(IDocument document) { + try { + document.replace(fReplacementOffset, fReplacementLength, fReplacementString); + } catch (BadLocationException x) { + // ignore + } + } + + @Override + public Point getSelection(IDocument document) { + return new Point(fReplacementOffset + fCursorPosition, 0); + } + + @Override + public IContextInformation getContextInformation() { + return null; + } + + @Override + public Image getImage() { + return null; + } + + @Override + public String getDisplayString() { + if (fDisplayString != null) + return fDisplayString; + return fReplacementString; + } + + @Override + public String getAdditionalProposalInfo() { + return null; + } + } + + private static class CompletionProposal extends IncompleteCompletionProposal + implements ICompletionProposalExtension, ICompletionProposalExtension2 { + + public CompletionProposal(String replacementString, int replacementOffset, int replacementLength, + int cursorPosition, String displayString) { + super(replacementString, replacementOffset, replacementLength, cursorPosition, displayString); + } + + @Override + public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) { + apply(viewer.getDocument()); + } + + @Override + public void selected(ITextViewer viewer, boolean smartToggle) { + // nothing + } + + @Override + public void unselected(ITextViewer viewer) { + // nothing + } + + @Override + public boolean validate(IDocument document, int offset, DocumentEvent event) { + if (offset > fReplacementOffset) { + try { + return isSubstringFoundOrderedInString(document.get(fReplacementOffset, offset - fReplacementOffset), fReplacementString); + } catch (BadLocationException e) { + throw new IllegalStateException("Completion validation failed"); + } + } + return false; + } + + @Override + public void apply(IDocument document, char trigger, int offset) { + apply(document); + } + + @Override + public boolean isValidFor(IDocument document, int offset) { + return validate(document, offset, null); + } + + @Override + public char[] getTriggerCharacters() { + return new char[0]; + } + + @Override + public int getContextInformationPosition() { + return 0; + } + + } + + @SuppressWarnings("boxing") + private static boolean isSubstringFoundOrderedInString(String subString, String string) { + int lastIndex = 0; + subString = subString.toLowerCase(); + string = string.toLowerCase(); + for (Character c : subString.toCharArray()) { + int index = string.indexOf(c, lastIndex); + if (index < 0) { + return false; + } else { + lastIndex = index + 1; + } + } + return true; + } + +} 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 3c1a81004..89520fefa 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,7 +32,6 @@ import org.eclipse.osgi.util.NLS; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Control; -import org.eclipse.swt.widgets.Display; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.SafeRunner; @@ -231,21 +230,27 @@ class AsyncCompletionProposalPopup extends CompletionProposalPopup { } List<ICompletionProposal> newProposals= new ArrayList<>(computedProposals); fComputedProposals= newProposals; - Display.getDefault().asyncExec(() -> { - if (autoInsert && !autoActivated && remaining.isEmpty() && newProposals.size() == 1 && canAutoInsert(newProposals.get(0))) { - if (Helper.okToUse(fProposalShell)) { - insertProposal(newProposals.get(0), (char) 0, 0, offset); - hide(); + Control control= fContentAssistSubjectControlAdapter.getControl(); + if (!control.isDisposed()) { + control.getDisplay().asyncExec(() -> { + // Don't run anything if offset has changed while runnable was scheduled (i.e. filtering might have occurred for fast CA) + if (offset == fInvocationOffset) { + if (autoInsert && !autoActivated && remaining.isEmpty() && newProposals.size() == 1 && canAutoInsert(newProposals.get(0))) { + if (Helper.okToUse(fProposalShell)) { + insertProposal(newProposals.get(0), (char) 0, 0, offset); + hide(); + } + return; + } + if (remaining.isEmpty() && callback != null) { + callback.accept(newProposals); + } else { + setProposals(newProposals, false); + displayProposals(); + } } - return; - } - if (remaining.isEmpty() && callback != null) { - callback.accept(newProposals); - } else { - setProposals(newProposals, false); - displayProposals(); - } - }); + }); + } }); } displayProposals(); |