From 673bde492c52e0435f30e0a09e83289e7f7d11c2 Mon Sep 17 00:00:00 2001 From: Dietrich Travkin Date: Fri, 17 Oct 2025 19:17:21 +0200 Subject: [PATCH] Fix incorrect reuse of code mining annotations with mismatched types Drawing code minings depends on the type of code mining annotations that are created for code minings (that are created by code mining providers). When redrawing code minings, the corresponding annotations have to be updated or re-created. The annotations' types have to comply with the code minings' types, i.e. a LineContentCodeMining must go with a CodeMiningLineContentAnnotation, a LineHeaderCodeMining with a CodeMiningLineHeaderAnnotation, and a DocumentFooterCodeMining with a CodeMiningDocumentFooterAnnotation. This PR fixes re-creation of annotations. In some cases, annotations were reused, despite of their wrong type. Now, they are re-created with the required type. The PR also extends the code mining demo to reproduce the issue #3405 and adds an automated test. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/3405 --- .../text/codemining/CodeMiningManager.java | 62 +++++++-- .../examples/codemining/CodeMiningDemo.java | 41 ++++-- .../ReferenceCodeMiningProvider.java | 88 ++++++++++++ .../codemining/ReferenceInLineCodeMining.java | 30 ++++ .../ReferenceLineHeaderCodeMining.java | 39 ++++++ .../text/tests/codemining/CodeMiningTest.java | 129 ++++++++++++++++++ 6 files changed, 367 insertions(+), 22 deletions(-) create mode 100644 examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceCodeMiningProvider.java create mode 100644 examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceInLineCodeMining.java create mode 100644 examples/org.eclipse.jface.text.examples/src/org/eclipse/jface/text/examples/codemining/ReferenceLineHeaderCodeMining.java 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; + } + + } }