Skip to content

Commit b858769

Browse files
committed
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 #3074
1 parent 88774cd commit b858769

File tree

2 files changed

+449
-15
lines changed

2 files changed

+449
-15
lines changed

bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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
@@ -33,6 +33,9 @@
3333
import org.eclipse.swt.widgets.Display;
3434

3535
import 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

3740
import 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,70 @@ 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+
if (endInvisibleRegionLength > 0) {
782+
collapse(end, endInvisibleRegionLength, true, true);
783+
}
784+
}
785+
786+
private static int computeEndOfVisibleRegion(int start, int length, IDocument document) throws BadLocationException {
787+
int documentLength= document.getLength();
788+
int end= start + length + 1;
789+
// ensure that trailing whitespace is included
790+
// In this case, the line break needs to be included as well
791+
boolean visibleRegionEndsWithTrailingWhitespace= end < documentLength && isWhitespaceButNotNewline(document.getChar(end - 1));
792+
while (end < documentLength && isWhitespaceButNotNewline(document.getChar(end))) {
793+
end++;
794+
visibleRegionEndsWithTrailingWhitespace= true;
795+
}
796+
if (visibleRegionEndsWithTrailingWhitespace && end < documentLength && isLineBreak(document.getChar(end))) {
797+
end++;
798+
}
799+
return end;
800+
}
801+
802+
private static boolean isWhitespaceButNotNewline(char c) {
803+
return Character.isWhitespace(c) && !isLineBreak(c);
804+
}
805+
806+
private static boolean isLineBreak(char c) {
807+
return c == '\n' || c == '\r';
689808
}
690809

691810
@Override
@@ -710,7 +829,9 @@ public void resetVisibleRegion() {
710829

711830
@Override
712831
public IRegion getVisibleRegion() {
713-
disableProjection();
832+
if (fVisibleRegionDuringProjection != null) {
833+
return fVisibleRegionDuringProjection;
834+
}
714835
IRegion visibleRegion= getModelCoverage();
715836
if (visibleRegion == null)
716837
visibleRegion= new Region(0, 0);
@@ -720,7 +841,9 @@ public IRegion getVisibleRegion() {
720841

721842
@Override
722843
public boolean overlapsWithVisibleRegion(int offset, int length) {
723-
disableProjection();
844+
if (fVisibleRegionDuringProjection != null) {
845+
return TextUtilities.overlaps(fVisibleRegionDuringProjection, new Region(offset, length));
846+
}
724847
IRegion coverage= getModelCoverage();
725848
if (coverage == null)
726849
return false;
@@ -769,10 +892,16 @@ private void executeReplaceVisibleDocument(IDocument visibleDocument) {
769892
*
770893
* @param offset the offset of the range to hide
771894
* @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
895+
* @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code>
896+
* otherwise
897+
* @param performOutsideVisibleRegion <code>true</code> if the range should be collapsed if it
898+
* overlaps with anything outside of the visible region, <code>false</code> otherwise
773899
* @throws BadLocationException in case the range is invalid
774900
*/
775-
private void collapse(int offset, int length, boolean fireRedraw) throws BadLocationException {
901+
private void collapse(int offset, int length, boolean fireRedraw, boolean performOutsideVisibleRegion) throws BadLocationException {
902+
if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions(offset, length)) {
903+
return;
904+
}
776905
ProjectionDocument projection= null;
777906

778907
IDocument visibleDocument= getVisibleDocument();
@@ -802,17 +931,23 @@ private void collapse(int offset, int length, boolean fireRedraw) throws BadLoca
802931
}
803932
}
804933

934+
805935
/**
806936
* Makes the given range visible again while not changing the folding state of any contained
807937
* ranges. If requested, a redraw request is issued.
808938
*
809939
* @param offset the offset of the range to be expanded
810940
* @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
941+
* @param fireRedraw <code>true</code> if a redraw request should be issued, <code>false</code>
942+
* otherwise
943+
* @param performOutsideVisibleRegion <code>true</code> if the range should be collapsed if it
944+
* overlaps with anything outside of the visible region, <code>false</code> otherwise
813945
* @throws BadLocationException in case the range is invalid
814946
*/
815-
private void expand(int offset, int length, boolean fireRedraw) throws BadLocationException {
947+
private void expand(int offset, int length, boolean fireRedraw, boolean performOutsideVisibleRegion) throws BadLocationException {
948+
if (!performOutsideVisibleRegion && overlapsWithNonVisibleRegions(offset, length)) {
949+
return;
950+
}
816951
IDocument slave= getVisibleDocument();
817952
if (slave instanceof ProjectionDocument) {
818953
ProjectionDocument projection= (ProjectionDocument) slave;
@@ -839,6 +974,11 @@ private void expand(int offset, int length, boolean fireRedraw) throws BadLocati
839974
}
840975
}
841976

977+
private boolean overlapsWithNonVisibleRegions(int offset, int length) {
978+
return fVisibleRegionDuringProjection != null
979+
&& (offset < fVisibleRegionDuringProjection.getOffset() || offset + length > fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength());
980+
}
981+
842982
/**
843983
* Processes the request for catch up with the annotation model in the UI thread. If the current
844984
* thread is not the UI thread or there are pending catch up requests, a new request is posted.
@@ -1054,7 +1194,7 @@ private void processDeletions(AnnotationModelEvent event, Annotation[] removedAn
10541194
if (annotation.isCollapsed()) {
10551195
Position expanded= event.getPositionOfRemovedAnnotation(annotation);
10561196
if (expanded != null) {
1057-
expand(expanded.getOffset(), expanded.getLength(), fireRedraw);
1197+
expand(expanded.getOffset(), expanded.getLength(), fireRedraw, false);
10581198
}
10591199
}
10601200
}
@@ -1158,11 +1298,11 @@ private void processChanges(Annotation[] annotations, boolean fireRedraw, List<P
11581298
IRegion[] regions= computeCollapsedRegions(position);
11591299
if (regions != null) {
11601300
for (IRegion region : regions) {
1161-
collapse(region.getOffset(), region.getLength(), fireRedraw);
1301+
collapse(region.getOffset(), region.getLength(), fireRedraw, false);
11621302
}
11631303
}
11641304
} else {
1165-
expand(position.getOffset(), position.getLength(), fireRedraw);
1305+
expand(position.getOffset(), position.getLength(), fireRedraw, false);
11661306
}
11671307
}
11681308
}

0 commit comments

Comments
 (0)