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