diff --git a/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java b/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java
index 400c8e2021a..2561539a867 100644
--- a/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java
+++ b/bundles/org.eclipse.jface.text/projection/org/eclipse/jface/text/source/projection/ProjectionViewer.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2000, 2018 IBM Corporation and others.
+ * Copyright (c) 2000, 2025 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -272,6 +272,34 @@ private void computeExpectedExecutionCosts() {
}
}
+ /**
+ * An {@link IDocumentListener} that makes sure that {@link #fVisibleRegionDuringProjection} is
+ * updated when the document changes and ensures that the collapsed region after the visible
+ * region is recreated appropriately.
+ */
+ private final class UpdateDocumentListener implements IDocumentListener {
+ @Override
+ public void documentChanged(DocumentEvent event) {
+ if (fVisibleRegionDuringProjection != null) {
+ int oldLength= event.getLength();
+ int newLength= event.getText().length();
+ int oldVisibleRegionEnd= fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength();
+
+ if (event.getOffset() < fVisibleRegionDuringProjection.getOffset()) {
+ fVisibleRegionDuringProjection= new Region(fVisibleRegionDuringProjection.getOffset() + newLength - oldLength, fVisibleRegionDuringProjection.getLength());
+ } else {
+ if (event.getOffset() + oldLength < oldVisibleRegionEnd) {
+ fVisibleRegionDuringProjection= new Region(fVisibleRegionDuringProjection.getOffset(), fVisibleRegionDuringProjection.getLength() + newLength - oldLength);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void documentAboutToBeChanged(DocumentEvent event) {
+ }
+ }
+
/** The projection annotation model used by this viewer. */
private ProjectionAnnotationModel fProjectionAnnotationModel;
/** The annotation model listener */
@@ -292,6 +320,11 @@ private void computeExpectedExecutionCosts() {
private IDocument fReplaceVisibleDocumentExecutionTrigger;
/** true
if projection was on the last time we switched to segmented mode. */
private boolean fWasProjectionEnabled;
+ /**
+ * The region set by {@link #setVisibleRegion(int, int)} during projection or null
+ * if not in a projection
+ */
+ private IRegion fVisibleRegionDuringProjection;
/** The queue of projection commands used to assess the costs of projection changes. */
private ProjectionCommandQueue fCommandQueue;
/**
@@ -301,6 +334,8 @@ private void computeExpectedExecutionCosts() {
*/
private int fDeletedLines;
+ private UpdateDocumentListener fUpdateDocumentListener= new UpdateDocumentListener();
+
/**
* Creates a new projection source viewer.
@@ -510,6 +545,11 @@ public final void disableProjection() {
fProjectionAnnotationModel.removeAllAnnotations();
fFindReplaceDocumentAdapter= null;
fireProjectionDisabled();
+ if (fVisibleRegionDuringProjection != null) {
+ super.setVisibleRegion(fVisibleRegionDuringProjection.getOffset(), fVisibleRegionDuringProjection.getLength());
+ fVisibleRegionDuringProjection= null;
+ }
+ getDocument().removeDocumentListener(fUpdateDocumentListener);
}
}
@@ -518,9 +558,14 @@ public final void disableProjection() {
*/
public final void enableProjection() {
if (!isProjectionMode()) {
+ IRegion visibleRegion= getVisibleRegion();
addProjectionAnnotationModel(getVisualAnnotationModel());
fFindReplaceDocumentAdapter= null;
fireProjectionEnabled();
+ if (visibleRegion != null && (visibleRegion.getOffset() != 0 || visibleRegion.getLength() != 0) && visibleRegion.getLength() < getDocument().getLength()) {
+ setVisibleRegion(visibleRegion.getOffset(), visibleRegion.getLength());
+ }
+ getDocument().addDocumentListener(fUpdateDocumentListener);
}
}
@@ -529,6 +574,10 @@ private void expandAll() {
IDocument doc= getDocument();
int length= doc == null ? 0 : doc.getLength();
if (isProjectionMode()) {
+ if (fVisibleRegionDuringProjection != null) {
+ offset= fVisibleRegionDuringProjection.getOffset();
+ length= fVisibleRegionDuringProjection.getLength();
+ }
fProjectionAnnotationModel.expandAll(offset, length);
}
}
@@ -683,9 +732,48 @@ private int toLineStart(IDocument document, int offset, boolean testLastLine) th
@Override
public void setVisibleRegion(int start, int length) {
- fWasProjectionEnabled= isProjectionMode();
- disableProjection();
- super.setVisibleRegion(start, length);
+ if (isProjectionMode()) {
+ try {
+ int documentLength= getDocument().getLength();
+ if (fVisibleRegionDuringProjection != null) {
+ expand(0, fVisibleRegionDuringProjection.getOffset(), false);
+ int oldEnd= fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength();
+ expand(oldEnd, documentLength - oldEnd, false);
+ }
+ collapse(0, start, true);
+
+ int end= start + length + 1;
+ // ensure that trailing whitespace is included
+ // In this case, the line break needs to be included as well
+ boolean movedDueToTrailingWhitespace= false;
+ while (end < documentLength && isWhitespaceButNotNewline(getDocument().getChar(end))) {
+ end++;
+ movedDueToTrailingWhitespace= true;
+ }
+ if (movedDueToTrailingWhitespace && end < documentLength && isLineBreak(getDocument().getChar(end))) {
+ end++;
+ }
+
+ int endInvisibleRegionLength= documentLength - end;
+ if (endInvisibleRegionLength > 0) {
+ collapse(end, endInvisibleRegionLength, true);
+ }
+ fVisibleRegionDuringProjection= new Region(start, end - start);
+ } catch (BadLocationException e) {
+ e.printStackTrace();
+ }
+ fVisibleRegionDuringProjection= new Region(start, length);
+ } else {
+ super.setVisibleRegion(start, length);
+ }
+ }
+
+ private boolean isWhitespaceButNotNewline(char c) {
+ return Character.isWhitespace(c) && !isLineBreak(c);
+ }
+
+ private boolean isLineBreak(char c) {
+ return c == '\n' || c == '\r';
}
@Override
@@ -710,6 +798,9 @@ public void resetVisibleRegion() {
@Override
public IRegion getVisibleRegion() {
+ if (fVisibleRegionDuringProjection != null) {
+ return fVisibleRegionDuringProjection;
+ }
disableProjection();
IRegion visibleRegion= getModelCoverage();
if (visibleRegion == null)
diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java
index 03c3773b542..11e82c5990b 100644
--- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java
+++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/ProjectionViewerTest.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2022 Red Hat, Inc. and others.
+ * Copyright (c) 2022, 2025 Red Hat, Inc. and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -18,6 +18,7 @@
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.jface.text.BadLocationException;
@@ -29,12 +30,28 @@
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.source.AnnotationModel;
+import org.eclipse.jface.text.source.IOverviewRuler;
+import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.projection.IProjectionPosition;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
public class ProjectionViewerTest {
+ /**
+ * A {@link ProjectionViewer} that provides access to {@link #getVisibleDocument()}.
+ */
+ private final class TestProjectionViewer extends ProjectionViewer {
+ private TestProjectionViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, boolean showsAnnotationOverview, int styles) {
+ super(parent, ruler, overviewRuler, showsAnnotationOverview, styles);
+ }
+
+ @Override
+ public IDocument getVisibleDocument() {
+ return super.getVisibleDocument();
+ }
+ }
+
private static final class ProjectionPosition extends Position implements IProjectionPosition {
public ProjectionPosition(IDocument document) {
@@ -75,4 +92,254 @@ public void testCopyPaste() {
shell.dispose();
}
}
+
+ @Test
+ public void testVisibleRegionDoesNotChangeWithProjections() {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ ProjectionViewer viewer= new ProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int regionLength= documentContent.indexOf('\n');
+ viewer.setVisibleRegion(0, regionLength);
+ viewer.enableProjection();
+ viewer.getProjectionAnnotationModel().addAnnotation(new ProjectionAnnotation(false), new ProjectionPosition(document));
+ shell.setVisible(true);
+ try {
+ assertEquals(0, viewer.getVisibleRegion().getOffset());
+ assertEquals(regionLength, viewer.getVisibleRegion().getLength());
+
+ viewer.getTextOperationTarget().doOperation(ProjectionViewer.COLLAPSE_ALL);
+ assertEquals(0, viewer.getVisibleRegion().getOffset());
+ assertEquals(regionLength, viewer.getVisibleRegion().getLength());
+ } finally {
+ shell.dispose();
+ }
+ }
+
+ @Test
+ public void testVisibleRegionProjectionCannotBeExpanded() {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int secondLineStart= documentContent.indexOf("World");
+ int secondLineEnd= documentContent.indexOf('\n', secondLineStart);
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ viewer.enableProjection();
+ shell.setVisible(true);
+ try {
+ assertEquals("World", viewer.getVisibleDocument().get());
+ viewer.getTextOperationTarget().doOperation(ProjectionViewer.EXPAND_ALL);
+ assertEquals("World", viewer.getVisibleDocument().get());
+ } finally {
+ shell.dispose();
+ }
+ }
+
+ @Test
+ public void testVisibleRegionAddsProjectionAnnotationsIfProjectionsEnabled() {
+ testProjectionAnnotationsFromVisibleRegion(true);
+ }
+
+ @Test
+ public void testEnableProjectionAddsProjectionAnnotationsIfVisibleRegionEnabled() {
+ testProjectionAnnotationsFromVisibleRegion(false);
+ }
+
+ private void testProjectionAnnotationsFromVisibleRegion(boolean enableProjectionFirst) {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int secondLineStart= documentContent.indexOf("World");
+ int secondLineEnd= documentContent.indexOf('\n', secondLineStart);
+
+ shell.setVisible(true);
+ if (enableProjectionFirst) {
+ viewer.enableProjection();
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ } else {
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ viewer.enableProjection();
+ }
+
+ try {
+ assertEquals("World", viewer.getVisibleDocument().get().trim());
+ } finally {
+ shell.dispose();
+ }
+ }
+
+ @Test
+ public void testInsertIntoVisibleRegion() throws BadLocationException {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int secondLineStart= documentContent.indexOf("World");
+ int secondLineEnd= documentContent.indexOf('\n', secondLineStart);
+
+ shell.setVisible(true);
+
+ try {
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ viewer.enableProjection();
+
+ assertEquals("World", viewer.getVisibleDocument().get());
+
+ viewer.getDocument().replace(documentContent.indexOf("rld"), 0, "---");
+
+ assertEquals("Wo---rld", viewer.getVisibleDocument().get());
+ } finally {
+ shell.dispose();
+ }
+ }
+
+ @Test
+ public void testRemoveVisibleRegionEnd() throws BadLocationException {
+ testReplaceVisibleRegionEnd("");
+ }
+
+ @Test
+ public void testReplaceVisibleRegionEnd() throws BadLocationException {
+ testReplaceVisibleRegionEnd("---");
+ }
+
+
+ private void testReplaceVisibleRegionEnd(String toReplaceWith) throws BadLocationException {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int secondLineStart= documentContent.indexOf("World");
+ int secondLineEnd= documentContent.indexOf('\n', secondLineStart);
+
+ shell.setVisible(true);
+
+ try {
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ viewer.enableProjection();
+
+ assertEquals("World", viewer.getVisibleDocument().get());
+
+ viewer.getDocument().replace(documentContent.indexOf("d\n1"), 3, toReplaceWith);
+
+ assertEquals("Worl" + toReplaceWith, viewer.getVisibleDocument().get());
+ } finally {
+ shell.dispose();
+ }
+ }
+
+ @Test
+ public void testRemoveVisibleRegionStart() throws BadLocationException {
+ testReplaceVisibleRegionStart("");
+ }
+
+ @Test
+ public void testReplaceVisibleRegionStart() throws BadLocationException {
+ testReplaceVisibleRegionStart("---");
+ }
+
+
+ private void testReplaceVisibleRegionStart(String toReplaceWith) throws BadLocationException {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int secondLineStart= documentContent.indexOf("World");
+ int secondLineEnd= documentContent.indexOf('\n', secondLineStart);
+
+ shell.setVisible(true);
+
+ try {
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ viewer.enableProjection();
+
+ assertEquals("World", viewer.getVisibleDocument().get());
+
+ viewer.getDocument().replace(documentContent.indexOf("o\nW"), 3, toReplaceWith);
+
+ assertEquals(toReplaceWith + "orld", viewer.getVisibleDocument().get());
+ } finally {
+ shell.dispose();
+ }
+ }
+
+ @Test
+ public void testVisibleRegionEndsWithWhitespace() {
+ Shell shell= new Shell();
+ shell.setLayout(new FillLayout());
+ TestProjectionViewer viewer= new TestProjectionViewer(shell, null, null, false, SWT.NONE);
+ String documentContent= """
+ Hello
+ World\t\t
+ 123
+ 456
+ """;
+ Document document= new Document(documentContent);
+ viewer.setDocument(document, new AnnotationModel());
+ int secondLineStart= documentContent.indexOf("World");
+ int secondLineTextEnd= documentContent.indexOf('\n', secondLineStart);
+ int secondLineEnd= documentContent.indexOf('\n', secondLineStart);
+
+ shell.setVisible(true);
+
+ try {
+ viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart);
+ viewer.enableProjection();
+
+ assertEquals("World\t\t", viewer.getVisibleDocument().get());
+
+ viewer.setVisibleRegion(secondLineStart, secondLineTextEnd - secondLineStart);
+
+ assertEquals("World\t\t\n", viewer.getVisibleDocument().get());
+
+
+ } finally {
+ shell.dispose();
+ }
+ }
+
}