From 794db81ea0ddf8f68297aa9b134bcbd001a6ad9d Mon Sep 17 00:00:00 2001 From: Tobias Melcher Date: Fri, 3 Jan 2025 16:32:13 +0100 Subject: [PATCH] support rendering of code minings at the very end of the document if the last line at the end of the document is empty Fixes: #2157 --- .../InlinedAnnotationDrawingStrategy.java | 85 ++++++++++++++++--- .../text/tests/codemining/CodeMiningTest.java | 79 ++++++++++++++++- .../StaticContentLineCodeMining.java | 5 ++ 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/inlined/InlinedAnnotationDrawingStrategy.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/inlined/InlinedAnnotationDrawingStrategy.java index ba3cbf1c7db..2f9424847d5 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/inlined/InlinedAnnotationDrawingStrategy.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/inlined/InlinedAnnotationDrawingStrategy.java @@ -28,6 +28,7 @@ import org.eclipse.jface.internal.text.codemining.CodeMiningLineHeaderAnnotation; import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.AnnotationPainter.IDrawingStrategy; @@ -99,7 +100,7 @@ private Font getAnnotationFont(StyledText textWidget) { if (annotationFont == null) { annotationFont = createInlineAnnotationFont(textWidget); textWidget.setData(INLINE_ANNOTATION_FONT, annotationFont); - textWidget.addDisposeListener(e -> ((Font)textWidget.getData(INLINE_ANNOTATION_FONT)).dispose()); + textWidget.addDisposeListener(e -> ((Font) textWidget.getData(INLINE_ANNOTATION_FONT)).dispose()); } return annotationFont; } @@ -154,7 +155,8 @@ public static void draw(AbstractInlinedAnnotation annotation, GC gc, StyledText private static void draw(LineHeaderAnnotation annotation, GC gc, StyledText textWidget, int offset, int length, Color color) { int line= textWidget.getLineAtOffset(offset); - if (isDeleted(annotation)) { + int charCount= textWidget.getCharCount(); + if (isDeleted(annotation, charCount)) { // When annotation is deleted, update metrics to null to remove extra spaces of the line header annotation. if (textWidget.getLineVerticalIndent(line) > 0) textWidget.setLineVerticalIndent(line, 0); @@ -180,9 +182,16 @@ private static void draw(LineHeaderAnnotation annotation, GC gc, StyledText text textWidget.setLineVerticalIndent(line, 0); } // Compute the location of the annotation - Rectangle bounds= textWidget.getTextBounds(offset, offset); - int x= bounds.x; - int y= bounds.y; + int x, y; + if (offset < charCount) { + Rectangle bounds= textWidget.getTextBounds(offset, offset); + x= bounds.x; + y= bounds.y; + } else { + Point locAtOff= textWidget.getLocationAtOffset(offset); + x= locAtOff.x; + y= locAtOff.y - height; + } // Draw the line header annotation gc.setBackground(textWidget.getBackground()); annotation.setLocation(x, y); @@ -193,7 +202,16 @@ private static void draw(LineHeaderAnnotation annotation, GC gc, StyledText text Rectangle client= textWidget.getClientArea(); textWidget.redraw(0, bounds.y, client.width, bounds.height, false); } else { - textWidget.redrawRange(offset, length, true); + if (offset >= charCount) { + if (charCount > 0) { + textWidget.redrawRange(charCount - 1, 1, true); + } else { + Rectangle client= textWidget.getClientArea(); + textWidget.redraw(0, 0, client.width, client.height, false); + } + } else { + textWidget.redrawRange(offset, length, true); + } } } @@ -212,7 +230,11 @@ private static void draw(LineContentAnnotation annotation, GC gc, StyledText tex Color color) { if (annotation instanceof CodeMiningLineContentAnnotation a) { if (a.isAfterPosition()) { - drawAsLeftOf1stCharacter(annotation, gc, textWidget, widgetOffset, length, color); + if (widgetOffset < textWidget.getCharCount()) { + drawAsLeftOf1stCharacter(annotation, gc, textWidget, widgetOffset, length, color); + } else { + drawAtEndOfDocumentInFirstColumn(annotation, gc, textWidget, widgetOffset, length, color); + } return; } } @@ -226,7 +248,7 @@ private static void draw(LineContentAnnotation annotation, GC gc, StyledText tex } private static void drawAfterLine(LineContentAnnotation annotation, GC gc, StyledText textWidget, int widgetOffset, int length, Color color) { - if (isDeleted(annotation)) { + if (isDeleted(annotation, textWidget.getCharCount())) { return; } if (gc != null) { @@ -247,6 +269,35 @@ private static void drawAfterLine(LineContentAnnotation annotation, GC gc, Style } } + private static void drawAtEndOfDocumentInFirstColumn(LineContentAnnotation annotation, GC gc, StyledText textWidget, int widgetOffset, int length, Color color) { + if (isDeleted(annotation, textWidget.getCharCount())) { + return; + } + if (gc != null) { + Point locAtOff= textWidget.getLocationAtOffset(widgetOffset); + int x= locAtOff.x; + int y= locAtOff.y; + annotation.setLocation(x, y); + annotation.draw(gc, textWidget, widgetOffset, length, color, x, y); + int width= annotation.getWidth(); + if (width != 0) { + if (!gc.getClipping().contains(x, y)) { + Rectangle client= textWidget.getClientArea(); + int height= textWidget.getLineHeight(); + textWidget.redraw(x, y, client.width, height, false); + } + } + } else { + int charCount= textWidget.getCharCount(); + if (charCount > 0) { + textWidget.redrawRange(charCount - 1, 1, true); + } else { + Rectangle client= textWidget.getClientArea(); + textWidget.redraw(0, 0, client.width, client.height, false); + } + } + } + protected static void drawAsLeftOf1stCharacter(LineContentAnnotation annotation, GC gc, StyledText textWidget, int widgetOffset, int length, Color color) { StyleRange style= null; try { @@ -254,7 +305,7 @@ protected static void drawAsLeftOf1stCharacter(LineContentAnnotation annotation, } catch (Exception e) { return; } - if (isDeleted(annotation)) { + if (isDeleted(annotation, textWidget.getCharCount())) { // When annotation is deleted, update metrics to null to remove extra spaces of the line content annotation. if (style != null && style.metrics != null) { style.metrics= null; @@ -369,7 +420,7 @@ protected static void drawAsRightOfPreviousCharacter(LineContentAnnotation annot } catch (Exception e) { return; } - if (isDeleted(annotation)) { + if (isDeleted(annotation, textWidget.getCharCount())) { // When annotation is deleted, update metrics to null to remove extra spaces of the line content annotation. if (style != null && style.metrics != null) { style.metrics= null; @@ -449,7 +500,17 @@ protected static void drawAsRightOfPreviousCharacter(LineContentAnnotation annot * @param annotation the inlined annotation to check * @return true if inlined annotation is deleted and false otherwise. */ - private static boolean isDeleted(AbstractInlinedAnnotation annotation) { - return annotation.isMarkedDeleted() || annotation.getPosition().isDeleted() || annotation.getPosition().getLength() == 0; + private static boolean isDeleted(AbstractInlinedAnnotation annotation,int maxOffset) { + if (annotation.isMarkedDeleted()) { + return true; + } + Position pos= annotation.getPosition(); + if (pos.isDeleted()) { + return true; + } + if (pos.getLength() == 0 && pos.getOffset() < maxOffset) { + return true; + } + return false; } } 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 b5558b2ff6e..d2442eb1c5f 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 @@ -35,6 +35,7 @@ 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.layout.FillLayout; import org.eclipse.swt.widgets.Display; @@ -250,6 +251,71 @@ protected boolean condition() { }.waitForCondition(fViewer.getTextWidget().getDisplay(), 1000)); } + @Test + public void testLineHeaderCodeMiningAtEndOfDocumentWithEmptyLine() throws Exception { + String source= "first\nsecond\n"; + fViewer.getDocument().set(source); + fViewer.setCodeMiningProviders(new ICodeMiningProvider[] { new ICodeMiningProvider() { + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, IProgressMonitor monitor) { + List minings= new ArrayList<>(); + try { + minings.add(new LineHeaderCodeMining(new Position(source.length(), 0), this, null) { + @Override + public String getLabel() { + return "multiline first line\nmultiline second line\nmultiline third line\nmultiline fourth line"; + } + }); + } catch (BadLocationException e) { + e.printStackTrace(); + } + return CompletableFuture.completedFuture(minings); + } + + @Override + public void dispose() { + } + } }); + Assert.assertTrue("Code mining is not visible at end of document", new DisplayHelper() { + @Override + protected boolean condition() { + try { + return hasCodeMiningPrintedAfterTextOnLine(fViewer, 2); + } catch (BadLocationException e) { + e.printStackTrace(); + return false; + } + } + }.waitForCondition(fViewer.getTextWidget().getDisplay(), 10_000)); + } + + @Test + public void testCodeMiningAtEndOfDocumentWithEmptyLine() throws Exception { + String source= "first\nsecond\n"; + fViewer.getDocument().set(source); + fViewer.setCodeMiningProviders(new ICodeMiningProvider[] { new ICodeMiningProvider() { + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, IProgressMonitor monitor) { + return CompletableFuture.completedFuture(Collections.singletonList(new StaticContentLineCodeMining(new Position(source.length(), 0), true, "mining", this))); + } + + @Override + public void dispose() { + } + } }); + Assert.assertTrue("Code mining is not visible at end of document", new DisplayHelper() { + @Override + protected boolean condition() { + try { + return hasCodeMiningPrintedAfterTextOnLine(fViewer, 2); + } catch (BadLocationException e) { + e.printStackTrace(); + return false; + } + } + }.waitForCondition(fViewer.getTextWidget().getDisplay(), 10_000)); + } + @Test public void testCodeMiningEndOfLine() { fViewer.getDocument().set("a\n"); @@ -389,7 +455,18 @@ private static boolean hasCodeMiningPrintedAfterTextOnLine(ITextViewer viewer, i if (lineLength < 0) { lineLength= 0; } - Rectangle secondLineBounds= widget.getTextBounds(document.getLineOffset(line), document.getLineOffset(line) + lineLength); + Rectangle secondLineBounds= null; + int lineOffset= document.getLineOffset(line); + if (lineOffset >= document.getLength()) { + int off= document.getLength() - 1; + secondLineBounds= widget.getTextBounds(off, off + lineLength); + Point l= widget.getLocationAtOffset(lineOffset); + int lineVerticalIndent= widget.getLineVerticalIndent(line); + secondLineBounds.x= l.x; + secondLineBounds.y= l.y - lineVerticalIndent; + } else { + secondLineBounds= widget.getTextBounds(lineOffset, lineOffset + lineLength); + } Image image = new Image(widget.getDisplay(), widget.getSize().x, widget.getSize().y); GC gc = new GC(widget); gc.copyArea(image, 0, 0); diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/StaticContentLineCodeMining.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/StaticContentLineCodeMining.java index 726b7a698ca..f44ca28dd22 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/StaticContentLineCodeMining.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/StaticContentLineCodeMining.java @@ -24,6 +24,11 @@ public StaticContentLineCodeMining(Position position, String message, ICodeMinin setLabel(message); } + public StaticContentLineCodeMining(Position position, boolean afterPosition, String message, ICodeMiningProvider provider) { + super(position, afterPosition, provider); + setLabel(message); + } + public StaticContentLineCodeMining(int i, char c, ICodeMiningProvider repeatLettersCodeMiningProvider) { super(new Position(i, 1), repeatLettersCodeMiningProvider); setLabel(Character.toString(c));