Skip to content

Commit 1848058

Browse files
danthe1stmickaelistria
authored andcommitted
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 b9ab7dc commit 1848058

File tree

2 files changed

+451
-15
lines changed

2 files changed

+451
-15
lines changed

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

Lines changed: 157 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

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

Comments
 (0)