Bug 553622: Add tests for context information to jface.text.tests

Change-Id: I35d5ee744a0df394dfa8288b40e6d5b8a3c633fb
Signed-off-by: Stephan Wahlbrink <sw@wahlbrink.eu>
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 1a315f1..08104d5 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,8 @@
 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.ContextInformationPresenterTest;
+import org.eclipse.jface.text.tests.contentassist.ContextInformationTest;
 import org.eclipse.jface.text.tests.contentassist.FilteringAsyncContentAssistTests;
 import org.eclipse.jface.text.tests.reconciler.AbstractReconcilerTest;
 import org.eclipse.jface.text.tests.rules.DefaultPartitionerTest;
@@ -51,6 +53,8 @@
 		DefaultPairMatcherTest2.class,
 		AsyncContentAssistTest.class,
 		FilteringAsyncContentAssistTests.class,
+		ContextInformationTest.class,
+		ContextInformationPresenterTest.class,
 
 		AbstractReconcilerTest.class,
 
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/AbstractContentAssistTest.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/AbstractContentAssistTest.java
new file mode 100644
index 0000000..c669281
--- /dev/null
+++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/AbstractContentAssistTest.java
@@ -0,0 +1,173 @@
+/*******************************************************************************
+ * Copyright (c) 2019 Stephan Wahlbrink and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Wahlbrink <sw@wahlbrink.eu>
+ *******************************************************************************/
+
+package org.eclipse.jface.text.tests.contentassist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.After;
+import org.junit.Assert;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Shell;
+
+import org.eclipse.jface.text.Document;
+import org.eclipse.jface.text.ITextOperationTarget;
+import org.eclipse.jface.text.contentassist.ContentAssistant;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.SourceViewer;
+import org.eclipse.jface.text.source.SourceViewerConfiguration;
+import org.eclipse.jface.text.tests.util.DisplayHelper;
+
+
+public class AbstractContentAssistTest {
+
+
+	private Shell shell;
+	private SourceViewer viewer;
+	private ContentAssistant assistant;
+	private Document document;
+
+
+	public AbstractContentAssistTest() {
+	}
+
+
+	@After
+	public void close() {
+		if (shell != null && !shell.isDisposed()) {
+			shell.close();
+		}
+	}
+
+
+	protected void setupSourceViewer(ContentAssistant contentAssistant, String initialText) {
+		shell= new Shell();
+		shell.setSize(500, 240);
+		shell.setLayout(new FillLayout());
+		
+		viewer= new SourceViewer(shell, null, SWT.NONE);
+		assistant= contentAssistant;
+		viewer.configure(createSourceViewerConfiguration());
+		
+		document= new Document();
+		if (initialText != null) {
+			document.set(initialText);
+		}
+		viewer.setDocument(document);
+		
+		shell.open();
+		Assert.assertTrue(new DisplayHelper() {
+			@Override
+			protected boolean condition() {
+				return viewer.getTextWidget().isVisible();
+			}
+		}.waitForCondition(shell.getDisplay(), 3000));
+	}
+
+	protected SourceViewerConfiguration createSourceViewerConfiguration() {
+		return new SourceViewerConfiguration() {
+			@Override
+			public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {
+				return AbstractContentAssistTest.this.getContentAssistant();
+			}
+		};
+	}
+
+	protected SourceViewer getSourceViewer() {
+		return viewer;
+	}
+
+	protected Document getDocument() {
+		return document;
+	}
+
+	protected ContentAssistant getContentAssistant() {
+		return assistant;
+	}
+
+	protected void selectAndReveal(int selectionStart, int selectionLength) {
+		StyledText widget= viewer.getTextWidget();
+		widget.setRedraw(false);
+
+		viewer.revealRange(selectionStart, selectionLength);
+		viewer.setSelectedRange(selectionStart, selectionLength);
+
+		widget.setRedraw(true);
+	}
+
+	protected void postSourceViewerKeyEvent(int keyCode, int stateMask, int type) {
+		processEvents();
+		Event event= new Event();
+		event.type= type;
+		event.widget= viewer.getTextWidget();
+		event.display= viewer.getTextWidget().getDisplay();
+		event.keyCode= keyCode;
+		event.stateMask= stateMask;
+		event.doit= true;
+		viewer.getTextWidget().notifyListeners(type, event);
+		processEvents();
+	}
+
+	protected void runTextOperation(int operation) {
+		ITextOperationTarget textOperationTarget= viewer.getTextOperationTarget();
+
+		assertTrue(textOperationTarget.canDoOperation(operation));
+		textOperationTarget.doOperation(operation);
+	}
+
+	protected void triggerContextInformation() {
+		runTextOperation(ISourceViewer.CONTENTASSIST_CONTEXT_INFORMATION);
+	}
+
+
+	protected void processEvents() {
+		DisplayHelper.driveEventQueue(shell.getDisplay());
+	}
+
+	protected List<Shell> getCurrentShells() {
+		return Arrays.stream(shell.getDisplay().getShells())
+				.filter(Shell::isVisible)
+				.collect(Collectors.toList());
+	}
+
+	protected List<Shell> findNewShells(Collection<Shell> beforeShells) {
+		return Arrays.stream(shell.getDisplay().getShells())
+				.filter(Shell::isVisible)
+				.filter(shell -> !beforeShells.contains(shell))
+				.collect(Collectors.toList());
+	}
+
+	protected Shell findNewShell(Collection<Shell> beforeShells) {
+		DisplayHelper.sleep(shell.getDisplay(), 100);
+		List<Shell> afterShells= findNewShells(beforeShells);
+		if (afterShells.isEmpty()) {
+			DisplayHelper.sleep(shell.getDisplay(), 1000);
+		}
+		afterShells= findNewShells(beforeShells);
+		assertEquals("No new shell found", 1, afterShells.size());
+		return afterShells.get(0);
+	}
+
+}
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/BarContentAssistProcessor.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/BarContentAssistProcessor.java
new file mode 100644
index 0000000..0cced65
--- /dev/null
+++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/BarContentAssistProcessor.java
@@ -0,0 +1,181 @@
+/*******************************************************************************
+ * Copyright (c) 2016, 2019 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Mickael Istria (Red Hat Inc.)
+ *     Stephan Wahlbrink <sw@wahlbrink.eu> - Bug 512251 - Fix IllegalArgumentException in ContextInformationPopup
+ *******************************************************************************/
+
+package org.eclipse.jface.text.tests.contentassist;
+
+import java.util.Arrays;
+
+import org.eclipse.swt.graphics.Image;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.contentassist.CompletionProposal;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.contentassist.IContextInformationExtension;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+
+
+public class BarContentAssistProcessor implements IContentAssistProcessor {
+
+	public static final String PROPOSAL= "bars are good for a beer.";
+
+	protected static class BarContextInformation implements IContextInformation, IContextInformationExtension {
+
+		private String contextDisplayString;
+
+		private String informationDisplayString;
+
+		private int offset;
+
+		public BarContextInformation(String contextDisplayString, String informationDisplayString, int offset) {
+			this.contextDisplayString= contextDisplayString;
+			this.informationDisplayString= informationDisplayString;
+			this.offset= offset;
+		}
+
+		@Override
+		public String getContextDisplayString() {
+			return contextDisplayString;
+		}
+
+		@Override
+		public Image getImage() {
+			return null;
+		}
+
+		@Override
+		public String getInformationDisplayString() {
+			return informationDisplayString;
+		}
+
+		@Override
+		public int getContextInformationPosition() {
+			return offset;
+		}
+
+	}
+
+	protected static class ContextInformationValidator implements IContextInformationValidator {
+
+		protected BarContextInformation info;
+
+		protected ITextViewer viewer;
+
+		protected int offset;
+
+		@Override
+		public void install(IContextInformation info, ITextViewer viewer, int offset) {
+			if (info instanceof BarContextInformation) {
+				this.info= (BarContextInformation) info;
+			}
+			this.viewer= viewer;
+			this.offset= offset;
+		}
+
+		@Override
+		public boolean isContextInformationValid(int offset) {
+			if (this.info == null) {
+				return false;
+			}
+			try {
+				IDocument document= viewer.getDocument();
+				IRegion line= document.getLineInformationOfOffset(this.offset);
+				int end= line.getOffset() + line.getLength();
+				return (offset >= this.offset && offset < end);
+			} catch (BadLocationException e) {
+				return false;
+			}
+		}
+	}
+
+
+	private final String completeString;
+
+	public BarContentAssistProcessor() {
+		this(PROPOSAL);
+	}
+
+	public BarContentAssistProcessor(String completeString) {
+		this.completeString= completeString;
+	}
+
+	@Override
+	public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
+		for (int offsetInProposal= Math.min(this.completeString.length(), viewer.getDocument().getLength()); offsetInProposal > 0; offsetInProposal--) {
+			String maybeMatchingString= this.completeString.substring(0, offsetInProposal);
+			try {
+				int lastIndex= offset - offsetInProposal + this.completeString.length();
+				if (offset >= offsetInProposal && viewer.getDocument().get(offset - offsetInProposal, maybeMatchingString.length()).equals(maybeMatchingString)) {
+					CompletionProposal proposal= new CompletionProposal(this.completeString.substring(offsetInProposal), offset, 0, lastIndex);
+					return new ICompletionProposal[] { proposal };
+				}
+			} catch (BadLocationException e) {
+				e.printStackTrace();
+			}
+		}
+		return new ICompletionProposal[0];
+	}
+
+	@Override
+	public char[] getCompletionProposalAutoActivationCharacters() {
+		return null;
+	}
+
+	@Override
+	public char[] getContextInformationAutoActivationCharacters() {
+		return null;
+	}
+
+	/**
+	 * Creates context info "idx= <word index in #PROPOSAL>" at the end of a word.
+	 **/
+	@Override
+	public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
+		try {
+			IDocument document= viewer.getDocument();
+			int wordBegin= offset;
+			while (wordBegin > 0 && Character.isLetterOrDigit(document.getChar(wordBegin - 1))) {
+				wordBegin--;
+			}
+			if (wordBegin < offset) {
+				String word= document.get(wordBegin, offset - wordBegin);
+				int wordIdx= Arrays.asList(completeString.split("\\W")).indexOf(word);
+				if (wordIdx >= 0) {
+					return new IContextInformation[] {
+							new BarContextInformation(word, "idx= " + wordIdx, wordBegin)
+					};
+				}
+			}
+		} catch (BadLocationException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+	@Override
+	public IContextInformationValidator getContextInformationValidator() {
+		return new ContextInformationValidator();
+	}
+
+	@Override
+	public String getErrorMessage() {
+		return null;
+	}
+
+}
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/ContextInformationPresenterTest.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/ContextInformationPresenterTest.java
new file mode 100644
index 0000000..80b8cef
--- /dev/null
+++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/ContextInformationPresenterTest.java
@@ -0,0 +1,127 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2019 Stephan Wahlbrink and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Wahlbrink <sw@wahlbrink.eu>
+ *******************************************************************************/
+package org.eclipse.jface.text.tests.contentassist;
+
+import static org.eclipse.jface.text.tests.contentassist.ContextInformationTest.getInfoText;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.TextPresentation;
+import org.eclipse.jface.text.contentassist.ContentAssistant;
+import org.eclipse.jface.text.contentassist.IContextInformationPresenter;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+
+
+public class ContextInformationPresenterTest extends AbstractContentAssistTest {
+
+	private static class ValidatorWithPresenter extends BarContentAssistProcessor.ContextInformationValidator
+			implements IContextInformationPresenter {
+
+		private boolean isStyled;
+
+		/**
+		 * Hightlight (bold) "idx" if caret is inside the word (including end)
+		 */
+		@Override
+		public boolean updatePresentation(int offset, TextPresentation presentation) {
+			if (info == null) { // Ignore unknown information
+				return false;
+			}
+			
+			int begin= info.getContextInformationPosition();
+			int end= begin + info.getContextDisplayString().length();
+			boolean style= (offset >= begin && offset <= end);
+			if (style == isStyled) {
+				return false;
+			}
+			if (style) {
+				presentation.clear();
+				presentation.addStyleRange(new StyleRange(0, 3, null, null, SWT.BOLD));
+			} else {
+				presentation.clear();
+			}
+			isStyled= style;
+			return true;
+		}
+
+	}
+
+
+	private Shell infoShell;
+
+
+	private ContentAssistant createBarContentAssist() {
+		ContentAssistant contentAssistant= new ContentAssistant();
+		contentAssistant.setContentAssistProcessor(new BarContentAssistProcessor() {
+			@Override
+			public IContextInformationValidator getContextInformationValidator() {
+				return new ValidatorWithPresenter();
+			}
+		}, IDocument.DEFAULT_CONTENT_TYPE);
+		return contentAssistant;
+	}
+
+	@Test
+	public void testContextInfo_withStyledTextPresentation() throws Exception {
+		setupSourceViewer(createBarContentAssist(), BarContentAssistProcessor.PROPOSAL);
+		
+		final List<Shell> beforeShells= getCurrentShells();
+		
+		postSourceViewerKeyEvent(SWT.ARROW_RIGHT, 0, SWT.KeyDown);
+		selectAndReveal(4, 0);
+		processEvents();
+		
+		triggerContextInformation();
+		this.infoShell= findNewShell(beforeShells);
+		assertEquals("idx= 0", getInfoText(this.infoShell));
+		assertArrayEquals(new StyleRange[] {
+				new StyleRange(0, 3, null, null, SWT.BOLD)
+		}, getInfoStyleRanges(this.infoShell));
+		
+		postSourceViewerKeyEvent(SWT.ARROW_RIGHT, 0, SWT.KeyDown);
+		
+		assertEquals("idx= 0", getInfoText(this.infoShell));
+		assertArrayEquals(new StyleRange[] {
+		}, getInfoStyleRanges(this.infoShell));
+	}
+
+
+	static StyleRange[] getInfoStyleRanges(final Shell shell) {
+		assertTrue(shell.isVisible());
+		Control[] children= shell.getChildren();
+		for (Control child : children) {
+			if (child instanceof Text) {
+				return null;
+			}
+			if (child instanceof StyledText) {
+				return ((StyledText) child).getStyleRanges();
+			}
+		}
+		return null;
+	}
+
+}
diff --git a/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/ContextInformationTest.java b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/ContextInformationTest.java
new file mode 100644
index 0000000..1d5d6af
--- /dev/null
+++ b/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/contentassist/ContextInformationTest.java
@@ -0,0 +1,106 @@
+/*******************************************************************************
+ * Copyright (c) 2017, 2019 Stephan Wahlbrink and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Stephan Wahlbrink <sw@wahlbrink.eu>
+ *******************************************************************************/
+package org.eclipse.jface.text.tests.contentassist;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+import org.eclipse.text.tests.Accessor;
+
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.contentassist.ContentAssistant;
+
+
+public class ContextInformationTest extends AbstractContentAssistTest {
+
+
+	private Shell infoShell;
+
+
+	private ContentAssistant createBarContentAssist() {
+		ContentAssistant contentAssistant= new ContentAssistant();
+		contentAssistant.setContentAssistProcessor(new BarContentAssistProcessor(), IDocument.DEFAULT_CONTENT_TYPE);
+		return contentAssistant;
+	}
+
+	@Test
+	public void testContextInfo() throws Exception {
+		setupSourceViewer(createBarContentAssist(), BarContentAssistProcessor.PROPOSAL);
+		
+		final List<Shell> beforeShells= getCurrentShells();
+		
+		selectAndReveal(4, 0);
+		processEvents();
+		
+		triggerContextInformation();
+		this.infoShell= findNewShell(beforeShells);
+		assertEquals("idx= 0", getInfoText(this.infoShell));
+		
+		selectAndReveal(8, 0);
+		processEvents();
+		
+		triggerContextInformation();
+		this.infoShell= findNewShell(beforeShells);
+		assertEquals("idx= 1", getInfoText(this.infoShell));
+	}
+
+	@Test
+	public void testContextInfo_hide_Bug512251() throws Exception {
+		setupSourceViewer(createBarContentAssist(), BarContentAssistProcessor.PROPOSAL);
+		
+		final List<Shell> beforeShells= getCurrentShells();
+		
+		selectAndReveal(4, 0);
+		processEvents();
+		
+		triggerContextInformation();
+		this.infoShell= findNewShell(beforeShells);
+		
+		selectAndReveal(8, 0);
+		processEvents();
+		
+		triggerContextInformation();
+		this.infoShell= findNewShell(beforeShells);
+		
+		// ITextEditorActionConstants.DELETE_LINE
+		getDocument().set("");
+		
+		new Accessor(getContentAssistant(), ContentAssistant.class).invoke("hide", new Object[0]);
+	}
+
+
+	static String getInfoText(final Shell shell) {
+		assertTrue(shell.isVisible());
+		Control[] children= shell.getChildren();
+		for (Control child : children) {
+			if (child instanceof Text) {
+				return ((Text) child).getText();
+			}
+			if (child instanceof StyledText) {
+				return ((StyledText) child).getText();
+			}
+		}
+		return null;
+	}
+
+}