From c7d5ff6e1430258b1a03e0fe573b5d9deb269da9 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Tue, 21 Oct 2025 18:37:24 +0200 Subject: [PATCH] hide projection annotations outside of the current visible region Projection regions that overlap with parts of the file outside of the current visible region cannot be collapsed hence their annotations should not be painted. --- .../projection/ProjectionAnnotation.java | 9 ++ .../source/projection/ProjectionViewer.java | 72 ++++++++- .../META-INF/MANIFEST.MF | 1 + .../text/tests/ProjectionViewerTest.java | 148 ++++++++++++++++++ 4 files changed, 227 insertions(+), 3 deletions(-) diff --git a/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionAnnotation.java b/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionAnnotation.java index 767944429f4..b8d5fd55e78 100644 --- a/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionAnnotation.java +++ b/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionAnnotation.java @@ -73,6 +73,8 @@ public void run() { /** Indicates whether this annotation should be painted as range */ private boolean fIsRangeIndication= false; + private boolean hidden= false; + /** * Creates a new expanded projection annotation. */ @@ -115,6 +117,9 @@ private void drawRangeIndication(GC gc, Canvas canvas, Rectangle r) { @Override public void paint(GC gc, Canvas canvas, Rectangle rectangle) { + if (hidden) { + return; + } Image image= getImage(canvas.getDisplay()); if (image != null) { ImageUtilities.drawImage(image, gc, canvas, rectangle, SWT.CENTER, SWT.TOP); @@ -128,6 +133,10 @@ public void paint(GC gc, Canvas canvas, Rectangle rectangle) { } } + void setHidden(boolean hidden) { + this.hidden= hidden; + } + @Override public int getLayer() { return IAnnotationPresentation.DEFAULT_LAYER; diff --git a/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java b/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java index 86595002d6b..6fcfb4e5169 100644 --- a/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java +++ b/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java @@ -124,6 +124,7 @@ private void processModelChanged(IAnnotationModel model, AnnotationModelEvent ev fProjectionSummary.updateSummaries(); } processCatchupRequest(event); + correctChangedAnnotationVisibility(event); } else if (model == getAnnotationModel() && fProjectionSummary != null) { fProjectionSummary.updateSummaries(); @@ -770,15 +771,46 @@ public void setVisibleRegion(int start, int length) { // If the visible region changes, make sure collapsed regions outside of the old visible regions are expanded // and collapse everything outside the new visible region int end= computeEndOfVisibleRegion(start, length, document); + Region newVisibleRegion= new Region(start, end - start - 1); + expandProjectionAnnotationsBorderingRegion(newVisibleRegion); expandOutsideCurrentVisibleRegion(document); collapseOutsideOfNewVisibleRegion(start, end, document); - fConfiguredVisibleRegion= new Region(start, end - start - 1); + fConfiguredVisibleRegion= newVisibleRegion; + hideProjectionAnnotationsOutsideOfVisibleRegion(); } catch (BadLocationException e) { ILog log= ILog.of(getClass()); log.log(new Status(IStatus.WARNING, getClass(), IStatus.OK, null, e)); } } + private void expandProjectionAnnotationsBorderingRegion(Region region) throws BadLocationException { + for (Iterator it= fProjectionAnnotationModel.getAnnotationIterator(); it.hasNext();) { + Annotation annotation= it.next(); + Position position= fProjectionAnnotationModel.getPosition(annotation); + if (bordersOrSurroundsRegion(position, region)) { + fProjectionAnnotationModel.expand(annotation); + } + } + } + + private void hideProjectionAnnotationsOutsideOfVisibleRegion() throws BadLocationException { + for (Iterator it= fProjectionAnnotationModel.getAnnotationIterator(); it.hasNext();) { + Annotation annotation= it.next(); + hideProjectionAnnotationIfPartsAreOutsideOfVisibleRegion(annotation); + } + } + + private void hideProjectionAnnotationIfPartsAreOutsideOfVisibleRegion(Annotation annotation) throws BadLocationException { + Position position= fProjectionAnnotationModel.getPosition(annotation); + if (annotation instanceof ProjectionAnnotation a) { + if (overlapsWithNonVisibleRegions(position.getOffset(), position.getLength())) { + a.setHidden(true); + } else { + a.setHidden(false); + } + } + } + private void expandOutsideCurrentVisibleRegion(IDocument document) throws BadLocationException { if (fConfiguredVisibleRegion != null) { expand(0, fConfiguredVisibleRegion.getOffset(), false, true); @@ -855,6 +887,12 @@ public void resetVisibleRegion() { super.resetVisibleRegion(); } fConfiguredVisibleRegion= null; + for (Iterator it= fProjectionAnnotationModel.getAnnotationIterator(); it.hasNext();) { + Annotation annotation= it.next(); + if (annotation instanceof ProjectionAnnotation a) { + a.setHidden(false); + } + } } @Override @@ -1014,11 +1052,25 @@ private boolean overlapsWithNonVisibleRegions(int offset, int length) throws Bad return false; } // ignore overlaps within the same line - int visibleRegionStartLineOffset= getDocument().getLineInformationOfOffset(fConfiguredVisibleRegion.getOffset()).getOffset(); - int regionToCheckEndLineOffset= getDocument().getLineInformationOfOffset(offset + length).getOffset(); + int visibleRegionStartLineOffset= atStartOfLine(fConfiguredVisibleRegion.getOffset()); + int regionToCheckEndLineOffset= atStartOfLine(offset + length); return offset < visibleRegionStartLineOffset || regionToCheckEndLineOffset > fConfiguredVisibleRegion.getOffset() + fConfiguredVisibleRegion.getLength(); } + + private boolean bordersOrSurroundsRegion(Position position, Region region) throws BadLocationException { + if (atStartOfLine(position.getOffset()) <= region.getOffset() + region.getLength() + && atStartOfLine(position.getOffset() + position.length) >= region.getOffset() + region.getLength()) { + return true; + } + return atStartOfLine(position.getOffset()) <= region.getOffset() + && position.getOffset() + position.getLength() > atStartOfLine(region.getOffset()); + } + + private int atStartOfLine(int off) throws BadLocationException { + return getDocument().getLineInformationOfOffset(off).getOffset(); + } + /** * Processes the request for catch up with the annotation model in the UI thread. If the current * thread is not the UI thread or there are pending catch up requests, a new request is posted. @@ -1090,6 +1142,20 @@ protected final void postCatchupRequest(final AnnotationModelEvent event) { } } + private void correctChangedAnnotationVisibility(AnnotationModelEvent event) { + try { + for (Annotation annotation : event.getAddedAnnotations()) { + hideProjectionAnnotationIfPartsAreOutsideOfVisibleRegion(annotation); + } + for (Annotation annotation : event.getChangedAnnotations()) { + hideProjectionAnnotationIfPartsAreOutsideOfVisibleRegion(annotation); + } + } catch (BadLocationException e) { + ILog log= ILog.of(getClass()); + log.log(new Status(IStatus.WARNING, getClass(), IStatus.OK, null, e)); + } + } + /** * Tests whether the visible document's master document * is identical to this viewer's document. diff --git a/tests/org.eclipse.jface.text.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.jface.text.tests/META-INF/MANIFEST.MF index fdef5681ba0..c18417e5f9c 100644 --- a/tests/org.eclipse.jface.text.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.jface.text.tests/META-INF/MANIFEST.MF @@ -30,6 +30,7 @@ Import-Package: org.mockito, org.mockito.invocation, org.mockito.stubbing, org.junit.jupiter.api;version="[5.14.0,6.0.0)", + org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", org.junit.jupiter.params;version="[5.14.0,6.0.0)", org.junit.jupiter.params.provider;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)" diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java index e3f2bfe6ca9..e32c54520a7 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java @@ -10,7 +10,10 @@ *******************************************************************************/ package org.eclipse.jface.text.tests; +import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -19,6 +22,7 @@ import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.jface.text.BadLocationException; @@ -368,4 +372,148 @@ public void testRemoveEntireVisibleRegion() throws BadLocationException { shell.dispose(); } } + + @Test + public void testSetVisibleRegionDoesNotExpandOutsideProjectionRegions() { + Shell shell= new Shell(); + shell.setLayout(new FillLayout()); + TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE); + String documentContent= """ + Hello + World + abc + 123 + 456 + 789 + """; + Document document= new Document(documentContent); + viewer.setDocument(document, new AnnotationModel()); + viewer.enableProjection(); + ProjectionAnnotation firstAnnotation= new ProjectionAnnotation(true); + ProjectionAnnotation secondAnnotation= new ProjectionAnnotation(true); + viewer.getProjectionAnnotationModel().addAnnotation(firstAnnotation, new Position(0, documentContent.indexOf("World"))); + viewer.getProjectionAnnotationModel().addAnnotation(secondAnnotation, new Position(documentContent.indexOf("456"), documentContent.length() - documentContent.indexOf("456"))); + + viewer.setVisibleRegion(documentContent.indexOf("abc"), documentContent.indexOf("123") - documentContent.indexOf("abc")); + shell.setVisible(true); + try { + assertTrue(firstAnnotation.isCollapsed()); + assertTrue(secondAnnotation.isCollapsed()); + } finally { + shell.dispose(); + } + } + + @Test + public void testSetVisibleRegionExpandsBorderingProjectionRegions() { + Shell shell= new Shell(); + shell.setLayout(new FillLayout()); + TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE); + String documentContent= """ + Hello + World + 123 + 456 + """; + Document document= new Document(documentContent); + viewer.setDocument(document, new AnnotationModel()); + viewer.enableProjection(); + ProjectionAnnotation firstAnnotation= new ProjectionAnnotation(true); + ProjectionAnnotation secondAnnotation= new ProjectionAnnotation(true); + viewer.getProjectionAnnotationModel().addAnnotation(firstAnnotation, new Position(0, documentContent.indexOf("123"))); + viewer.getProjectionAnnotationModel().addAnnotation(secondAnnotation, new Position(documentContent.indexOf("123"), documentContent.length() - documentContent.indexOf("123"))); + + viewer.setVisibleRegion(documentContent.indexOf("World"), documentContent.indexOf("456") - documentContent.indexOf("World")); + shell.setVisible(true); + try { + assertFalse(firstAnnotation.isCollapsed()); + assertFalse(secondAnnotation.isCollapsed()); + } finally { + shell.dispose(); + } + } + + @Test + public void testProjectionRegionsShownOnlyInVisibleRegion() { + Shell shell= new Shell(Display.getCurrent()); + shell.setLayout(new FillLayout()); + TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, true, SWT.ALL); + String documentContent= """ + + visible_region_start + + projection_start + + visible_region_end + + projection_end + + """; + Document document= new Document(documentContent); + viewer.setDocument(document, new AnnotationModel()); + ProjectionAnnotation annotation= addVisibleRegionAndProjection(viewer, documentContent); + try { + assertEquals(""" + visible_region_start + + projection_start + + visible_region_end + """, viewer.getVisibleDocument().get()); + + annotation.paint(null, null, null); //should exit early and not throw NPE + } finally { + shell.dispose(); + } + } + + @Test + public void testProjectionRegionsShownWithinVisibleRegion() { + Shell shell= new Shell(Display.getCurrent()); + shell.setLayout(new FillLayout()); + TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, true, SWT.ALL); + String documentContent= """ + + visible_region_start + + projection_start + + projection_end + + visible_region_end + + """; + Document document= new Document(documentContent); + viewer.setDocument(document, new AnnotationModel()); + ProjectionAnnotation annotation= addVisibleRegionAndProjection(viewer, documentContent); + try { + assertEquals(""" + visible_region_start + + projection_start + + projection_end + + visible_region_end + """, viewer.getVisibleDocument().get()); + + assertThrows(NullPointerException.class, () -> annotation.paint(null, null, null), "expected to run painting logic"); + } finally { + shell.dispose(); + } + } + + private ProjectionAnnotation addVisibleRegionAndProjection(TestProjectionViewer viewer, String documentContent) { + int visibleRegionStart= documentContent.indexOf("visible_region_start"); + int visibleRegionEnd= documentContent.indexOf("\n", documentContent.indexOf("visible_region_end")) + 1; + + int projectionStart= documentContent.indexOf("projection_start"); + int projectionEnd= documentContent.indexOf("\n", documentContent.indexOf("projection_end")) + 1; + + viewer.setVisibleRegion(visibleRegionStart, visibleRegionEnd - visibleRegionStart); + viewer.enableProjection(); + ProjectionAnnotation annotation= new ProjectionAnnotation(); + viewer.getProjectionAnnotationModel().addAnnotation(annotation, new Position(projectionStart, projectionEnd - projectionStart)); + return annotation; + } }