diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningManager.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningManager.java index a0dae7975eb..1de7ce01dca 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningManager.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningManager.java @@ -10,6 +10,7 @@ * * Contributors: * Angelo Zerr - [CodeMining] Provide CodeMining support with CodeMiningManager - Bug 527720 + * Dietrich Travkin - Fix code mining redrawing - Issue 3405 */ package org.eclipse.jface.internal.text.codemining; @@ -230,6 +231,41 @@ private static Map> groupByLines(List codeMiningType; + public final Class annotationType; + + CodeMiningMode(Class codeMiningType, + Class annotationType) { + this.codeMiningType= codeMiningType; + this.annotationType= annotationType; + } + + public static CodeMiningMode createFor(List minings) { + Assert.isNotNull(minings); + + CodeMiningMode mode= CodeMiningMode.HeaderLine; + if (!minings.isEmpty()) { + ICodeMining first= minings.get(0); + + if (CodeMiningMode.InLine.codeMiningType.isInstance(first)) { + mode= CodeMiningMode.InLine; + } else if (CodeMiningMode.HeaderLine.codeMiningType.isInstance(first)) { + mode= CodeMiningMode.HeaderLine; + } else if (CodeMiningMode.FooterLine.codeMiningType.isInstance(first)) { + mode= CodeMiningMode.FooterLine; + } else { + mode= CodeMiningMode.InLine; + } + } + return mode; + } + } + /** * Render the codemining grouped by line position. * @@ -257,11 +293,13 @@ private void renderCodeMinings(Map> groups, ISourceV Position pos= new Position(g.getKey().offset, g.getKey().length); List minings= g.getValue(); ICodeMining first= minings.get(0); - boolean inLineHeader= !minings.isEmpty() ? (first instanceof LineHeaderCodeMining) : true; + + CodeMiningMode mode= CodeMiningMode.createFor(minings); + // Try to find existing annotation AbstractInlinedAnnotation ann= fInlinedAnnotationSupport.findExistingAnnotation(pos); - if (ann == null) { - // The annotation doesn't exists, create it. + if (ann == null || !mode.annotationType.isInstance(ann)) { + // The annotation doesn't exists or has wrong type => create a new one. boolean afterPosition= false; if (first instanceof LineContentCodeMining m) { afterPosition= m.isAfterPosition(); @@ -274,16 +312,14 @@ private void renderCodeMinings(Map> groups, ISourceV mouseOut= first.getMouseOut(); mouseMove= first.getMouseMove(); } - if (inLineHeader) { - ann= new CodeMiningLineHeaderAnnotation(pos, viewer, mouseHover, mouseOut, mouseMove); - } else { - boolean inFooter= !minings.isEmpty() ? (first instanceof DocumentFooterCodeMining) : false; - if (inFooter) { - ann= new CodeMiningDocumentFooterAnnotation(pos, viewer, mouseHover, mouseOut, mouseMove); - } else { - ann= new CodeMiningLineContentAnnotation(pos, viewer, afterPosition, mouseHover, mouseOut, mouseMove); - } - } + + ann= switch (mode) { + case InLine -> new CodeMiningLineContentAnnotation(pos, viewer, afterPosition, mouseHover, mouseOut, mouseMove); + case HeaderLine -> new CodeMiningLineHeaderAnnotation(pos, viewer, mouseHover, mouseOut, mouseMove); + case FooterLine -> new CodeMiningDocumentFooterAnnotation(pos, viewer, mouseHover, mouseOut, mouseMove); + + default -> throw new IllegalStateException("Found unexpected code mining display mode: " + mode); //$NON-NLS-1$ + }; } else if (ann instanceof ICodeMiningAnnotation && ((ICodeMiningAnnotation) ann).isInVisibleLines()) { // annotation is in visible lines annotationsToRedraw.add((ICodeMiningAnnotation) ann); diff --git a/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/CodeMiningDemo.java b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/CodeMiningDemo.java index b70080822bd..1bcd83ca424 100644 --- a/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/CodeMiningDemo.java +++ b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/CodeMiningDemo.java @@ -10,6 +10,7 @@ * * Contributors: * Angelo Zerr - [CodeMining] Add CodeMining support in SourceViewer - Bug 527515 + * Dietrich Travkin - Fix code mining redrawing - Issue 3405 */ package org.eclipse.jface.text.examples.codemining; @@ -34,7 +35,9 @@ import org.eclipse.jface.text.source.ISourceViewerExtension5; import org.eclipse.jface.text.source.SourceViewer; import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; @@ -45,14 +48,22 @@ public class CodeMiningDemo { private static boolean showWhitespaces = false; + private static AtomicReference useInLineCodeMinings = new AtomicReference<>(false); + + private static String LINE_HEADER = "Line header"; + private static String IN_LINE = "In-line"; public static void main(String[] args) throws Exception { Display display = new Display(); Shell shell = new Shell(display); - shell.setLayout(new GridLayout()); + shell.setLayout(new GridLayout(2, false)); shell.setText("Code Mining demo"); + Button toggleInLineButton = new Button(shell, SWT.PUSH); + toggleInLineButton.setText(LINE_HEADER); + GridDataFactory.fillDefaults().align(SWT.BEGINNING, SWT.FILL).grab(false, false).applyTo(toggleInLineButton); + AtomicReference endOfLineString = new AtomicReference<>("End of line"); Text endOfLineText = new Text(shell, SWT.NONE); endOfLineText.setText(endOfLineString.get()); @@ -70,7 +81,9 @@ public static void main(String[] args) throws Exception { + "// Name class with a number N to emulate Nms before resolving the references CodeMining\n" + "// Empty lines show a header annotating they're empty.\n" + "// The word `echo` is echoed.\n" - + "// Lines containing `end` get an annotation at their end\n\n" + + "// Lines containing `end` get an annotation at their end\n" + + "// Press the toggle button in the upper left corner to switch between\n" + + "// showing reference titles in-line and showing them in additional lines.\n\n" + "class A\n" // + "new A\n" // + "new A\n\n" // @@ -79,29 +92,39 @@ public static void main(String[] args) throws Exception { + "class 5\n" // + "new 5\n" // + "new 5\n" // - + "new 5\n" // + + "new 5\n\n" // + + "Text with some references like [REF-X]\n" + "and [REF-Y] in it.\n\n" + "multiline \n" // + "multiline \n\n" // + "suffix \n"), new AnnotationModel()); - GridDataFactory.fillDefaults().grab(true, true).applyTo(sourceViewer.getTextWidget()); + GridDataFactory.fillDefaults().span(2, 1).grab(true, true).applyTo(sourceViewer.getTextWidget()); // Add AnnotationPainter (required by CodeMining) addAnnotationPainter(sourceViewer); + + toggleInLineButton.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> { + useInLineCodeMinings.set(!useInLineCodeMinings.get()); + toggleInLineButton.setText(useInLineCodeMinings.get() ? IN_LINE : LINE_HEADER); + sourceViewer.updateCodeMinings(); + })); + // Initialize codemining providers - ((ISourceViewerExtension5) sourceViewer).setCodeMiningProviders(new ICodeMiningProvider[] { + sourceViewer.setCodeMiningProviders(new ICodeMiningProvider[] { new ClassReferenceCodeMiningProvider(), // new ClassImplementationsCodeMiningProvider(), // new ToEchoWithHeaderAndInlineCodeMiningProvider("echo"), // new MultilineCodeMiningProvider(), // new EmptyLineCodeMiningProvider(), // new EchoAtEndOfLineCodeMiningProvider(endOfLineString), // - new LineContentCodeMiningAfterPositionProvider() }); + new LineContentCodeMiningAfterPositionProvider(), // + new ReferenceCodeMiningProvider(useInLineCodeMinings) }); + // Execute codemining in a reconciler MonoReconciler reconciler = new MonoReconciler(new IReconcilingStrategy() { @Override public void setDocument(IDocument document) { - ((ISourceViewerExtension5) sourceViewer).updateCodeMinings(); + sourceViewer.updateCodeMinings(); } @Override @@ -111,14 +134,14 @@ public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) { @Override public void reconcile(IRegion partition) { - ((ISourceViewerExtension5) sourceViewer).updateCodeMinings(); + sourceViewer.updateCodeMinings(); } }, false); reconciler.install(sourceViewer); endOfLineText.addModifyListener(event -> { endOfLineString.set(endOfLineText.getText()); - ((ISourceViewerExtension5) sourceViewer).updateCodeMinings(); + sourceViewer.updateCodeMinings(); }); shell.open(); diff --git a/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceCodeMiningProvider.java b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceCodeMiningProvider.java new file mode 100644 index 00000000000..41911bf221f --- /dev/null +++ b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceCodeMiningProvider.java @@ -0,0 +1,88 @@ +/******************************************************************************* + * Copyright (c) 2025, Advantest Europe GmbH + * + * 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: + * Dietrich Travkin - Fix code mining redrawing - Issue 3405 + * + *******************************************************************************/ +package org.eclipse.jface.text.examples.codemining; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider; +import org.eclipse.jface.text.codemining.ICodeMining; + +public class ReferenceCodeMiningProvider extends AbstractCodeMiningProvider { + + private static final String REGEX_REF = "\\[REF-X\\]|\\[REF-Y\\]"; + private static final Pattern REGEX_PATTERN = Pattern.compile(REGEX_REF); + + private AtomicReference useInLineCodeMinings; + + public ReferenceCodeMiningProvider(AtomicReference useInLineCodeMinings) { + this.useInLineCodeMinings = useInLineCodeMinings; + } + + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, + IProgressMonitor monitor) { + return CompletableFuture.supplyAsync(() -> { + IDocument document = viewer.getDocument(); + + if (document == null) { + return Collections.emptyList(); + } + + return createCodeMiningsFor(document); + }); + } + + List createCodeMiningsFor(IDocument document) { + String documentContent = document.get(); + List minings = new ArrayList<>(); + + Matcher regexMatcher = REGEX_PATTERN.matcher(documentContent); + while (regexMatcher.find()) { + String matchedText = regexMatcher.group(); + int startIndex = regexMatcher.start(); + + String title = matchedText.endsWith("X]") ? "Plugging into Eclipse" + : "Building commercial quality plug-ins"; + + if (useInLineCodeMinings.get()) { + minings.add(new ReferenceInLineCodeMining(title + ": ", startIndex, document, this)); + } else { + try { + int offset = startIndex; + int line = document.getLineOfOffset(offset); + int lineOffset = document.getLineOffset(line); + + minings.add(new ReferenceLineHeaderCodeMining(title, line, offset - lineOffset, title.length(), + document, this)); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + } + + return minings; + } + +} diff --git a/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceInLineCodeMining.java b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceInLineCodeMining.java new file mode 100644 index 00000000000..13274360e55 --- /dev/null +++ b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceInLineCodeMining.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025, Advantest Europe GmbH + * + * 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: + * Dietrich Travkin - Fix code mining redrawing - Issue 3405 + * + *******************************************************************************/ +package org.eclipse.jface.text.examples.codemining; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.codemining.LineContentCodeMining; + +public class ReferenceInLineCodeMining extends LineContentCodeMining { + + public ReferenceInLineCodeMining(String label, int positionOffset, IDocument document, + ICodeMiningProvider provider) { + super(new Position(positionOffset, 1), true, provider); + this.setLabel(label); + } + +} diff --git a/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceLineHeaderCodeMining.java b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceLineHeaderCodeMining.java new file mode 100644 index 00000000000..27c10473307 --- /dev/null +++ b/examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceLineHeaderCodeMining.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2025, Advantest Europe GmbH + * + * 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: + * Dietrich Travkin - Fix code mining redrawing - Issue 3405 + * + *******************************************************************************/ +package org.eclipse.jface.text.examples.codemining; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.codemining.LineHeaderCodeMining; +import org.eclipse.jface.text.source.inlined.Positions; + +public class ReferenceLineHeaderCodeMining extends LineHeaderCodeMining { + + public ReferenceLineHeaderCodeMining(String label, int beforeLineNumber, int columnInLine, int length, + IDocument document, ICodeMiningProvider provider) throws BadLocationException { + super(calculatePosition(beforeLineNumber, columnInLine, document), provider, null); + this.setLabel(label); + } + + private static Position calculatePosition(int beforeLineNumber, int columnInLine, IDocument document) + throws BadLocationException { + Position pos = Positions.of(beforeLineNumber, document, true); + pos.setOffset(pos.offset + columnInLine); + return pos; + } + +} diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java index 4ac04ebf52a..5d1c16e21ec 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java @@ -18,6 +18,9 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.junit.After; import org.junit.Assert; @@ -55,6 +58,7 @@ import org.eclipse.jface.text.codemining.DocumentFooterCodeMining; import org.eclipse.jface.text.codemining.ICodeMining; import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.codemining.LineContentCodeMining; import org.eclipse.jface.text.codemining.LineHeaderCodeMining; import org.eclipse.jface.text.reconciler.DirtyRegion; import org.eclipse.jface.text.reconciler.IReconcilingStrategy; @@ -62,6 +66,7 @@ import org.eclipse.jface.text.source.AnnotationModel; import org.eclipse.jface.text.source.AnnotationPainter; import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.source.inlined.Positions; import org.eclipse.jface.text.tests.TextViewerTest; import org.eclipse.ui.tests.harness.util.DisplayHelper; @@ -491,6 +496,49 @@ protected boolean condition() { }.waitForCondition(widget.getDisplay(), 1000)); } + @Test + public void testCodeMiningSwitchingBetweenInLineAndLineHeader() { + String ref= "REF-X"; + String text= "Here " + ref + " is a reference."; + fViewer.getDocument().set(text); + int index= text.indexOf(ref); + + // in-line mode + AtomicReference useInLineCodeMinings= new AtomicReference<>(true); + fViewer.setCodeMiningProviders(new ICodeMiningProvider[] { new RefTestCodeMiningProvider(useInLineCodeMinings) }); + + StyledText widget= fViewer.getTextWidget(); + Assert.assertTrue("Line header code minigs were used. Expected in-line code minings instead.", new DisplayHelper() { + @Override + protected boolean condition() { + return widget.getStyleRangeAtOffset(index) != null + && widget.isVisible() && widget.getLineVerticalIndent(0) == 0; + } + }.waitForCondition(widget.getDisplay(), 1000)); + + // switch to line header mode + useInLineCodeMinings.set(false); + fViewer.updateCodeMinings(); + + Assert.assertTrue("In-line code minigs were used (or no code minings at all). Expected line header code minings.", new DisplayHelper() { + @Override + protected boolean condition() { + return widget.getStyleRangeAtOffset(index) == null && widget.getLineVerticalIndent(0) > 0; + } + }.waitForCondition(widget.getDisplay(), 1000)); + + // switch back to in-line mode + useInLineCodeMinings.set(true); + fViewer.updateCodeMinings(); + + Assert.assertTrue("Line header code minigs were used. Expected in-line code minings instead.", new DisplayHelper() { + @Override + protected boolean condition() { + return widget.getStyleRangeAtOffset(index) != null && widget.getLineVerticalIndent(0) == 0; + } + }.waitForCondition(widget.getDisplay(), 1000)); + } + private static boolean hasCodeMiningPrintedBelowLine(ITextViewer viewer, int line) throws BadLocationException { StyledText widget= viewer.getTextWidget(); IDocument document= viewer.getDocument(); @@ -574,4 +622,85 @@ private static boolean hasCodeMiningPrintedAfterTextOnLine(ITextViewer viewer, i image.dispose(); return false; } + + private static class RefTestCodeMiningProvider extends AbstractCodeMiningProvider { + + private static final String REGEX_REF= "REF-X"; + + private static final Pattern REGEX_PATTERN= Pattern.compile(REGEX_REF); + + private AtomicReference useInLineCodeMinings; + + public RefTestCodeMiningProvider(AtomicReference useInLineCodeMinings) { + this.useInLineCodeMinings= useInLineCodeMinings; + } + + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, + IProgressMonitor monitor) { + return CompletableFuture.supplyAsync(() -> { + IDocument document= viewer.getDocument(); + + if (document == null) { + return Collections.emptyList(); + } + + return createCodeMiningsFor(document); + }); + } + + List createCodeMiningsFor(IDocument document) { + String documentContent= document.get(); + List minings= new ArrayList<>(); + + Matcher regexMatcher= REGEX_PATTERN.matcher(documentContent); + while (regexMatcher.find()) { + int startIndex= regexMatcher.start(); + String title= "Building commercial quality plug-ins"; + + if (useInLineCodeMinings.get()) { + minings.add(new ReferenceInLineCodeMining(title + ": ", startIndex, this)); + } else { + try { + int offset= startIndex; + int line= document.getLineOfOffset(offset); + int lineOffset= document.getLineOffset(line); + + minings.add(new ReferenceLineHeaderCodeMining(title, line, offset - lineOffset, + document, this)); + } catch (BadLocationException e) { + e.printStackTrace(); + } + } + } + + return minings; + } + } + + private static class ReferenceInLineCodeMining extends LineContentCodeMining { + + public ReferenceInLineCodeMining(String label, int positionOffset, ICodeMiningProvider provider) { + super(new Position(positionOffset, 1), true, provider); + this.setLabel(label); + } + + } + + private static class ReferenceLineHeaderCodeMining extends LineHeaderCodeMining { + + public ReferenceLineHeaderCodeMining(String label, int beforeLineNumber, int columnInLine, + IDocument document, ICodeMiningProvider provider) throws BadLocationException { + super(calculatePosition(beforeLineNumber, columnInLine, document), provider, null); + this.setLabel(label); + } + + private static Position calculatePosition(int beforeLineNumber, int columnInLine, IDocument document) + throws BadLocationException { + Position pos= Positions.of(beforeLineNumber, document, true); + pos.setOffset(pos.offset + columnInLine); + return pos; + } + + } }