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; + } }