Skip to content

Commit 027ef5f

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 027ef5f

File tree

2 files changed

+454
-15
lines changed

2 files changed

+454
-15
lines changed

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

Lines changed: 159 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,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

Comments
 (0)