Skip to content

Commit b858769

Browse files
committed
Allow using visible regions with projections eclipse-platform#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 eclipse-platform#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)