From c5c29771c3345ea5e272797e7c0c52c5fc570e26 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 5 Jul 2025 14:39:06 +0200 Subject: [PATCH] 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 https://github.com/eclipse-platform/eclipse.platform.ui/pull/3074 --- .../source/projection/ProjectionViewer.java | 131 +++++++- .../text/tests/ProjectionViewerTest.java | 296 +++++++++++++++++- 2 files changed, 421 insertions(+), 6 deletions(-) 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..60d9911d752 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 @@ -33,6 +33,9 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; import org.eclipse.jface.internal.text.SelectionProcessor; @@ -272,6 +275,32 @@ 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) { + return; + } + 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 +321,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 +335,8 @@ private void computeExpectedExecutionCosts() { */ private int fDeletedLines; + private UpdateDocumentListener fUpdateDocumentListener; + /** * Creates a new projection source viewer. @@ -313,6 +349,7 @@ private void computeExpectedExecutionCosts() { */ public ProjectionViewer(Composite parent, IVerticalRuler ruler, IOverviewRuler overviewRuler, boolean showsAnnotationOverview, int styles) { super(parent, ruler, overviewRuler, showsAnnotationOverview, styles); + fUpdateDocumentListener= new UpdateDocumentListener(); } /** @@ -510,6 +547,14 @@ public final void disableProjection() { fProjectionAnnotationModel.removeAllAnnotations(); fFindReplaceDocumentAdapter= null; fireProjectionDisabled(); + if (fVisibleRegionDuringProjection != null) { + super.setVisibleRegion(fVisibleRegionDuringProjection.getOffset(), fVisibleRegionDuringProjection.getLength()); + fVisibleRegionDuringProjection= null; + } + IDocument document= getDocument(); + if (document != null) { + document.removeDocumentListener(fUpdateDocumentListener); + } } } @@ -521,6 +566,15 @@ public final void enableProjection() { addProjectionAnnotationModel(getVisualAnnotationModel()); fFindReplaceDocumentAdapter= null; fireProjectionEnabled(); + IDocument document= getDocument(); + if (document == null) { + return; + } + IRegion visibleRegion= getVisibleRegion(); + if (visibleRegion != null && (visibleRegion.getOffset() != 0 || visibleRegion.getLength() != 0) && visibleRegion.getLength() < document.getLength()) { + setVisibleRegion(visibleRegion.getOffset(), visibleRegion.getLength()); + } + document.addDocumentListener(fUpdateDocumentListener); } } @@ -529,6 +583,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 +741,70 @@ 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()) { + super.setVisibleRegion(start, length); + return; + } + IDocument document= getDocument(); + if (document == null) { + return; + } + try { + // If the visible region changes, make sure collapsed regions outside of the old visible regions are expanded + // and collapse everything outside the new visible region + int end= computeEndOfVisibleRegion(start, length, document); + expandOutsideCurrentVisibleRegion(document); + collapseOutsideOfNewVisibleRegion(start, end, document); + fVisibleRegionDuringProjection= new Region(start, end - start - 1); + } catch (BadLocationException e) { + ILog log= ILog.of(getClass()); + log.log(new Status(IStatus.WARNING, getClass(), IStatus.OK, null, e)); + } + } + + private void expandOutsideCurrentVisibleRegion(IDocument document) throws BadLocationException { + if (fVisibleRegionDuringProjection != null) { + expand(0, fVisibleRegionDuringProjection.getOffset(), false); + int oldEnd= fVisibleRegionDuringProjection.getOffset() + fVisibleRegionDuringProjection.getLength(); + int length= document.getLength() - oldEnd; + if (length > 0) { + expand(oldEnd, length, false); + } + } + } + + private void collapseOutsideOfNewVisibleRegion(int start, int end, IDocument document) throws BadLocationException { + int documentLength= document.getLength(); + collapse(0, start, true); + + int endInvisibleRegionLength= documentLength - end; + if (endInvisibleRegionLength > 0) { + collapse(end, endInvisibleRegionLength, true); + } + } + + private static int computeEndOfVisibleRegion(int start, int length, IDocument document) throws BadLocationException { + int documentLength= document.getLength(); + int end= start + length + 1; + // ensure that trailing whitespace is included + // In this case, the line break needs to be included as well + boolean visibleRegionEndsWithTrailingWhitespace= end < documentLength && isWhitespaceButNotNewline(document.getChar(end - 1)); + while (end < documentLength && isWhitespaceButNotNewline(document.getChar(end))) { + end++; + visibleRegionEndsWithTrailingWhitespace= true; + } + if (visibleRegionEndsWithTrailingWhitespace && end < documentLength && isLineBreak(document.getChar(end))) { + end++; + } + return end; + } + + private static boolean isWhitespaceButNotNewline(char c) { + return Character.isWhitespace(c) && !isLineBreak(c); + } + + private static boolean isLineBreak(char c) { + return c == '\n' || c == '\r'; } @Override @@ -710,7 +829,9 @@ public void resetVisibleRegion() { @Override public IRegion getVisibleRegion() { - disableProjection(); + if (fVisibleRegionDuringProjection != null) { + return fVisibleRegionDuringProjection; + } IRegion visibleRegion= getModelCoverage(); if (visibleRegion == null) visibleRegion= new Region(0, 0); 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..f0ebe1b11dd 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,281 @@ 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(); + } + } + + @Test + public void testRemoveEntireVisibleRegion() 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); + viewer.setVisibleRegion(secondLineStart, secondLineEnd - secondLineStart); + viewer.enableProjection(); + shell.setVisible(true); + try { + document.replace(secondLineStart, secondLineEnd - secondLineStart, ""); + assertEquals("", viewer.getVisibleDocument().get()); + assertEquals(new Region(secondLineStart, 0), viewer.getVisibleRegion()); + } finally { + shell.dispose(); + } + } + }