Skip to content

Commit 027ef5f

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 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)