11/*******************************************************************************
2- * Copyright (c) 2000, 2018 IBM Corporation and others.
2+ * Copyright (c) 2000, 2025 IBM Corporation and others.
33 *
44 * This program and the accompanying materials
55 * are made available under the terms of the Eclipse Public License 2.0
3333import org .eclipse .swt .widgets .Display ;
3434
3535import org .eclipse .core .runtime .Assert ;
36+ import org .eclipse .core .runtime .ILog ;
37+ import org .eclipse .core .runtime .IStatus ;
38+ import org .eclipse .core .runtime .Status ;
3639
3740import org .eclipse .jface .internal .text .SelectionProcessor ;
3841
@@ -272,6 +275,32 @@ private void computeExpectedExecutionCosts() {
272275 }
273276 }
274277
278+ /**
279+ * An {@link IDocumentListener} that makes sure that {@link #fVisibleRegionDuringProjection} is
280+ * updated when the document changes and ensures that the collapsed region after the visible
281+ * region is recreated appropriately.
282+ */
283+ private final class UpdateDocumentListener implements IDocumentListener {
284+ @ Override
285+ public void documentChanged (DocumentEvent event ) {
286+ if (fVisibleRegionDuringProjection == null ) {
287+ return ;
288+ }
289+ int oldLength = event .getLength ();
290+ int newLength = event .getText ().length ();
291+ int oldVisibleRegionEnd = fVisibleRegionDuringProjection .getOffset () + fVisibleRegionDuringProjection .getLength ();
292+ if (event .getOffset () < fVisibleRegionDuringProjection .getOffset ()) {
293+ fVisibleRegionDuringProjection = new Region (fVisibleRegionDuringProjection .getOffset () + newLength - oldLength , fVisibleRegionDuringProjection .getLength ());
294+ } else if (event .getOffset () + oldLength <= oldVisibleRegionEnd ) {
295+ fVisibleRegionDuringProjection = new Region (fVisibleRegionDuringProjection .getOffset (), fVisibleRegionDuringProjection .getLength () + newLength - oldLength );
296+ }
297+ }
298+
299+ @ Override
300+ public void documentAboutToBeChanged (DocumentEvent event ) {
301+ }
302+ }
303+
275304 /** The projection annotation model used by this viewer. */
276305 private ProjectionAnnotationModel fProjectionAnnotationModel ;
277306 /** The annotation model listener */
@@ -292,6 +321,11 @@ private void computeExpectedExecutionCosts() {
292321 private IDocument fReplaceVisibleDocumentExecutionTrigger ;
293322 /** <code>true</code> if projection was on the last time we switched to segmented mode. */
294323 private boolean fWasProjectionEnabled ;
324+ /**
325+ * The region set by {@link #setVisibleRegion(int, int)} during projection or <code>null</code>
326+ * if not in a projection
327+ */
328+ private IRegion fVisibleRegionDuringProjection ;
295329 /** The queue of projection commands used to assess the costs of projection changes. */
296330 private ProjectionCommandQueue fCommandQueue ;
297331 /**
@@ -301,6 +335,8 @@ private void computeExpectedExecutionCosts() {
301335 */
302336 private int fDeletedLines ;
303337
338+ private UpdateDocumentListener fUpdateDocumentListener ;
339+
304340
305341 /**
306342 * Creates a new projection source viewer.
@@ -313,6 +349,7 @@ private void computeExpectedExecutionCosts() {
313349 */
314350 public ProjectionViewer (Composite parent , IVerticalRuler ruler , IOverviewRuler overviewRuler , boolean showsAnnotationOverview , int styles ) {
315351 super (parent , ruler , overviewRuler , showsAnnotationOverview , styles );
352+ fUpdateDocumentListener = new UpdateDocumentListener ();
316353 }
317354
318355 /**
@@ -510,6 +547,14 @@ public final void disableProjection() {
510547 fProjectionAnnotationModel .removeAllAnnotations ();
511548 fFindReplaceDocumentAdapter = null ;
512549 fireProjectionDisabled ();
550+ if (fVisibleRegionDuringProjection != null ) {
551+ super .setVisibleRegion (fVisibleRegionDuringProjection .getOffset (), fVisibleRegionDuringProjection .getLength ());
552+ fVisibleRegionDuringProjection = null ;
553+ }
554+ IDocument document = getDocument ();
555+ if (document != null ) {
556+ document .removeDocumentListener (fUpdateDocumentListener );
557+ }
513558 }
514559 }
515560
@@ -521,6 +566,15 @@ public final void enableProjection() {
521566 addProjectionAnnotationModel (getVisualAnnotationModel ());
522567 fFindReplaceDocumentAdapter = null ;
523568 fireProjectionEnabled ();
569+ IDocument document = getDocument ();
570+ if (document == null ) {
571+ return ;
572+ }
573+ IRegion visibleRegion = getVisibleRegion ();
574+ if (visibleRegion != null && (visibleRegion .getOffset () != 0 || visibleRegion .getLength () != 0 ) && visibleRegion .getLength () < document .getLength ()) {
575+ setVisibleRegion (visibleRegion .getOffset (), visibleRegion .getLength ());
576+ }
577+ document .addDocumentListener (fUpdateDocumentListener );
524578 }
525579 }
526580
@@ -529,6 +583,10 @@ private void expandAll() {
529583 IDocument doc = getDocument ();
530584 int length = doc == null ? 0 : doc .getLength ();
531585 if (isProjectionMode ()) {
586+ if (fVisibleRegionDuringProjection != null ) {
587+ offset = fVisibleRegionDuringProjection .getOffset ();
588+ length = fVisibleRegionDuringProjection .getLength ();
589+ }
532590 fProjectionAnnotationModel .expandAll (offset , length );
533591 }
534592 }
@@ -683,9 +741,75 @@ private int toLineStart(IDocument document, int offset, boolean testLastLine) th
683741
684742 @ Override
685743 public void setVisibleRegion (int start , int length ) {
686- fWasProjectionEnabled = isProjectionMode ();
687- disableProjection ();
688- super .setVisibleRegion (start , length );
744+ if (!isProjectionMode ()) {
745+ super .setVisibleRegion (start , length );
746+ return ;
747+ }
748+ IDocument document = getDocument ();
749+ if (document == null ) {
750+ return ;
751+ }
752+ try {
753+ // If the visible region changes, make sure collapsed regions outside of the old visible regions are expanded
754+ // and collapse everything outside the new visible region
755+ int end = computeEndOfVisibleRegion (start , length , document );
756+ expandOutsideCurrentVisibleRegion (document );
757+ collapseOutsideOfNewVisibleRegion (start , end , document );
758+ fVisibleRegionDuringProjection = new Region (start , end - start - 1 );
759+ } catch (BadLocationException e ) {
760+ ILog log = ILog .of (getClass ());
761+ log .log (new Status (IStatus .WARNING , getClass (), IStatus .OK , null , e ));
762+ }
763+ }
764+
765+ private void expandOutsideCurrentVisibleRegion (IDocument document ) throws BadLocationException {
766+ if (fVisibleRegionDuringProjection != null ) {
767+ expand (0 , fVisibleRegionDuringProjection .getOffset (), false , true );
768+ int oldEnd = fVisibleRegionDuringProjection .getOffset () + fVisibleRegionDuringProjection .getLength ();
769+ int length = document .getLength () - oldEnd ;
770+ if (length > 0 ) {
771+ expand (oldEnd , length , false , true );
772+ }
773+ }
774+ }
775+
776+ private void collapseOutsideOfNewVisibleRegion (int start , int end , IDocument document ) throws BadLocationException {
777+ int documentLength = document .getLength ();
778+ collapse (0 , start , true , true );
779+
780+ int endInvisibleRegionLength = documentLength - end ;
781+
782+ if (isLineBreak (document .getChar (documentLength - 1 ))) {
783+ // 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)
784+ endInvisibleRegionLength ++;
785+ }
786+ if (endInvisibleRegionLength > 0 ) {
787+ collapse (end , endInvisibleRegionLength , true , true );
788+ }
789+ }
790+
791+ private static int computeEndOfVisibleRegion (int start , int length , IDocument document ) throws BadLocationException {
792+ int documentLength = document .getLength ();
793+ int end = start + length + 1 ;
794+ // ensure that trailing whitespace is included
795+ // In this case, the line break needs to be included as well
796+ boolean visibleRegionEndsWithTrailingWhitespace = end < documentLength && isWhitespaceButNotNewline (document .getChar (end - 1 ));
797+ while (end < documentLength && isWhitespaceButNotNewline (document .getChar (end ))) {
798+ end ++;
799+ visibleRegionEndsWithTrailingWhitespace = true ;
800+ }
801+ if (visibleRegionEndsWithTrailingWhitespace && end < documentLength && isLineBreak (document .getChar (end ))) {
802+ end ++;
803+ }
804+ return end ;
805+ }
806+
807+ private static boolean isWhitespaceButNotNewline (char c ) {
808+ return Character .isWhitespace (c ) && !isLineBreak (c );
809+ }
810+
811+ private static boolean isLineBreak (char c ) {
812+ return c == '\n' || c == '\r' ;
689813 }
690814
691815 @ Override
@@ -710,7 +834,9 @@ public void resetVisibleRegion() {
710834
711835 @ Override
712836 public IRegion getVisibleRegion () {
713- disableProjection ();
837+ if (fVisibleRegionDuringProjection != null ) {
838+ return fVisibleRegionDuringProjection ;
839+ }
714840 IRegion visibleRegion = getModelCoverage ();
715841 if (visibleRegion == null )
716842 visibleRegion = new Region (0 , 0 );
@@ -720,7 +846,9 @@ public IRegion getVisibleRegion() {
720846
721847 @ Override
722848 public boolean overlapsWithVisibleRegion (int offset , int length ) {
723- disableProjection ();
849+ if (fVisibleRegionDuringProjection != null ) {
850+ return TextUtilities .overlaps (fVisibleRegionDuringProjection , new Region (offset , length ));
851+ }
724852 IRegion coverage = getModelCoverage ();
725853 if (coverage == null )
726854 return false ;
@@ -769,10 +897,16 @@ private void executeReplaceVisibleDocument(IDocument visibleDocument) {
769897 *
770898 * @param offset the offset of the range to hide
771899 * @param length the length of the range to hide
772- * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code> otherwise
900+ * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code>
901+ * otherwise
902+ * @param performOutsideVisibleRegion <code>true</code> if the range should be collapsed if it
903+ * overlaps with anything outside of the visible region, <code>false</code> otherwise
773904 * @throws BadLocationException in case the range is invalid
774905 */
775- private void collapse (int offset , int length , boolean fireRedraw ) throws BadLocationException {
906+ private void collapse (int offset , int length , boolean fireRedraw , boolean performOutsideVisibleRegion ) throws BadLocationException {
907+ if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions (offset , length )) {
908+ return ;
909+ }
776910 ProjectionDocument projection = null ;
777911
778912 IDocument visibleDocument = getVisibleDocument ();
@@ -802,17 +936,23 @@ private void collapse(int offset, int length, boolean fireRedraw) throws BadLoca
802936 }
803937 }
804938
939+
805940 /**
806941 * Makes the given range visible again while not changing the folding state of any contained
807942 * ranges. If requested, a redraw request is issued.
808943 *
809944 * @param offset the offset of the range to be expanded
810945 * @param length the length of the range to be expanded
811- * @param fireRedraw <code>true</code> if a redraw request should be issued,
812- * <code>false</code> otherwise
946+ * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code>
947+ * otherwise
948+ * @param performOutsideVisibleRegion <code>true</code> if the range should be collapsed if it
949+ * overlaps with anything outside of the visible region, <code>false</code> otherwise
813950 * @throws BadLocationException in case the range is invalid
814951 */
815- private void expand (int offset , int length , boolean fireRedraw ) throws BadLocationException {
952+ private void expand (int offset , int length , boolean fireRedraw , boolean performOutsideVisibleRegion ) throws BadLocationException {
953+ if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions (offset , length )) {
954+ return ;
955+ }
816956 IDocument slave = getVisibleDocument ();
817957 if (slave instanceof ProjectionDocument ) {
818958 ProjectionDocument projection = (ProjectionDocument ) slave ;
@@ -839,6 +979,11 @@ private void expand(int offset, int length, boolean fireRedraw) throws BadLocati
839979 }
840980 }
841981
982+ private boolean overlapsWithNonVisibleRegions (int offset , int length ) {
983+ return fVisibleRegionDuringProjection != null
984+ && (offset < fVisibleRegionDuringProjection .getOffset () || offset + length > fVisibleRegionDuringProjection .getOffset () + fVisibleRegionDuringProjection .getLength ());
985+ }
986+
842987 /**
843988 * Processes the request for catch up with the annotation model in the UI thread. If the current
844989 * thread is not the UI thread or there are pending catch up requests, a new request is posted.
@@ -1054,7 +1199,7 @@ private void processDeletions(AnnotationModelEvent event, Annotation[] removedAn
10541199 if (annotation .isCollapsed ()) {
10551200 Position expanded = event .getPositionOfRemovedAnnotation (annotation );
10561201 if (expanded != null ) {
1057- expand (expanded .getOffset (), expanded .getLength (), fireRedraw );
1202+ expand (expanded .getOffset (), expanded .getLength (), fireRedraw , false );
10581203 }
10591204 }
10601205 }
@@ -1158,11 +1303,11 @@ private void processChanges(Annotation[] annotations, boolean fireRedraw, List<P
11581303 IRegion [] regions = computeCollapsedRegions (position );
11591304 if (regions != null ) {
11601305 for (IRegion region : regions ) {
1161- collapse (region .getOffset (), region .getLength (), fireRedraw );
1306+ collapse (region .getOffset (), region .getLength (), fireRedraw , false );
11621307 }
11631308 }
11641309 } else {
1165- expand (position .getOffset (), position .getLength (), fireRedraw );
1310+ expand (position .getOffset (), position .getLength (), fireRedraw , false );
11661311 }
11671312 }
11681313 }
0 commit comments