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