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
@@ -277,6 +280,32 @@ private void computeExpectedExecutionCosts() {
277280 }
278281 }
279282
283+ /**
284+ * An {@link IDocumentListener} that makes sure that {@link #fVisibleRegionDuringProjection} is
285+ * updated when the document changes and ensures that the collapsed region after the visible
286+ * region is recreated appropriately.
287+ */
288+ private final class UpdateDocumentListener implements IDocumentListener {
289+ @ Override
290+ public void documentChanged (DocumentEvent event ) {
291+ if (fVisibleRegionDuringProjection == null ) {
292+ return ;
293+ }
294+ int oldLength = event .getLength ();
295+ int newLength = event .getText ().length ();
296+ int oldVisibleRegionEnd = fVisibleRegionDuringProjection .getOffset () + fVisibleRegionDuringProjection .getLength ();
297+ if (event .getOffset () < fVisibleRegionDuringProjection .getOffset ()) {
298+ fVisibleRegionDuringProjection = new Region (fVisibleRegionDuringProjection .getOffset () + newLength - oldLength , fVisibleRegionDuringProjection .getLength ());
299+ } else if (event .getOffset () + oldLength <= oldVisibleRegionEnd ) {
300+ fVisibleRegionDuringProjection = new Region (fVisibleRegionDuringProjection .getOffset (), fVisibleRegionDuringProjection .getLength () + newLength - oldLength );
301+ }
302+ }
303+
304+ @ Override
305+ public void documentAboutToBeChanged (DocumentEvent event ) {
306+ }
307+ }
308+
280309 /** The projection annotation model used by this viewer. */
281310 private ProjectionAnnotationModel fProjectionAnnotationModel ;
282311 /** The annotation model listener */
@@ -297,6 +326,11 @@ private void computeExpectedExecutionCosts() {
297326 private IDocument fReplaceVisibleDocumentExecutionTrigger ;
298327 /** <code>true</code> if projection was on the last time we switched to segmented mode. */
299328 private boolean fWasProjectionEnabled ;
329+ /**
330+ * The region set by {@link #setVisibleRegion(int, int)} during projection or <code>null</code>
331+ * if not in a projection
332+ */
333+ private IRegion fVisibleRegionDuringProjection ;
300334 /** The queue of projection commands used to assess the costs of projection changes. */
301335 private ProjectionCommandQueue fCommandQueue ;
302336 /**
@@ -306,6 +340,7 @@ private void computeExpectedExecutionCosts() {
306340 */
307341 private int fDeletedLines ;
308342
343+ private UpdateDocumentListener fUpdateDocumentListener ;
309344
310345 /**
311346 * Creates a new projection source viewer.
@@ -318,6 +353,7 @@ private void computeExpectedExecutionCosts() {
318353 */
319354 public ProjectionViewer (Composite parent , IVerticalRuler ruler , IOverviewRuler overviewRuler , boolean showsAnnotationOverview , int styles ) {
320355 super (parent , ruler , overviewRuler , showsAnnotationOverview , styles );
356+ fUpdateDocumentListener = new UpdateDocumentListener ();
321357 }
322358
323359 /**
@@ -514,6 +550,14 @@ public final void disableProjection() {
514550 fProjectionAnnotationModel .removeAllAnnotations ();
515551 fFindReplaceDocumentAdapter = null ;
516552 fireProjectionDisabled ();
553+ if (fVisibleRegionDuringProjection != null ) {
554+ super .setVisibleRegion (fVisibleRegionDuringProjection .getOffset (), fVisibleRegionDuringProjection .getLength ());
555+ fVisibleRegionDuringProjection = null ;
556+ }
557+ IDocument document = getDocument ();
558+ if (document != null ) {
559+ document .removeDocumentListener (fUpdateDocumentListener );
560+ }
517561 }
518562 }
519563
@@ -525,6 +569,15 @@ public final void enableProjection() {
525569 addProjectionAnnotationModel (getVisualAnnotationModel ());
526570 fFindReplaceDocumentAdapter = null ;
527571 fireProjectionEnabled ();
572+ IDocument document = getDocument ();
573+ if (document == null ) {
574+ return ;
575+ }
576+ IRegion visibleRegion = getVisibleRegion ();
577+ if (visibleRegion != null && (visibleRegion .getOffset () != 0 || visibleRegion .getLength () != 0 ) && visibleRegion .getLength () < document .getLength ()) {
578+ setVisibleRegion (visibleRegion .getOffset (), visibleRegion .getLength ());
579+ }
580+ document .addDocumentListener (fUpdateDocumentListener );
528581 }
529582 }
530583
@@ -533,6 +586,10 @@ private void expandAll() {
533586 IDocument doc = getDocument ();
534587 int length = doc == null ? 0 : doc .getLength ();
535588 if (isProjectionMode ()) {
589+ if (fVisibleRegionDuringProjection != null ) {
590+ offset = fVisibleRegionDuringProjection .getOffset ();
591+ length = fVisibleRegionDuringProjection .getLength ();
592+ }
536593 fProjectionAnnotationModel .expandAll (offset , length );
537594 }
538595 }
@@ -691,9 +748,75 @@ private int toLineStart(IDocument document, int offset, boolean testLastLine) th
691748
692749 @ Override
693750 public void setVisibleRegion (int start , int length ) {
694- fWasProjectionEnabled = isProjectionMode ();
695- disableProjection ();
696- super .setVisibleRegion (start , length );
751+ if (!isProjectionMode ()) {
752+ super .setVisibleRegion (start , length );
753+ return ;
754+ }
755+ IDocument document = getDocument ();
756+ if (document == null ) {
757+ return ;
758+ }
759+ try {
760+ // If the visible region changes, make sure collapsed regions outside of the old visible regions are expanded
761+ // and collapse everything outside the new visible region
762+ int end = computeEndOfVisibleRegion (start , length , document );
763+ expandOutsideCurrentVisibleRegion (document );
764+ collapseOutsideOfNewVisibleRegion (start , end , document );
765+ fVisibleRegionDuringProjection = new Region (start , end - start - 1 );
766+ } catch (BadLocationException e ) {
767+ ILog log = ILog .of (getClass ());
768+ log .log (new Status (IStatus .WARNING , getClass (), IStatus .OK , null , e ));
769+ }
770+ }
771+
772+ private void expandOutsideCurrentVisibleRegion (IDocument document ) throws BadLocationException {
773+ if (fVisibleRegionDuringProjection != null ) {
774+ expand (0 , fVisibleRegionDuringProjection .getOffset (), false , true );
775+ int oldEnd = fVisibleRegionDuringProjection .getOffset () + fVisibleRegionDuringProjection .getLength ();
776+ int length = document .getLength () - oldEnd ;
777+ if (length > 0 ) {
778+ expand (oldEnd , length , false , true );
779+ }
780+ }
781+ }
782+
783+ private void collapseOutsideOfNewVisibleRegion (int start , int end , IDocument document ) throws BadLocationException {
784+ int documentLength = document .getLength ();
785+ collapse (0 , start , true , true );
786+
787+ int endInvisibleRegionLength = documentLength - end ;
788+
789+ if (isLineBreak (document .getChar (documentLength - 1 ))) {
790+ // 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)
791+ endInvisibleRegionLength ++;
792+ }
793+ if (endInvisibleRegionLength > 0 ) {
794+ collapse (end , endInvisibleRegionLength , true , true );
795+ }
796+ }
797+
798+ private static int computeEndOfVisibleRegion (int start , int length , IDocument document ) throws BadLocationException {
799+ int documentLength = document .getLength ();
800+ int end = start + length + 1 ;
801+ // ensure that trailing whitespace is included
802+ // In this case, the line break needs to be included as well
803+ boolean visibleRegionEndsWithTrailingWhitespace = end < documentLength && isWhitespaceButNotNewline (document .getChar (end - 1 ));
804+ while (end < documentLength && isWhitespaceButNotNewline (document .getChar (end ))) {
805+ end ++;
806+ visibleRegionEndsWithTrailingWhitespace = true ;
807+ }
808+ if (visibleRegionEndsWithTrailingWhitespace && end < documentLength && isLineBreak (document .getChar (end ))) {
809+ end ++;
810+ }
811+ return end ;
812+ }
813+
814+ private static boolean isWhitespaceButNotNewline (char c ) {
815+ return Character .isWhitespace (c ) && !isLineBreak (c );
816+ }
817+
818+ private static boolean isLineBreak (char c ) {
819+ return c == '\n' || c == '\r' ;
697820 }
698821
699822 @ Override
@@ -719,7 +842,9 @@ public void resetVisibleRegion() {
719842
720843 @ Override
721844 public IRegion getVisibleRegion () {
722- disableProjection ();
845+ if (fVisibleRegionDuringProjection != null ) {
846+ return fVisibleRegionDuringProjection ;
847+ }
723848 IRegion visibleRegion = getModelCoverage ();
724849 if (visibleRegion == null ) {
725850 visibleRegion = new Region (0 , 0 );
@@ -730,7 +855,9 @@ public IRegion getVisibleRegion() {
730855
731856 @ Override
732857 public boolean overlapsWithVisibleRegion (int offset , int length ) {
733- disableProjection ();
858+ if (fVisibleRegionDuringProjection != null ) {
859+ return TextUtilities .overlaps (fVisibleRegionDuringProjection , new Region (offset , length ));
860+ }
734861 IRegion coverage = getModelCoverage ();
735862 if (coverage == null ) {
736863 return false ;
@@ -784,10 +911,16 @@ private void executeReplaceVisibleDocument(IDocument visibleDocument) {
784911 *
785912 * @param offset the offset of the range to hide
786913 * @param length the length of the range to hide
787- * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code> otherwise
914+ * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code>
915+ * otherwise
916+ * @param performOutsideVisibleRegion <code>true</code> if the range should be collapsed if it
917+ * overlaps with anything outside of the visible region, <code>false</code> otherwise
788918 * @throws BadLocationException in case the range is invalid
789919 */
790- private void collapse (int offset , int length , boolean fireRedraw ) throws BadLocationException {
920+ private void collapse (int offset , int length , boolean fireRedraw , boolean performOutsideVisibleRegion ) throws BadLocationException {
921+ if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions (offset , length )) {
922+ return ;
923+ }
791924 ProjectionDocument projection = null ;
792925
793926 IDocument visibleDocument = getVisibleDocument ();
@@ -824,11 +957,16 @@ private void collapse(int offset, int length, boolean fireRedraw) throws BadLoca
824957 *
825958 * @param offset the offset of the range to be expanded
826959 * @param length the length of the range to be expanded
827- * @param fireRedraw <code>true</code> if a redraw request should be issued,
828- * <code>false</code> otherwise
960+ * @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code>
961+ * otherwise
962+ * @param performOutsideVisibleRegion <code>true</code> if the range should be collapsed if it
963+ * overlaps with anything outside of the visible region, <code>false</code> otherwise
829964 * @throws BadLocationException in case the range is invalid
830965 */
831- private void expand (int offset , int length , boolean fireRedraw ) throws BadLocationException {
966+ private void expand (int offset , int length , boolean fireRedraw , boolean performOutsideVisibleRegion ) throws BadLocationException {
967+ if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions (offset , length )) {
968+ return ;
969+ }
832970 IDocument slave = getVisibleDocument ();
833971 if (slave instanceof ProjectionDocument projection ) {
834972 // expand
@@ -854,6 +992,11 @@ private void expand(int offset, int length, boolean fireRedraw) throws BadLocati
854992 }
855993 }
856994
995+ private boolean overlapsWithNonVisibleRegions (int offset , int length ) {
996+ return fVisibleRegionDuringProjection != null
997+ && (offset < fVisibleRegionDuringProjection .getOffset () || offset + length > fVisibleRegionDuringProjection .getOffset () + fVisibleRegionDuringProjection .getLength ());
998+ }
999+
8571000 /**
8581001 * Processes the request for catch up with the annotation model in the UI thread. If the current
8591002 * 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
10741217 if (annotation .isCollapsed ()) {
10751218 Position expanded = event .getPositionOfRemovedAnnotation (annotation );
10761219 if (expanded != null ) {
1077- expand (expanded .getOffset (), expanded .getLength (), fireRedraw );
1220+ expand (expanded .getOffset (), expanded .getLength (), fireRedraw , false );
10781221 }
10791222 }
10801223 }
@@ -1184,11 +1327,11 @@ private void processChanges(Annotation[] annotations, boolean fireRedraw, List<P
11841327 IRegion [] regions = computeCollapsedRegions (position );
11851328 if (regions != null ) {
11861329 for (IRegion region : regions ) {
1187- collapse (region .getOffset (), region .getLength (), fireRedraw );
1330+ collapse (region .getOffset (), region .getLength (), fireRedraw , false );
11881331 }
11891332 }
11901333 } else {
1191- expand (position .getOffset (), position .getLength (), fireRedraw );
1334+ expand (position .getOffset (), position .getLength (), fireRedraw , false );
11921335 }
11931336 }
11941337 }
0 commit comments