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(); + } + } + }