From 92394ce525fd3cbd2d5ff844d6117bd12dca16f3 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 5 Jul 2025 14:39:06 +0200 Subject: [PATCH] Allow using visible regions with projections #3073 While ProjectionViewer supports both using visible regions and projections, these features cannot be used in conjunction. This change allows the use of projections when visible regions are used. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/pull/3074 --- .../source/projection/ProjectionViewer.java | 171 +++++++++- .../text/tests/ProjectionViewerTest.java | 295 +++++++++++++++++- 2 files changed, 451 insertions(+), 15 deletions(-) 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 fa4eac0e6a1..50fbf0e1654 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2018 IBM Corporation and others. + * Copyright (c) 2000, 2025 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -33,6 +33,9 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; import org.eclipse.jface.internal.text.SelectionProcessor; @@ -277,6 +280,32 @@ private void computeExpectedExecutionCosts() { } } + /** + * An {@link IDocumentListener} that makes sure that {@link #fVisibleRegionDuringProjection} is + * updated when the document changes and ensures that the collapsed region after the visible + * region is recreated appropriately. + */ + private final class UpdateDocumentListener implements IDocumentListener { + @Override + public void documentChanged(DocumentEvent event) { + if (fVisibleRegionDuringProjection == null) { + return; + } + int oldLength= event.getLength(); + int newLength= event.getText().length(); + int oldVisibleRegionEnd= fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength(); + if (event.getOffset() < fVisibleRegionDuringProjection.getOffset()) { + fVisibleRegionDuringProjection= new Region(fVisibleRegionDuringProjection.getOffset() + newLength - oldLength, fVisibleRegionDuringProjection.getLength()); + } else if (event.getOffset() + oldLength <= oldVisibleRegionEnd) { + fVisibleRegionDuringProjection= new Region(fVisibleRegionDuringProjection.getOffset(), fVisibleRegionDuringProjection.getLength() + newLength - oldLength); + } + } + + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + } + } + /** The projection annotation model used by this viewer. */ private ProjectionAnnotationModel fProjectionAnnotationModel; /** The annotation model listener */ @@ -297,6 +326,11 @@ private void computeExpectedExecutionCosts() { private IDocument fReplaceVisibleDocumentExecutionTrigger; /** true if projection was on the last time we switched to segmented mode. */ private boolean fWasProjectionEnabled; + /** + * The region set by {@link #setVisibleRegion(int, int)} during projection or null + * if not in a projection + */ + private IRegion fVisibleRegionDuringProjection; /** The queue of projection commands used to assess the costs of projection changes. */ private ProjectionCommandQueue fCommandQueue; /** @@ -306,6 +340,7 @@ private void computeExpectedExecutionCosts() { */ private int fDeletedLines; + private UpdateDocumentListener fUpdateDocumentListener; /** * Creates a new projection source viewer. @@ -318,6 +353,7 @@ private void computeExpectedExecutionCosts() { */ public ProjectionViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, boolean showsAnnotationOverview, int styles) { super(parent, ruler, overviewRuler, showsAnnotationOverview, styles); + fUpdateDocumentListener= new UpdateDocumentListener(); } /** @@ -514,6 +550,14 @@ public final void disableProjection() { fProjectionAnnotationModel.removeAllAnnotations(); fFindReplaceDocumentAdapter= null; fireProjectionDisabled(); + if (fVisibleRegionDuringProjection != null) { + super.setVisibleRegion(fVisibleRegionDuringProjection.getOffset(), fVisibleRegionDuringProjection.getLength()); + fVisibleRegionDuringProjection= null; + } + IDocument document= getDocument(); + if (document != null) { + document.removeDocumentListener(fUpdateDocumentListener); + } } } @@ -525,6 +569,15 @@ public final void enableProjection() { addProjectionAnnotationModel(getVisualAnnotationModel()); fFindReplaceDocumentAdapter= null; fireProjectionEnabled(); + IDocument document= getDocument(); + if (document == null) { + return; + } + IRegion visibleRegion= getVisibleRegion(); + if (visibleRegion != null && (visibleRegion.getOffset() != 0 || visibleRegion.getLength() != 0) && visibleRegion.getLength() < document.getLength()) { + setVisibleRegion(visibleRegion.getOffset(), visibleRegion.getLength()); + } + document.addDocumentListener(fUpdateDocumentListener); } } @@ -533,6 +586,10 @@ private void expandAll() { IDocument doc= getDocument(); int length= doc == null ? 0 : doc.getLength(); if (isProjectionMode()) { + if (fVisibleRegionDuringProjection != null) { + offset= fVisibleRegionDuringProjection.getOffset(); + length= fVisibleRegionDuringProjection.getLength(); + } fProjectionAnnotationModel.expandAll(offset, length); } } @@ -691,9 +748,75 @@ private int toLineStart(IDocument document, int offset, boolean testLastLine) th @Override public void setVisibleRegion(int start, int length) { - fWasProjectionEnabled= isProjectionMode(); - disableProjection(); - super.setVisibleRegion(start, length); + if (!isProjectionMode()) { + super.setVisibleRegion(start, length); + return; + } + IDocument document= getDocument(); + if (document == null) { + return; + } + try { + // 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); + expandOutsideCurrentVisibleRegion(document); + collapseOutsideOfNewVisibleRegion(start, end, document); + fVisibleRegionDuringProjection= new Region(start, end - start - 1); + } catch (BadLocationException e) { + ILog log= ILog.of(getClass()); + log.log(new Status(IStatus.WARNING, getClass(), IStatus.OK, null, e)); + } + } + + private void expandOutsideCurrentVisibleRegion(IDocument document) throws BadLocationException { + if (fVisibleRegionDuringProjection != null) { + expand(0, fVisibleRegionDuringProjection.getOffset(), false, true); + int oldEnd= fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength(); + int length= document.getLength() - oldEnd; + if (length > 0) { + expand(oldEnd, length, false, true); + } + } + } + + private void collapseOutsideOfNewVisibleRegion(int start, int end, IDocument document) throws BadLocationException { + int documentLength= document.getLength(); + collapse(0, start, true, true); + + int endInvisibleRegionLength= documentLength - end; + + if (isLineBreak(document.getChar(documentLength - 1))) { + // if the file ends with an empty line, make sure it is included as well (ensuring the user doesn't accidentially remove parts outside the visible region) + endInvisibleRegionLength++; + } + if (endInvisibleRegionLength > 0) { + collapse(end, endInvisibleRegionLength, true, true); + } + } + + private static int computeEndOfVisibleRegion(int start, int length, IDocument document) throws BadLocationException { + int documentLength= document.getLength(); + int end= start + length + 1; + // ensure that trailing whitespace is included + // In this case, the line break needs to be included as well + boolean visibleRegionEndsWithTrailingWhitespace= end < documentLength && isWhitespaceButNotNewline(document.getChar(end - 1)); + while (end < documentLength && isWhitespaceButNotNewline(document.getChar(end))) { + end++; + visibleRegionEndsWithTrailingWhitespace= true; + } + if (visibleRegionEndsWithTrailingWhitespace && end < documentLength && isLineBreak(document.getChar(end))) { + end++; + } + return end; + } + + private static boolean isWhitespaceButNotNewline(char c) { + return Character.isWhitespace(c) && !isLineBreak(c); + } + + private static boolean isLineBreak(char c) { + return c == '\n' || c == '\r'; } @Override @@ -719,7 +842,9 @@ public void resetVisibleRegion() { @Override public IRegion getVisibleRegion() { - disableProjection(); + if (fVisibleRegionDuringProjection != null) { + return fVisibleRegionDuringProjection; + } IRegion visibleRegion= getModelCoverage(); if (visibleRegion == null) { visibleRegion= new Region(0, 0); @@ -730,7 +855,9 @@ public IRegion getVisibleRegion() { @Override public boolean overlapsWithVisibleRegion(int offset, int length) { - disableProjection(); + if (fVisibleRegionDuringProjection != null) { + return TextUtilities.overlaps(fVisibleRegionDuringProjection, new Region(offset, length)); + } IRegion coverage= getModelCoverage(); if (coverage == null) { return false; @@ -784,10 +911,16 @@ private void executeReplaceVisibleDocument(IDocument visibleDocument) { * * @param offset the offset of the range to hide * @param length the length of the range to hide - * @param fireRedraw true if a redraw request should be issued, false otherwise + * @param fireRedraw true if a redraw request should be issued, false + * otherwise + * @param performOutsideVisibleRegion true if the range should be collapsed if it + * overlaps with anything outside of the visible region, false otherwise * @throws BadLocationException in case the range is invalid */ - private void collapse(int offset, int length, boolean fireRedraw) throws BadLocationException { + private void collapse(int offset, int length, boolean fireRedraw, boolean performOutsideVisibleRegion) throws BadLocationException { + if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions(offset, length)) { + return; + } ProjectionDocument projection= null; IDocument visibleDocument= getVisibleDocument(); @@ -824,11 +957,16 @@ private void collapse(int offset, int length, boolean fireRedraw) throws BadLoca * * @param offset the offset of the range to be expanded * @param length the length of the range to be expanded - * @param fireRedraw true if a redraw request should be issued, - * false otherwise + * @param fireRedraw true if a redraw request should be issued, false + * otherwise + * @param performOutsideVisibleRegion true if the range should be collapsed if it + * overlaps with anything outside of the visible region, false otherwise * @throws BadLocationException in case the range is invalid */ - private void expand(int offset, int length, boolean fireRedraw) throws BadLocationException { + private void expand(int offset, int length, boolean fireRedraw, boolean performOutsideVisibleRegion) throws BadLocationException { + if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions(offset, length)) { + return; + } IDocument slave= getVisibleDocument(); if (slave instanceof ProjectionDocument projection) { // expand @@ -854,6 +992,11 @@ private void expand(int offset, int length, boolean fireRedraw) throws BadLocati } } + private boolean overlapsWithNonVisibleRegions(int offset, int length) { + return fVisibleRegionDuringProjection != null + && (offset < fVisibleRegionDuringProjection.getOffset() || offset + length > fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength()); + } + /** * 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. @@ -1074,7 +1217,7 @@ private void processDeletions(AnnotationModelEvent event, Annotation[] removedAn if (annotation.isCollapsed()) { Position expanded= event.getPositionOfRemovedAnnotation(annotation); if (expanded != null) { - expand(expanded.getOffset(), expanded.getLength(), fireRedraw); + expand(expanded.getOffset(), expanded.getLength(), fireRedraw, false); } } } @@ -1184,11 +1327,11 @@ private void processChanges(Annotation[] annotations, boolean fireRedraw, List