diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/AnnotationPainter.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/AnnotationPainter.java
index 4e967c8df0a..230eba927e8 100644
--- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/AnnotationPainter.java
+++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/AnnotationPainter.java
@@ -1413,7 +1413,12 @@ private void drawDecoration(Decoration pp, GC gc, Annotation annotation, IRegion
int paintStart= Math.max(lineOffset, p.getOffset());
String lineDelimiter= document.getLineDelimiter(i);
int delimiterLength= lineDelimiter != null ? lineDelimiter.length() : 0;
- int paintLength= Math.min(lineOffset + document.getLineLength(i) - delimiterLength, p.getOffset() + p.getLength()) - paintStart;
+ int lineLengthWithoutDelimiter= document.getLineLength(i) - delimiterLength;
+ int paintLength= Math.min(lineOffset + lineLengthWithoutDelimiter, p.getOffset() + p.getLength()) - paintStart;
+ if (paintLength == 0 && lineDelimiter != null && lineDelimiter.length() > 0) {
+ // textWidget.redrawRange with length 0 is ignored and no redraw takes place
+ paintLength= lineDelimiter.length();
+ }
if (paintLength >= 0 && regionsTouchOrOverlap(paintStart, paintLength, clippingOffset, clippingLength)) {
// otherwise inside a line delimiter
IRegion widgetRange= getWidgetRange(paintStart, paintLength);
diff --git a/tests/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF
index 8f3a59a9efc..140fd8f7b70 100644
--- a/tests/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF
+++ b/tests/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF
@@ -5,7 +5,8 @@ Bundle-SymbolicName: org.eclipse.ui.editors.tests;singleton:=true
Bundle-Version: 3.13.500.qualifier
Bundle-Vendor: %Plugin.providerName
Bundle-Localization: plugin
-Export-Package: org.eclipse.ui.editors.tests,
+Export-Package: org.eclipse.jface.text.tests.codemining,
+ org.eclipse.ui.editors.tests,
org.eclipse.ui.internal.texteditor.stickyscroll
Require-Bundle:
org.eclipse.core.runtime;bundle-version="[3.29.0,4.0.0)",
diff --git a/tests/org.eclipse.ui.editors.tests/plugin.xml b/tests/org.eclipse.ui.editors.tests/plugin.xml
index f1c781ff384..c43805fae01 100644
--- a/tests/org.eclipse.ui.editors.tests/plugin.xml
+++ b/tests/org.eclipse.ui.editors.tests/plugin.xml
@@ -16,4 +16,10 @@
extensions="testfile">
+
+
+
+
\ No newline at end of file
diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java
new file mode 100644
index 00000000000..d91521f50f0
--- /dev/null
+++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java
@@ -0,0 +1,293 @@
+/*******************************************************************************
+ * Copyright (c) 2024 SAP
+ *
+ * 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
+ *******************************************************************************/
+package org.eclipse.jface.text.tests.codemining;
+
+import java.io.ByteArrayInputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.Callable;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.widgets.Display;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspaceRunnable;
+import org.eclipse.core.resources.ResourcesPlugin;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.text.source.ISourceViewerExtension5;
+
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.ide.IDE;
+
+public class CodeMiningTest {
+ private static String PROJECT_NAME = "test_" + new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
+
+ private static IProject project;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ hideWelcomePage();
+ createProject(PROJECT_NAME);
+ }
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ if (project != null)
+ project.delete(true, new NullProgressMonitor());
+ }
+
+ @After
+ public void after() {
+ closeAllEditors();
+ drainEventQueue();
+ CodeMiningTestProvider.provideContentMiningAtOffset = -1;
+ CodeMiningTestProvider.provideHeaderMiningAtLine = -1;
+ }
+
+ private static void closeAllEditors() {
+ IWorkbenchWindow[] windows = PlatformUI.getWorkbench().getWorkbenchWindows();
+ for (IWorkbenchWindow window : windows) {
+ for (IWorkbenchPage page : window.getPages()) {
+ page.closeAllEditors(false);
+ }
+ }
+ }
+
+ private static void createProject(String projectName) throws Exception {
+ project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
+ if (project.exists()) {
+ project.delete(true, true, new NullProgressMonitor());
+ }
+ ResourcesPlugin.getWorkspace().run(new IWorkspaceRunnable() {
+
+ @Override
+ public void run(IProgressMonitor monitor) throws CoreException {
+ project.create(monitor);
+ project.open(monitor);
+ }
+
+ }, new NullProgressMonitor());
+ }
+
+ @Test
+ public void testCodeMiningOnEmptyLine() throws Exception {
+ IFile file = project.getFile("test.txt");
+ if (file.exists()) {
+ file.delete(true, new NullProgressMonitor());
+ }
+ String source = "first line\n" + //
+ "\n" + //
+ "third line\n";
+ file.create(new ByteArrayInputStream(source.getBytes("UTF-8")), true, new NullProgressMonitor());
+ IEditorPart editor = IDE.openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), file);
+ drainEventQueue();
+ ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ITextViewer.class);
+ StyledText widget = viewer.getTextWidget();
+ Assert.assertTrue("line content mining not available",
+ waitForCondition(widget.getDisplay(), 1000, new Callable() {
+ @Override
+ public Boolean call() throws Exception {
+ return widget.getStyleRangeAtOffset(0) != null
+ && widget.getStyleRangeAtOffset(0).metrics != null;
+ }
+ }));
+ drainEventQueue();
+
+ CodeMiningTestProvider.provideHeaderMiningAtLine = 1;
+ ((ISourceViewerExtension5) viewer).updateCodeMinings();
+
+ Assert.assertTrue("Code mining not drawn at empty line after calling updateCodeMinings",
+ waitForCondition(widget.getDisplay(), 2000, new Callable() {
+ @Override
+ public Boolean call() throws Exception {
+ try {
+ return existsPixelWithNonBackgroundColorAtLine(viewer, 1);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ }));
+ }
+
+ @Test
+ public void testCodeMiningAtEndOfLine() throws Exception {
+ IFile file = project.getFile("test.txt");
+ if (file.exists()) {
+ file.delete(true, new NullProgressMonitor());
+ }
+ String firstPart = "first line\n" + //
+ "second line";
+ String secondPart = "\n" + //
+ "third line\n";
+ String source = firstPart + secondPart;
+ file.create(new ByteArrayInputStream(source.getBytes("UTF-8")), true, new NullProgressMonitor());
+ IEditorPart editor = IDE.openEditor(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), file);
+ drainEventQueue();
+ ISourceViewer viewer = (ISourceViewer) editor.getAdapter(ITextViewer.class);
+ StyledText widget = viewer.getTextWidget();
+ Assert.assertTrue("line content mining not available",
+ waitForCondition(widget.getDisplay(), 1000, new Callable() {
+ @Override
+ public Boolean call() throws Exception {
+ return widget.getStyleRangeAtOffset(0) != null
+ && widget.getStyleRangeAtOffset(0).metrics != null;
+ }
+ }));
+ drainEventQueue();
+
+ CodeMiningTestProvider.provideContentMiningAtOffset = firstPart.length();
+ ((ISourceViewerExtension5) viewer).updateCodeMinings();
+
+ Assert.assertTrue("Code mining not drawn at the end of second line after calling updateCodeMinings",
+ waitForCondition(widget.getDisplay(), 2000, new Callable() {
+ @Override
+ public Boolean call() throws Exception {
+ try {
+ return existsPixelWithNonBackgroundColorAtEndOfLine(viewer, 1);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ }));
+ }
+
+ private void drainEventQueue() {
+ while (Display.getDefault().readAndDispatch()) {
+ }
+ }
+
+ private static void hideWelcomePage() {
+ IWorkbenchPage ap = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
+ ap.hideView(ap.findViewReference("org.eclipse.ui.internal.introview"));
+ }
+
+ private static boolean existsPixelWithNonBackgroundColorAtLine(ITextViewer viewer, int line)
+ throws BadLocationException {
+ StyledText widget = viewer.getTextWidget();
+ IDocument document = viewer.getDocument();
+ String delim = document.getLineDelimiter(line);
+ int delimLen = 0;
+ if (delim != null) {
+ delimLen = delim.length();
+ }
+ int lineLength = document.getLineLength(line) - delimLen;
+ if (lineLength < 0) {
+ lineLength = 0;
+ }
+ int verticalScroolBarWidth = viewer.getTextWidget().getVerticalBar().getThumbBounds().width;
+ Rectangle lineBounds = widget.getTextBounds(document.getLineOffset(line),
+ document.getLineOffset(line) + lineLength);
+ Image image = new Image(widget.getDisplay(), widget.getSize().x, widget.getSize().y);
+ try {
+ GC gc = new GC(widget);
+ gc.copyArea(image, 0, 0);
+ gc.dispose();
+ ImageData imageData = image.getImageData();
+ for (int x = lineBounds.x + 1; x < image.getBounds().width - verticalScroolBarWidth
+ && x < imageData.width - verticalScroolBarWidth; x++) {
+ for (int y = lineBounds.y; y < lineBounds.y + lineBounds.height; y++) {
+ if (!imageData.palette.getRGB(imageData.getPixel(x, y)).equals(widget.getBackground().getRGB())) {
+ return true;
+ }
+ }
+ }
+ } finally {
+ image.dispose();
+ }
+ return false;
+ }
+
+ private static boolean existsPixelWithNonBackgroundColorAtEndOfLine(ITextViewer viewer, int line)
+ throws BadLocationException {
+ StyledText widget = viewer.getTextWidget();
+ IDocument document = viewer.getDocument();
+ int verticalScroolBarWidth = viewer.getTextWidget().getVerticalBar().getThumbBounds().width;
+ int lineOffset = document.getLineOffset(line);
+ String lineStr = document.get(lineOffset,
+ document.getLineLength(line) - document.getLineDelimiter(line).length());
+ Rectangle lineBounds = widget.getTextBounds(lineOffset, lineOffset);
+ Image image = new Image(widget.getDisplay(), widget.getSize().x, widget.getSize().y);
+ try {
+ GC gc = new GC(widget);
+ gc.copyArea(image, 0, 0);
+ Point textExtent = gc.textExtent(lineStr);
+ lineBounds.x += textExtent.x;
+ gc.dispose();
+ ImageData imageData = image.getImageData();
+ for (int x = lineBounds.x + 1; x < image.getBounds().width - verticalScroolBarWidth
+ && x < imageData.width - verticalScroolBarWidth; x++) {
+ for (int y = lineBounds.y; y < lineBounds.y + lineBounds.height; y++) {
+ if (!imageData.palette.getRGB(imageData.getPixel(x, y)).equals(widget.getBackground().getRGB())) {
+ return true;
+ }
+ }
+ }
+ } finally {
+ image.dispose();
+ }
+ return false;
+ }
+
+ private final boolean waitForCondition(Display display, long timeout, Callable condition)
+ throws Exception {
+ // if the condition already holds, succeed
+ if (condition.call()) {
+ return true;
+ }
+ if (timeout < 0) {
+ return false;
+ }
+ drainEventQueue();
+ if (condition.call()) {
+ return true;
+ }
+ if (timeout == 0) {
+ return false;
+ }
+ // repeatedly sleep until condition becomes true or timeout elapses
+ long start = System.currentTimeMillis();
+ long diff = timeout;
+ Boolean cond = false;
+ do {
+ if (display.sleep()) {
+ drainEventQueue();
+ }
+ cond = condition.call();
+ diff = System.currentTimeMillis() - start;
+ } while (!cond && diff < timeout);
+ return cond;
+ }
+}
diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTestProvider.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTestProvider.java
new file mode 100644
index 00000000000..60f5ff4ffee
--- /dev/null
+++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTestProvider.java
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * Copyright (c) 2024 SAP
+ *
+ * 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
+ *******************************************************************************/
+package org.eclipse.jface.text.tests.codemining;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.codemining.AbstractCodeMining;
+import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider;
+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;
+
+public class CodeMiningTestProvider extends AbstractCodeMiningProvider {
+ public static int provideHeaderMiningAtLine = -1;
+ public static int provideContentMiningAtOffset = -1;
+
+ @Override
+ public CompletableFuture> provideCodeMinings(ITextViewer viewer,
+ IProgressMonitor monitor) {
+ try {
+ List minings = new ArrayList<>();
+ // used as indication when the code minings are finished with
+ // drawing - widget.getStyleRangeAtOffset(0).metrics
+ minings.add(new StaticContentLineCodeMining(new Position(1, 1), "mining", this));
+ if (provideHeaderMiningAtLine >= 0) {
+ minings.add(new LineHeaderCodeMining(provideHeaderMiningAtLine, viewer.getDocument(), this) {
+ @Override
+ public String getLabel() {
+ return "line header mining";
+ }
+ });
+ }
+ if (provideContentMiningAtOffset >= 0) {
+ minings.add(new AbstractCodeMining(new Position(provideContentMiningAtOffset, 1), this, null) {
+ @Override
+ public String getLabel() {
+ return "Content mining";
+ }
+ });
+ }
+ return CompletableFuture.completedFuture(minings);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static final class StaticContentLineCodeMining extends LineContentCodeMining {
+
+ public StaticContentLineCodeMining(Position position, String message, ICodeMiningProvider provider) {
+ super(position, provider);
+ setLabel(message);
+ }
+
+ public StaticContentLineCodeMining(int i, char c, ICodeMiningProvider repeatLettersCodeMiningProvider) {
+ super(new Position(i, 1), repeatLettersCodeMiningProvider);
+ setLabel(Character.toString(c));
+ }
+
+ @Override
+ public boolean isResolved() {
+ return true;
+ }
+ }
+}
diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java
index 0154edc63a5..66cda995575 100644
--- a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java
+++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java
@@ -19,6 +19,8 @@
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
+import org.eclipse.jface.text.tests.codemining.CodeMiningTest;
+
import org.eclipse.ui.internal.texteditor.stickyscroll.StickyLinesProviderTest;
import org.eclipse.ui.internal.texteditor.stickyscroll.StickyScrollingControlTest;
import org.eclipse.ui.internal.texteditor.stickyscroll.StickyScrollingHandlerTest;
@@ -48,6 +50,8 @@
StickyScrollingControlTest.class,
StickyScrollingHandlerTest.class,
StickyLinesProviderTest.class,
+
+ CodeMiningTest.class,
})
public class EditorsTestSuite {
// see @SuiteClasses