From e6406586d3ca65ae8d4108efc280170c0f09a99f Mon Sep 17 00:00:00 2001 From: Denis Zhdanov Date: Thu, 1 Jul 2010 16:09:15 +0400 Subject: [PATCH] IDEA-53596 Soft wrap for editors 1. Introduced pure mock unit testing framework for soft wraps offsets & positions transformations; 2. Added unit tests for soft wrap processing; 3. Soft wrap processing is refactored in order to improve maintainability (as it's possible to check if the change brokes processing now); 4. Corrected left and right arrows processing in case of soft wraps (move to the next/previous visual soft-wrapped line); --- .../injected/editor/EditorWindow.java | 5 + platform/platform-api/platform-api.iml | 1 + .../editor/EditorModificationUtil.java | 38 +- .../openapi/editor/LogicalPosition.java | 43 +- .../openapi/editor/SoftWrapModel.java | 9 + .../openapi/editor/TextChangeTest.java | 55 ++ .../editor/actions/EditorActionUtil.java | 8 +- .../intellij/openapi/editor/ex/EditorEx.java | 2 + .../openapi/editor/ex/util/EditorUtil.java | 3 +- .../openapi/editor/impl/CaretModelImpl.java | 32 +- .../openapi/editor/impl/EditorImpl.java | 25 +- .../editor/impl/SoftWrapModelImpl.java | 325 ++++----- .../impl/softwrap/SoftWrapsStorage.java | 146 +++++ .../textarea/TextComponentSoftWrapModel.java | 5 + .../editor/impl/SoftWrapModelImplTest.java | 620 ++++++++++++++++++ .../src/com/intellij/mock/MockFoldRegion.java | 109 +++ 16 files changed, 1221 insertions(+), 205 deletions(-) create mode 100644 platform/platform-api/testSrc/com/intellij/openapi/editor/TextChangeTest.java create mode 100644 platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java create mode 100644 platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/SoftWrapModelImplTest.java create mode 100644 platform/testFramework/src/com/intellij/mock/MockFoldRegion.java diff --git a/platform/lang-impl/src/com/intellij/injected/editor/EditorWindow.java b/platform/lang-impl/src/com/intellij/injected/editor/EditorWindow.java index a107bb1d9691..8fc447cb4c66 100644 --- a/platform/lang-impl/src/com/intellij/injected/editor/EditorWindow.java +++ b/platform/lang-impl/src/com/intellij/injected/editor/EditorWindow.java @@ -153,6 +153,11 @@ public class EditorWindow implements EditorEx, UserDataHolderEx { return myDelegate.isViewer(); } + @Override + public boolean isUnderRepainting() { + return myDelegate.isUnderRepainting(); + } + public boolean isRendererMode() { return myDelegate.isRendererMode(); } diff --git a/platform/platform-api/platform-api.iml b/platform/platform-api/platform-api.iml index e0035d800df2..64d58129d8ac 100644 --- a/platform/platform-api/platform-api.iml +++ b/platform/platform-api/platform-api.iml @@ -4,6 +4,7 @@ + diff --git a/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java b/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java index c90a8ffb1d6c..39e3a57c658c 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/EditorModificationUtil.java @@ -31,6 +31,7 @@ import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.io.IOException; +import java.util.List; public class EditorModificationUtil { private EditorModificationUtil() {} @@ -221,16 +222,45 @@ public class EditorModificationUtil { } } + /** + * Calculates columns difference between current editor caret position and end of the logical line fragment displayed + * on a current visual line. + * + * @param editor target editor + * @return column difference between current editor caret position and end of the logical line fragment displayed + * on a current visual line + */ public static int calcAfterLineEnd(Editor editor) { Document document = editor.getDocument(); - LogicalPosition logicalPosition = editor.getCaretModel().getLogicalPosition(); - int columnNumber = logicalPosition.column; + CaretModel caretModel = editor.getCaretModel(); + LogicalPosition logicalPosition = caretModel.getLogicalPosition(); int lineNumber = logicalPosition.line; + int columnNumber = logicalPosition.column; if (lineNumber >= document.getLineCount()) { return columnNumber; } - int lineEndOffset = document.getLineEndOffset(lineNumber); - int lineEndColumnNumber = editor.offsetToLogicalPosition(lineEndOffset).column; + + int caretOffset = caretModel.getOffset(); + int anchorLineEndOffset = document.getLineEndOffset(lineNumber); + List softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(logicalPosition.line); + for (TextChange softWrap : softWraps) { + if (softWrap.getStart() > caretOffset) { + anchorLineEndOffset = softWrap.getStart(); + break; + } + + // Same offset corresponds to all soft wrap-introduced symbols, however, current method should behave differently in + // situations when the caret is located just before the soft wrap and at the next visual line. + if (softWrap.getStart() == caretOffset) { + boolean visuallyBeforeSoftWrap = caretModel.getVisualPosition().line < editor.offsetToVisualPosition(caretOffset).line; + if (visuallyBeforeSoftWrap) { + anchorLineEndOffset = softWrap.getStart(); + break; + } + } + } + + int lineEndColumnNumber = editor.offsetToLogicalPosition(anchorLineEndOffset).column; return columnNumber - lineEndColumnNumber; } diff --git a/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java b/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java index 3b9f0b34a704..7b8e3bf1e9c7 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java @@ -44,27 +44,25 @@ public class LogicalPosition implements Comparable { /** * Identifies if current logical position may be correctly mapped to visual position. E.g. we can define properties like - * {@link #softWrapLines}, {@link #softWrapColumnDiff} etc during {@code 'visual position' -> 'logical position'} conversion + * {@link #softWrapLinesBeforeCurrentLogicalLine}, {@link #softWrapColumnDiff} etc during {@code 'visual position' -> 'logical position'} conversion * in order to be able to easy match it back to visual position. */ public final boolean visualPositionAware; /** - * Number of virtual soft wrap-introduced lines before the visual position that corresponds to the current logical position. + * Number of virtual soft wrap-introduced lines before the current logical line. * * @see #visualPositionAware */ - public final int softWrapLines; + public final int softWrapLinesBeforeCurrentLogicalLine; /** - * Number of virtual soft lines introduced by the current soft wrap before the visual position that corresponds + * Number of virtual soft wrap introduced lines on a current logical line before the visual position that corresponds * to the current logical position. - *

- * This value is assumed to be not greater than {@link #softWrapLines} all the time. * * @see #visualPositionAware */ - public final int linesFromActiveSoftWrap; + public final int softWrapLinesOnCurrentLogicalLine; /** * Number to add to the {@link #column logical column} in order to get soft wrap-introduced visual column offset. @@ -91,21 +89,22 @@ public class LogicalPosition implements Comparable { this(line, column, 0, 0, 0, 0, 0, false); } - public LogicalPosition(int line, int column, int softWrapLines, int linesFromActiveSoftWrap, int softWrapColumnDiff, - int foldedLines, int foldingColumnDiff) + public LogicalPosition(int line, int column, int softWrapLinesBeforeCurrentLogicalLine, int softWrapLinesOnCurrentLogicalLine, + int softWrapColumnDiff, int foldedLines, int foldingColumnDiff) { - this(line, column, softWrapLines, linesFromActiveSoftWrap, softWrapColumnDiff, foldedLines, foldingColumnDiff, true); + this( + line, column, softWrapLinesBeforeCurrentLogicalLine, softWrapLinesOnCurrentLogicalLine, softWrapColumnDiff, foldedLines, + foldingColumnDiff, true + ); } - private LogicalPosition(int line, int column, int softWrapLines, int linesFromActiveSoftWrap, int softWrapColumnDiff, int foldedLines, - int foldingColumnDiff, boolean visualPositionAware) + private LogicalPosition(int line, int column, int softWrapLinesBeforeCurrentLogicalLine, int softWrapLinesOnCurrentLogicalLine, + int softWrapColumnDiff, int foldedLines, int foldingColumnDiff, boolean visualPositionAware) { - assert linesFromActiveSoftWrap <= softWrapLines; - this.line = line; this.column = column; - this.softWrapLines = softWrapLines; - this.linesFromActiveSoftWrap = linesFromActiveSoftWrap; + this.softWrapLinesBeforeCurrentLogicalLine = softWrapLinesBeforeCurrentLogicalLine; + this.softWrapLinesOnCurrentLogicalLine = softWrapLinesOnCurrentLogicalLine; this.softWrapColumnDiff = softWrapColumnDiff; this.foldedLines = foldedLines; this.foldingColumnDiff = foldingColumnDiff; @@ -130,7 +129,10 @@ public class LogicalPosition implements Comparable { * @return visual position based on a state of the current logical position */ public VisualPosition toVisualPosition() { - return new VisualPosition(line + softWrapLines - foldedLines, column + softWrapColumnDiff + foldingColumnDiff); + return new VisualPosition( + line + softWrapLinesBeforeCurrentLogicalLine + softWrapLinesOnCurrentLogicalLine - foldedLines, + column + softWrapColumnDiff + foldingColumnDiff + ); } public boolean equals(Object o) { @@ -147,14 +149,15 @@ public class LogicalPosition implements Comparable { @NonNls public String toString() { return "LogicalPosition: line=" + line + " column=" + column + "; visual position aware=" + visualPositionAware - + " soft wrap: lines=" + softWrapLines + " (active=" + linesFromActiveSoftWrap + ") columns diff=" + softWrapColumnDiff - + "; folding: lines = " + foldedLines + " columns diff=" + foldingColumnDiff; + + " soft wrap: lines=" + (softWrapLinesBeforeCurrentLogicalLine + softWrapLinesOnCurrentLogicalLine) + " (before=" + + softWrapLinesBeforeCurrentLogicalLine + "; current=" + softWrapLinesOnCurrentLogicalLine + + ") columns diff=" + softWrapColumnDiff + "; folding: lines = " + foldedLines + " columns diff=" + foldingColumnDiff; } public int compareTo(LogicalPosition position) { if (line != position.line) return line - position.line; if (column != position.column) return column - position.column; - if (softWrapLines != position.softWrapLines) return softWrapLines - position.softWrapLines; + if (softWrapLinesBeforeCurrentLogicalLine != position.softWrapLinesBeforeCurrentLogicalLine) return softWrapLinesBeforeCurrentLogicalLine - position.softWrapLinesBeforeCurrentLogicalLine; return softWrapColumnDiff - position.softWrapColumnDiff; } } diff --git a/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java b/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java index ae447fe5ee2f..8f77a3086b40 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/SoftWrapModel.java @@ -105,4 +105,13 @@ public interface SoftWrapModel { * @param position position where the document is to be modified */ void beforeDocumentChange(@NotNull VisualPosition position); + + /** + * Allows to answer if given visual position points to soft wrap-introduced virtual space. + * + * @param position target visual position to check + * @return true if given visual position points to soft wrap-introduced virtual space; + * false otherwise + */ + boolean isInsideSoftWrap(@NotNull VisualPosition position); } diff --git a/platform/platform-api/testSrc/com/intellij/openapi/editor/TextChangeTest.java b/platform/platform-api/testSrc/com/intellij/openapi/editor/TextChangeTest.java new file mode 100644 index 000000000000..0009ff18b083 --- /dev/null +++ b/platform/platform-api/testSrc/com/intellij/openapi/editor/TextChangeTest.java @@ -0,0 +1,55 @@ +package com.intellij.openapi.editor; + +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +/** + * @author Denis Zhdanov + * @since 06/29/2010 + */ +public class TextChangeTest { + + @Test(expected = IllegalArgumentException.class) + public void negativeStartIndex() { + new TextChange("", -1); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeEndIndex() { + new TextChange("", 0, -1); + } + + @Test(expected = IllegalArgumentException.class) + public void inconsistentIndices() { + new TextChange("", 2, 1); + } + + @Test + public void propertiesExposing() { + int start = 1; + int end = 10; + String text = "test"; + TextChange textChange = new TextChange(text, start, end); + assertEquals(text, textChange.getText()); + assertArrayEquals(text.toCharArray(), textChange.getChars()); + assertEquals(start, textChange.getStart()); + assertEquals(end, textChange.getEnd()); + } + + @Test(expected = IllegalArgumentException.class) + public void advanceToNegativeOffset() { + new TextChange("", 1, 2).advance(-2); + } + + @Test + public void advance() { + TextChange base = new TextChange("xyz", 3, 5); + + int[] offsets = {5, 0, -3}; + for (int offset : offsets) { + assertEquals(new TextChange(base.getText(), base.getStart() + offset, base.getEnd() + offset), base.advance(offset)); + } + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java index 63aa173a6f59..598f36197e5e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java @@ -462,14 +462,12 @@ public class EditorActionUtil { newOffset = i; } - // Check if there are white space symbols between the current caret position and visual line end. - if (newOffset == offset) { + // Move to the calculated end of visual line if caret is located on a last non-white space symbols on a line and there are + // remaining white space symbols. + if (newOffset == offset || newOffset == caretModel.getOffset()) { caretModel.moveToVisualPosition(visualEndOfLineWithCaret); } else { - if (newOffset == caretModel.getOffset()) { - newOffset = offset; - } caretModel.moveToOffset(newOffset); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java index d57e1cfb1dc5..ea689d85a0a3 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/EditorEx.java @@ -99,6 +99,8 @@ public interface EditorEx extends Editor { void setRendererMode(boolean isRendererMode); + boolean isUnderRepainting(); + void setFile(VirtualFile vFile); DataContext getDataContext(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java index fb514e5382fd..845dfd3ff759 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java @@ -101,7 +101,8 @@ public class EditorUtil { return resVisEnd.column; } } - int result = editor.offsetToVisualPosition(softWrap.getStart() - 1).column; + int result = editor.offsetToVisualPosition(softWrap.getStart() - 1).column; // Column of the symbol just before the soft wrap + result++; // Because we calculated column of the symbol just before the soft wrap result += calcColumnNumber(editor, softWrap.getText(), softWrapStartOffset, softWrapEndOffset); return result; } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java index 56a94026f1ac..3310c6c80f17 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/CaretModelImpl.java @@ -199,10 +199,20 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { } VisualPosition pos = new VisualPosition(newLineNumber, newColumnNumber); - moveToVisualPosition(pos); + if (columnShift != 0 && lineShift == 0 && myEditor.getSoftWrapModel().isInsideSoftWrap(pos)) { + LogicalPosition logical = myEditor.visualToLogicalPosition(pos); + int offsetToUse = myEditor.logicalPositionToOffset(logical); + if (columnShift < 0) { + offsetToUse--; - if (!editorSettings.isVirtualSpace() && columnShift == 0) { - myEditor.setLastColumnNumber(lastColumnNumber); + } + moveToOffset(offsetToUse); + } + else { + moveToVisualPosition(pos); + if (!editorSettings.isVirtualSpace() && columnShift == 0) { + myEditor.setLastColumnNumber(lastColumnNumber); + } } if (withSelection) { @@ -227,8 +237,8 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { validateCallContext(); int column = pos.column; int line = pos.line; - int softWrapLines = pos.softWrapLines; - int linesFromCurrentSoftWrap = pos.linesFromActiveSoftWrap; + int softWrapLinesBefore = pos.softWrapLinesBeforeCurrentLogicalLine; + int softWrapLinesCurrent = pos.softWrapLinesOnCurrentLogicalLine; int softWrapColumns = pos.softWrapColumnDiff; Document doc = myEditor.getDocument(); @@ -239,8 +249,8 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { } if (line < 0) { line = 0; - softWrapLines = 0; - linesFromCurrentSoftWrap = 0; + softWrapLinesBefore = 0; + softWrapLinesCurrent = 0; } int lineCount = doc.getLineCount(); @@ -249,8 +259,8 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { } else if (line > lineCount - 1) { line = lineCount - 1; - softWrapLines = 0; - linesFromCurrentSoftWrap = 0; + softWrapLinesBefore = 0; + softWrapLinesCurrent = 0; } EditorSettings editorSettings = myEditor.getSettings(); @@ -272,7 +282,7 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { LogicalPosition oldCaretPosition = myLogicalCaret; setCurrentLogicalCaret(new LogicalPosition( - line, column, softWrapLines, linesFromCurrentSoftWrap, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff + line, column, softWrapLinesBefore, softWrapLinesCurrent, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff )); final int offset = myEditor.logicalPositionToOffset(myLogicalCaret); @@ -307,7 +317,7 @@ public class CaretModelImpl implements CaretModel, PrioritizedDocumentListener { requestRepaint(oldInfo); if (oldCaretPosition.column + oldCaretPosition.softWrapColumnDiff != myLogicalCaret.column + myLogicalCaret.softWrapColumnDiff - || oldCaretPosition.line + oldCaretPosition.softWrapLines != myLogicalCaret.line + myLogicalCaret.softWrapLines) + || oldCaretPosition.line + oldCaretPosition.softWrapLinesBeforeCurrentLogicalLine != myLogicalCaret.line + myLogicalCaret.softWrapLinesBeforeCurrentLogicalLine) { CaretEvent event = new CaretEvent(myEditor, oldCaretPosition, myLogicalCaret); for (CaretListener listener : myCaretListeners) { diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index 4c09f1fc8f4b..ffe3b12fd5de 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -844,7 +844,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (softWrap != null) { // There is a possible case that soft wrap contains more than one line feed inside and we need to start counting not // from its first line. - int softWrapLinesToSkip = activeSoftWrapProcessed ? 0 : logicalPosition.linesFromActiveSoftWrap; + int softWrapLinesToSkip = activeSoftWrapProcessed ? 0 : logicalPosition.softWrapLinesOnCurrentLogicalLine; CharSequence softWrapText = softWrap.getText(); for (int i = 0; i < softWrapText.length(); i++) { c = softWrapText.charAt(i); @@ -1832,7 +1832,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (softWrap != null) { // There is a possible case that soft wrap contains more than one line feed inside and we need to start drawing not // from its first line. - int softWrapLinesToSkip = activeSoftWrapProcessed ? 0 : logicalPosition.linesFromActiveSoftWrap; + int softWrapLinesToSkip = activeSoftWrapProcessed ? 0 : logicalPosition.softWrapLinesOnCurrentLogicalLine; char[] softWrapChars = softWrap.getChars(); // We don't draw every soft wrap symbol one-by-one but whole visual line. Current variable holds index that points @@ -2379,7 +2379,11 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi public Dimension getContentSize() { Dimension size = mySizeContainer.getContentSize(); - return new Dimension(size.width, size.height + mySettings.getAdditionalLinesCount() * getLineHeight()); + //TODO den move soft wrap-related logic to editor size container + return new Dimension( + size.width, + size.height + (mySettings.getAdditionalLinesCount() + mySoftWrapModel.getSoftWrapsIntroducedLinesNumber()) * getLineHeight() + ); } public JScrollPane getScrollPane() { @@ -2567,8 +2571,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi else if (visiblePos.column > softWrapAwareVisFoldEnd.column) { int columnToUse = softWrapAwareLogFoldEnd.column + visiblePos.column - softWrapAwareVisFoldEnd.column; return new LogicalPosition( - softWrapAwareLogFoldEnd.line, columnToUse, softWrapAwareLogFoldEnd.softWrapLines, - softWrapAwareLogFoldEnd.linesFromActiveSoftWrap, visiblePos.column - columnToUse - softWrapAwareLogFoldEnd.foldingColumnDiff, + softWrapAwareLogFoldEnd.line, columnToUse, softWrapAwareLogFoldEnd.softWrapLinesBeforeCurrentLogicalLine, + softWrapAwareLogFoldEnd.softWrapLinesOnCurrentLogicalLine, visiblePos.column - columnToUse - softWrapAwareLogFoldEnd.foldingColumnDiff, softWrapAwareLogFoldEnd.foldedLines, softWrapAwareLogFoldEnd.foldingColumnDiff ); } @@ -2642,15 +2646,15 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi int columnNumber = pos.column; int lineNumber = pos.line; - int softWrapLines = pos.softWrapLines; - int linesFromCurrentSoftWrap = pos.linesFromActiveSoftWrap; + int softWrapLinesBeforeTargetLogicalLine = pos.softWrapLinesBeforeCurrentLogicalLine; + int softWrapLinesOnTargetLogicalLine = pos.softWrapLinesOnCurrentLogicalLine; int softWrapColumns = pos.softWrapColumnDiff; if (lineNumber < 0) { lineNumber = 0; columnNumber = 0; - softWrapLines = 0; - linesFromCurrentSoftWrap = 0; + softWrapLinesBeforeTargetLogicalLine = 0; + softWrapLinesOnTargetLogicalLine = 0; softWrapColumns = 0; } @@ -2686,7 +2690,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } } LogicalPosition pos1 = new LogicalPosition( - lineNumber, columnNumber, softWrapLines, linesFromCurrentSoftWrap, softWrapColumns, pos.foldedLines, pos.foldingColumnDiff + lineNumber, columnNumber, softWrapLinesBeforeTargetLogicalLine, softWrapLinesOnTargetLogicalLine, softWrapColumns, + pos.foldedLines, pos.foldingColumnDiff ); getCaretModel().moveToLogicalPosition(pos1); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java index e3a14b80b6cc..f7580cb0ca4f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java @@ -20,8 +20,10 @@ import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.event.DocumentListener; import com.intellij.openapi.editor.event.VisibleAreaEvent; import com.intellij.openapi.editor.event.VisibleAreaListener; +import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.editor.ex.SoftWrapModelEx; import com.intellij.openapi.editor.ex.util.EditorUtil; +import com.intellij.openapi.editor.impl.softwrap.SoftWrapsStorage; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.text.CharArrayUtil; import gnu.trove.TIntHashSet; @@ -33,7 +35,6 @@ import java.awt.*; import java.nio.CharBuffer; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; /** @@ -60,16 +61,12 @@ import java.util.List; */ public class SoftWrapModelImpl implements SoftWrapModelEx { - private static final Comparator SOFT_WRAPS_BY_OFFSET_COMPARATOR = new Comparator() { - public int compare(TextChange c1, TextChange c2) { - return c1.getStart() - c2.getEnd(); - } - }; - private final CharBuffer myCharBuffer = CharBuffer.allocate(1); + private final SoftWrapsStorage myStorage; + /** - * Holds lines where soft wraps should be removed. + * Holds logical lines where soft wraps should be removed. *

* The general idea is to do the following: *

    @@ -79,11 +76,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { */ private final TIntHashSet myDirtyLines = new TIntHashSet(); - /** Holds registered soft wraps sorted by offsets in ascending order. */ - private final List myWraps = new ArrayList(); - private final List myWrapsView = Collections.unmodifiableList(myWraps); - - private final EditorImpl myEditor; + private final EditorEx myEditor; private int myRightEdgeLocation = -1; private boolean myInitialized; @@ -99,8 +92,13 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { /** Holds number of 'active' calls, i.e. number of methods calls of the current object within the current call stack. */ private int myActive; - public SoftWrapModelImpl(@NotNull EditorImpl editor) { + public SoftWrapModelImpl(@NotNull EditorEx editor) { + this(editor, new SoftWrapsStorage()); + } + + public SoftWrapModelImpl(@NotNull EditorEx editor, @NotNull SoftWrapsStorage storage) { myEditor = editor; + myStorage = storage; } public boolean isSoftWrappingEnabled() { @@ -112,8 +110,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { if (myActive <= 0) { dropDataIfNecessary(); } - int i = getSoftWrapIndex(offset); - return i >= 0 ? myWraps.get(i) : null; + return myStorage.getSoftWrap(offset); } @Override @@ -123,14 +120,15 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { int start = document.getLineStartOffset(documentLine); int end = document.getLineEndOffset(documentLine); - int i = getSoftWrapIndex(start); + int i = myStorage.getSoftWrapIndex(start); if (i < 0) { i = -i - 1; } + List softWraps = myStorage.getSoftWraps(); List result = null; - for (; i < myWraps.size(); i++) { - TextChange softWrap = myWraps.get(i); + for (; i < softWraps.size(); i++) { + TextChange softWrap = softWraps.get(i); if (softWrap.getStart() >= end) { break; } @@ -147,48 +145,12 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { */ public int getSoftWrapsIntroducedLinesNumber() { int result = 0; - for (TextChange myWrap : myWraps) { + for (TextChange myWrap : myStorage.getSoftWraps()) { result += StringUtil.countNewLines(myWrap.getText()); } return result; } - /** - * Tries to find index of the target soft wrap stored at {@link #myWraps} collection. 'Target' soft wrap is the one - * that starts at the given offset. - * - * @param offset target offset - * @return index that conforms to {@link Collections#binarySearch(List, Object)} contract, i.e. non-negative returned - * index points to soft wrap that starts at the given offset; '-(negative value) - 1' points - * to position at {@link #myWraps} collection where soft wrap for the given index should be inserted - */ - private int getSoftWrapIndex(int offset) { - TextChange searchKey = new TextChange("", offset); - return Collections.binarySearch(myWraps, searchKey, SOFT_WRAPS_BY_OFFSET_COMPARATOR); - } - - private boolean hasSoftWrapAt(int offset) { - return getSoftWrapIndex(offset) >= 0; - } - - /** - * Inserts given soft wrap to {@link #myWraps} collection at the given index. - * - * @param softWrap soft wrap to store - * @return previous soft wrap object stored for the same offset if any; null otherwise - */ - @Nullable - private TextChange storeSoftWrap(TextChange softWrap) { - int i = Collections.binarySearch(myWraps, softWrap, SOFT_WRAPS_BY_OFFSET_COMPARATOR); - if (i >= 0) { - return myWraps.set(i, softWrap); - } - - i = -i - 1; - myWraps.add(i, softWrap); - return null; - } - /** * Allows to answer if symbols of the given char array located at [start; end) interval should be soft wrapped, * i.e. represented on a next line. @@ -206,7 +168,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { initIfNecessary(); dropDataIfNecessary(); - if (hasSoftWrapAt(start)) { + if (myStorage.hasSoftWrapAt(start)) { return true; } @@ -241,17 +203,21 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { dropDataIfNecessary(); //TODO den implement indent calculation on formatting options result = containsOnlyWhiteSpaces(chars, start, end) ? new TextChange("\n", start) : new TextChange("\n ", start); - storeSoftWrap(result); + myStorage.storeSoftWrap(result); return result; } public List getRegisteredSoftWraps() { - return myWrapsView; + return myStorage.getSoftWraps(); } /** * Drops information about registered soft wraps if they are marked as 'dirty'. - * //TODO den add doc about editor repainting + *

    + * The general idea of current {@link SoftWrapModel} implementation is that new soft wraps are created as necessary during target + * document painting. Hence, we drop registered soft wraps during this method processing only + * {@link EditorImpl#isUnderRepainting() during editor repainting} (there is a possible case that this method is called during + * other processing like gutter repainting). * * @see #myDataIsDirty */ @@ -259,38 +225,27 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { if (!myEditor.isUnderRepainting()) { return; } + + if (myDataIsDirty) { + myDataIsDirty = false; + myDirtyLines.clear(); + myStorage.removeAll(); + return; + } + Document document = myEditor.getDocument(); for (TIntIterator it = myDirtyLines.iterator(); it.hasNext();) { int line = it.next(); int start = document.getLineStartOffset(line); int end = document.getLineEndOffset(line); - - int startIndex = getSoftWrapIndex(start); - if (startIndex < 0) { - startIndex = -startIndex - 1; - } - int endIndex = startIndex; - for (; endIndex < myWraps.size(); endIndex++) { - TextChange softWrap = myWraps.get(endIndex); - if (softWrap.getStart() >= end) { - break; - } - } - myWraps.subList(startIndex, endIndex).clear(); + myStorage.removeInRange(start, end); } myDirtyLines.clear(); - - if (!myDataIsDirty) { - return; - - } - myDataIsDirty = false; - myWraps.clear(); } @NotNull public LogicalPosition adjustLogicalPosition(@NotNull LogicalPosition defaultLogical, @NotNull VisualPosition visual) { - if (myActive > 0 || !isSoftWrappingEnabled() || myWraps.isEmpty() || myEditor.getDocument().getTextLength() <= 0) { + if (myActive > 0 || !isSoftWrappingEnabled() || myStorage.isEmpty() || myEditor.getDocument().getTextLength() <= 0) { return defaultLogical; } @@ -310,54 +265,69 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } private LogicalPosition doAdjustLogicalPosition(LogicalPosition defaultLogical, VisualPosition visual) { - DocumentImpl document = (DocumentImpl)myEditor.getDocument(); + Document document = myEditor.getDocument(); int maxOffset = document.getLineEndOffset(Math.min(defaultLogical.line, document.getLineCount() - 1)); // This index points to registered soft wrap that is guaranteed to be located after the target visual line. - int endIndex = getSoftWrapIndex(maxOffset + 1); + int endIndex = myStorage.getSoftWrapIndex(maxOffset + 1); if (endIndex < 0) { endIndex = -endIndex - 1; } - int softWrapIntroducedLineFeeds = 0; + int softWrapLinesBeforeCurrentLogicalLine = 0; + int softWrapLinesOnCurrentLogicalLine = 0; + int lastSoftWrapLogicalLine = -1; FoldingModel foldingModel = myEditor.getFoldingModel(); int i = 0; - int max = Math.min(myWraps.size(), endIndex); + List softWraps = myStorage.getSoftWraps(); + int max = Math.min(softWraps.size(), endIndex); for (; i < max; i++) { - TextChange softWrap = myWraps.get(i); + TextChange softWrap = softWraps.get(i); if (foldingModel.isOffsetCollapsed(softWrap.getStart())) { continue; } int currentSoftWrapLineFeeds = StringUtil.countNewLines(softWrap.getText()); int softWrapLine = document.getLineNumber(softWrap.getStart()); - int visualLineBeforeSoftWrapAppliance - = myEditor.logicalToVisualPosition(new LogicalPosition(softWrapLine, 0)).line + softWrapIntroducedLineFeeds; + int visualLineBeforeSoftWrapAppliance = myEditor.logicalToVisualPosition(new LogicalPosition(softWrapLine, 0)).line + + softWrapLinesBeforeCurrentLogicalLine + softWrapLinesOnCurrentLogicalLine; if (visualLineBeforeSoftWrapAppliance > visual.line) { - int logicalLine = defaultLogical.line - softWrapIntroducedLineFeeds; - LogicalPosition foldingUnawarePosition - = new LogicalPosition(logicalLine, defaultLogical.column, softWrapIntroducedLineFeeds, 0, 0, 0, 0); + softWrapLinesBeforeCurrentLogicalLine += softWrapLinesOnCurrentLogicalLine; + int logicalLine = defaultLogical.line - softWrapLinesBeforeCurrentLogicalLine; + LogicalPosition foldingUnawarePosition = new LogicalPosition( + logicalLine, defaultLogical.column, softWrapLinesBeforeCurrentLogicalLine, 0, 0, 0, 0 + ); return adjustFoldingData(foldingModel, foldingUnawarePosition); } + if (lastSoftWrapLogicalLine >= 0 && lastSoftWrapLogicalLine != softWrapLine) { + softWrapLinesBeforeCurrentLogicalLine += softWrapLinesOnCurrentLogicalLine; + softWrapLinesOnCurrentLogicalLine = 0; + } + lastSoftWrapLogicalLine = softWrapLine; + int visualLineAfterSoftWrapAppliance = visualLineBeforeSoftWrapAppliance + currentSoftWrapLineFeeds; if (visualLineAfterSoftWrapAppliance < visual.line) { - softWrapIntroducedLineFeeds += currentSoftWrapLineFeeds; + softWrapLinesOnCurrentLogicalLine += currentSoftWrapLineFeeds; continue; } // If we're here that means that current soft wrap affects logical line that is matched to the given visual line. // We iterate from the logical line start then in order to calculate resulting logical position. - Context context = new Context(defaultLogical, visual, softWrapIntroducedLineFeeds, visualLineBeforeSoftWrapAppliance, foldingModel); + Context context = new Context( + defaultLogical, visual, softWrapLinesBeforeCurrentLogicalLine, softWrapLinesOnCurrentLogicalLine, + visualLineBeforeSoftWrapAppliance, foldingModel + ); int startLineOffset = document.getLineStartOffset(softWrapLine); int endLineOffset = document.getLineEndOffset(softWrapLine); - CharSequence documentText = document.getCharsNoThreadCheck(); + CharSequence documentText = document.getCharsSequence(); for (int j = startLineOffset; j < endLineOffset; j++) { // Process soft wrap at the current offset if any. - if (j == softWrap.getStart()) { - CharSequence softWrapText = softWrap.getText(); + TextChange softWrapToProcess = myStorage.getSoftWrap(j); + if (softWrapToProcess != null && j >= softWrap.getStart()) { + CharSequence softWrapText = softWrapToProcess.getText(); for (int k = 0; k < softWrapText.length(); k++) { LogicalPosition result = context.onSoftWrapSymbol(softWrapText.charAt(k)); if (result != null) { @@ -374,19 +344,23 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } // If we are here that means that target visual position is located at virtual space after the line end. - int logicalLine = defaultLogical.line - softWrapIntroducedLineFeeds - context.lineFeedsFromCurrentSoftWrap; + int logicalLine = defaultLogical.line - softWrapLinesBeforeCurrentLogicalLine + - context.softWrapLinesOnCurrentLineBeforeTargetSoftWrap - context.targetSoftWrapLines; int logicalColumn = context.symbolsOnCurrentLogicalLine + visual.column - context.symbolsOnCurrentVisualLine; int softWrapColumnDiff = visual.column - logicalColumn; LogicalPosition foldingUnawarePosition = new LogicalPosition( - logicalLine, logicalColumn, softWrapIntroducedLineFeeds + context.lineFeedsFromCurrentSoftWrap, - context.lineFeedsFromCurrentSoftWrap, softWrapColumnDiff, 0, 0 + logicalLine, logicalColumn, softWrapLinesBeforeCurrentLogicalLine, + context.softWrapLinesOnCurrentLineBeforeTargetSoftWrap + context.targetSoftWrapLines, softWrapColumnDiff, 0, 0 ); return adjustFoldingData(foldingModel, foldingUnawarePosition); } // If we are here that means that there is no soft wrap on a logical line that corresponds to the target visual line. - int logicalLine = defaultLogical.line - softWrapIntroducedLineFeeds; - LogicalPosition foldingUnaware = new LogicalPosition(logicalLine, defaultLogical.column, softWrapIntroducedLineFeeds, 0, 0, 0, 0); + softWrapLinesBeforeCurrentLogicalLine += softWrapLinesOnCurrentLogicalLine; + int logicalLine = defaultLogical.line - softWrapLinesBeforeCurrentLogicalLine; + LogicalPosition foldingUnaware = new LogicalPosition( + logicalLine, defaultLogical.column, softWrapLinesBeforeCurrentLogicalLine, 0, 0, 0, 0 + ); return adjustFoldingData(foldingModel, foldingUnaware); } @@ -402,8 +376,9 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { int foldedLines = 0; int foldColumnDiff = 0; int softWrapColumnDiff = position.softWrapColumnDiff; - DocumentImpl document = (DocumentImpl)myEditor.getDocument(); - CharSequence text = document.getCharsNoThreadCheck(); + Document document = myEditor.getDocument(); + CharSequence text = document.getCharsSequence(); + int targetLine = document.getLineNumber(offset); for (FoldRegion foldRegion : foldingModel.getAllFoldRegions()) { if (foldRegion.getStartOffset() >= offset) { break; @@ -415,7 +390,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { int foldingStartLine = document.getLineNumber(foldRegion.getStartOffset()); int foldingEndLine = document.getLineNumber(foldRegion.getEndOffset()); - foldedLines += foldingEndLine - foldingStartLine; + foldedLines += Math.min(targetLine, foldingEndLine) - foldingStartLine; // Process situation when target offset is located inside the folded region. if (offset >= foldRegion.getStartOffset() && offset < foldRegion.getEndOffset()) { @@ -447,8 +422,9 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { else { int endOffsetOfLineWithFoldingEnd = document.getLineEndOffset(foldingEndLine); int columnsBetweenFoldingEndAndOffset = toVisualColumnSymbolsNumber(text, foldRegion.getEndOffset(), endOffsetOfLineWithFoldingEnd); - if (position.column > endOffsetOfLineWithFoldingEnd) { - columnsBetweenFoldingEndAndOffset += position.column - endOffsetOfLineWithFoldingEnd; + LogicalPosition foldingEndLineEndPosition = myEditor.offsetToLogicalPosition(endOffsetOfLineWithFoldingEnd); + if (position.column > foldingEndLineEndPosition.column) { + columnsBetweenFoldingEndAndOffset += position.column - foldingEndLineEndPosition.column; } int visualFoldingStartColumn = calculateVisualFoldingStartColumn(foldRegion); int foldingPlaceholderWidth = toVisualColumnSymbolsNumber(foldRegion.getPlaceholderText()); @@ -459,25 +435,26 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } return new LogicalPosition( - position.line, position.column, position.softWrapLines, position.linesFromActiveSoftWrap, + position.line, position.column, position.softWrapLinesBeforeCurrentLogicalLine, position.softWrapLinesOnCurrentLogicalLine, softWrapColumnDiff, foldedLines, foldColumnDiff ); } private int calculateVisualFoldingStartColumn(FoldRegion region) { - DocumentImpl document = (DocumentImpl)myEditor.getDocument(); + Document document = myEditor.getDocument(); int foldingStartOffset = region.getStartOffset(); int logicalLine = document.getLineNumber(foldingStartOffset); int logicalLineStartOffset = document.getLineStartOffset(logicalLine); - int softWrapIndex = getSoftWrapIndex(logicalLineStartOffset); + int softWrapIndex = myStorage.getSoftWrapIndex(logicalLineStartOffset); if (softWrapIndex < 0) { softWrapIndex = -softWrapIndex - 1; } + List softWraps = myStorage.getSoftWraps(); int startOffsetOfVisualLineWithFoldingStart = logicalLineStartOffset; - for (; softWrapIndex < myWraps.size(); softWrapIndex++) { - TextChange softWrap = myWraps.get(softWrapIndex); + for (; softWrapIndex < softWraps.size(); softWrapIndex++) { + TextChange softWrap = softWraps.get(softWrapIndex); if (softWrap.getStart() >= foldingStartOffset) { break; } @@ -486,7 +463,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } assert startOffsetOfVisualLineWithFoldingStart <= foldingStartOffset; - return toVisualColumnSymbolsNumber(document.getCharsNoThreadCheck(), startOffsetOfVisualLineWithFoldingStart, foldingStartOffset); + return toVisualColumnSymbolsNumber(document.getCharsSequence(), startOffsetOfVisualLineWithFoldingStart, foldingStartOffset); } @NotNull @@ -505,39 +482,40 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { @SuppressWarnings({"AssignmentToForLoopParameter"}) private LogicalPosition offsetToLogicalPosition(int offset) { - DocumentImpl document = (DocumentImpl)myEditor.getDocument(); - CharSequence chars = document.getCharsNoThreadCheck(); + Document document = myEditor.getDocument(); + CharSequence chars = document.getCharsSequence(); int targetLine = document.getLineNumber(offset); int targetLineStartOffset = document.getLineStartOffset(targetLine); updateVisibleAreaIfNecessary(); - int softWrapIntroducedLines = 0; - int linesFromCurrentSoftWrap = 0; + int softWrapIntroducedLinesBefore = 0; + int softWrapsOnCurrentLogicalLine = 0; int symbolsOnCurrentLogicalLine = 0; int symbolsOnCurrentVisibleLine = 0; + List softWraps = myStorage.getSoftWraps(); // Retrieve information about logical position that is soft-wraps unaware. - LogicalPosition rawLineStartLogicalPosition = myEditor.offsetToLogicalPosition(targetLineStartOffset, false); + LogicalPosition rawLineStartLogicalPosition = myEditor.offsetToLogicalPosition(targetLineStartOffset); // Calculate number of soft wrap-introduced lines before the line that holds target offset. - int index = getSoftWrapIndex(targetLineStartOffset); + int index = myStorage.getSoftWrapIndex(targetLineStartOffset); if (index < 0) { index = -index - 1; } - int max = Math.min(index, myWraps.size()); + int max = Math.min(index, softWraps.size()); for (int j = 0; j < max; j++) { - softWrapIntroducedLines += StringUtil.countNewLines(myWraps.get(j).getText()); + softWrapIntroducedLinesBefore += StringUtil.countNewLines(softWraps.get(j).getText()); } FoldingModel foldingModel = myEditor.getFoldingModel(); // Return eagerly if there are no soft wraps before the target offset on a line that contains it. - if (max >= myWraps.size() || myWraps.get(max).getStart() > offset) { + if (max >= softWraps.size() || softWraps.get(max).getStart() > offset) { int column = toVisualColumnSymbolsNumber(chars, targetLineStartOffset, offset); LogicalPosition foldingUnawarePosition = new LogicalPosition( - rawLineStartLogicalPosition.line, column, softWrapIntroducedLines, 0, 0, 0, 0 + rawLineStartLogicalPosition.line, column, softWrapIntroducedLinesBefore, 0, 0, 0, 0 ); return adjustFoldingData(foldingModel, foldingUnawarePosition); } @@ -563,8 +541,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { CharSequence softWrapText = softWrap.getText(); for (int j = 0; j < softWrapText.length(); j++) { if (softWrapText.charAt(j) == '\n') { - softWrapIntroducedLines++; - linesFromCurrentSoftWrap++; + softWrapsOnCurrentLogicalLine++; symbolsOnCurrentVisibleLine = 0; } else { @@ -584,7 +561,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } LogicalPosition foldingUnawarePosition = new LogicalPosition( - rawLineStartLogicalPosition.line, symbolsOnCurrentLogicalLine, softWrapIntroducedLines, linesFromCurrentSoftWrap, + rawLineStartLogicalPosition.line, symbolsOnCurrentLogicalLine, softWrapIntroducedLinesBefore, softWrapsOnCurrentLogicalLine, symbolsOnCurrentVisibleLine - symbolsOnCurrentLogicalLine, 0, 0 ); return adjustFoldingData(foldingModel, foldingUnawarePosition); @@ -592,7 +569,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { @NotNull public VisualPosition adjustVisualPosition(@NotNull LogicalPosition logical, @NotNull VisualPosition defaultVisual) { - if (myActive > 0 || !isSoftWrappingEnabled() || myWraps.isEmpty()) { + if (myActive > 0 || !isSoftWrappingEnabled() || myStorage.isEmpty()) { return defaultVisual; } @@ -613,16 +590,18 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { @NotNull private VisualPosition doAdjustVisualPosition(LogicalPosition logical, VisualPosition visual) { + List softWraps = myStorage.getSoftWraps(); + // Check if there are registered soft wraps before the target logical position. int maxOffset = myEditor.logicalPositionToOffset(logical); - int endIndex = getSoftWrapIndex(maxOffset); + int endIndex = myStorage.getSoftWrapIndex(maxOffset); if (endIndex < 0) { endIndex = -endIndex - 2; // We subtract '2' instead of '1' here in order to point to offset of the first soft wrap the // is located before the given logical position. } // Return eagerly if no soft wraps are registered before the target offset. - if (endIndex < 0 || endIndex >= myWraps.size()) { + if (endIndex < 0 || endIndex >= softWraps.size()) { return visual; } @@ -632,7 +611,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { FoldingModel foldingModel = myEditor.getFoldingModel(); int targetLogicalLineStartOffset = myEditor.logicalPositionToOffset(new LogicalPosition(logical.line, 0)); for (int i = endIndex; i >= 0; i--) { - TextChange softWrap = myWraps.get(i); + TextChange softWrap = softWraps.get(i); if (softWrap == null) { assert false; continue; @@ -664,15 +643,41 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { return new VisualPosition(visual.line + lineDiff, columnToUse); } + @Override + public boolean isInsideSoftWrap(@NotNull VisualPosition visual) { + LogicalPosition logical = myEditor.visualToLogicalPosition(visual); + int offset = myEditor.logicalPositionToOffset(logical); + if (offset <= 0) { + // Never expect to be here, just a defensive programming. + return false; + } + + TextChange softWrap = getSoftWrap(offset); + if (softWrap == null) { + return false; + } + + // We consider visual positions that point after the last symbol before soft wrap and the first symbol after soft wrap to not + // belong to soft wrap-introduced virtual space. + VisualPosition visualAfterSoftWrap = myEditor.offsetToVisualPosition(offset); + if (visualAfterSoftWrap.equals(visual)) { + return false; + } + + VisualPosition visualBeforeSoftWrap = myEditor.offsetToVisualPosition(offset - 1); + return visual.line > visualBeforeSoftWrap.line || visual.column > visualBeforeSoftWrap.column + 1; + } + public void beforeDocumentChange(@NotNull VisualPosition visualPosition) { + List softWraps = myStorage.getSoftWraps(); LogicalPosition logicalPosition = myEditor.visualToLogicalPosition(visualPosition); int offset = myEditor.logicalPositionToOffset(logicalPosition); - int i = getSoftWrapIndex(offset); - if (i < 0 || i >= myWraps.size()) { + int i = myStorage.getSoftWrapIndex(offset); + if (i < 0 || i >= softWraps.size()) { return; } - TextChange softWrap = myWraps.get(i); + TextChange softWrap = softWraps.get(i); VisualPosition visualCaretPosition = myEditor.getCaretModel().getVisualPosition(); @@ -684,7 +689,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { // Restore caret position. myEditor.getCaretModel().moveToVisualPosition(visualCaretPosition); - myWraps.remove(i); + myStorage.removeByIndex(i); } /** @@ -693,14 +698,15 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { * @param change change introduced to the document */ private void updateRegisteredSoftWraps(TextChange change) { - int softWrapIndex = getSoftWrapIndex(change.getStart()); + List softWraps = myStorage.getSoftWraps(); + int softWrapIndex = myStorage.getSoftWrapIndex(change.getStart()); if (softWrapIndex < 0) { softWrapIndex = -softWrapIndex - 1; } Document document = myEditor.getDocument(); - if (softWrapIndex >= myWraps.size() || document.getTextLength() <= 0) { + if (softWrapIndex >= softWraps.size() || document.getTextLength() <= 0) { return; } int firstChangedLine = document.getLineNumber(change.getStart()); @@ -710,13 +716,14 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } // Collect soft wraps which offsets should be modified. - List modified = myWraps.subList(softWrapIndex, myWraps.size()); - List toModify = new ArrayList(modified); - modified.clear(); + List toModify = new ArrayList(softWraps.subList(softWrapIndex, softWraps.size())); + + // Remove soft wraps that should be modified. + myStorage.removeInRange(softWrapIndex, softWraps.size()); // Add modified soft wraps. for (TextChange softWrap : toModify) { - myWraps.add(softWrap.advance(change.getDiff())); + softWraps.add(softWrap.advance(change.getDiff())); } } @@ -802,21 +809,24 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { private class Context { - public final FoldingModel foldingModel; + public final FoldingModel foldingModel; public final LogicalPosition softWrapUnawareLogicalPosition; - public final VisualPosition targetVisualPosition; - public final int softWrapIntroducedLines; - public final int visualLineBeforeSoftWrapAppliance; - public int lineFeedsFromCurrentSoftWrap; + public final VisualPosition targetVisualPosition; + public final int softWrapLinesBefore; + public final int visualLineBeforeSoftWrapAppliance; + public final int softWrapLinesOnCurrentLineBeforeTargetSoftWrap; + + public int targetSoftWrapLines; public int symbolsOnCurrentLogicalLine; public int symbolsOnCurrentVisualLine; - Context(LogicalPosition softWrapUnawareLogicalPosition, VisualPosition targetVisualPosition, int softWrapIntroducedLines, - int visualLineBeforeSoftWrapAppliance, FoldingModel foldingModel) + Context(LogicalPosition softWrapUnawareLogicalPosition, VisualPosition targetVisualPosition, int softWrapLinesBefore, + int softWrapLinesOnCurrentLineBeforeTargetSoftWrap, int visualLineBeforeSoftWrapAppliance, FoldingModel foldingModel) { this.softWrapUnawareLogicalPosition = softWrapUnawareLogicalPosition; this.targetVisualPosition = targetVisualPosition; - this.softWrapIntroducedLines = softWrapIntroducedLines; + this.softWrapLinesBefore = softWrapLinesBefore; + this.softWrapLinesOnCurrentLineBeforeTargetSoftWrap = softWrapLinesOnCurrentLineBeforeTargetSoftWrap; this.visualLineBeforeSoftWrapAppliance = visualLineBeforeSoftWrapAppliance; this.foldingModel = foldingModel; } @@ -832,19 +842,20 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { public LogicalPosition onSoftWrapSymbol(char c) { // Process line feed inside soft wrap. if (c == '\n') { - if (targetVisualPosition.line == visualLineBeforeSoftWrapAppliance + lineFeedsFromCurrentSoftWrap) { + if (targetVisualPosition.line == visualLineBeforeSoftWrapAppliance + targetSoftWrapLines) { return build(targetVisualPosition.column - symbolsOnCurrentLogicalLine); } else { - lineFeedsFromCurrentSoftWrap++; + targetSoftWrapLines++; symbolsOnCurrentVisualLine = 0; return null; } } // Just update information about tracked symbols number if current visual line is too low. - if (targetVisualPosition.line > visualLineBeforeSoftWrapAppliance + lineFeedsFromCurrentSoftWrap) { + if (targetVisualPosition.line > visualLineBeforeSoftWrapAppliance + targetSoftWrapLines) { symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c); + return null; } // There is a possible case that, for example, target visual column is zero and it points to the soft-wrapped line, @@ -880,7 +891,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } // Just update information about tracked symbols number if current visual line is too low. - if (targetVisualPosition.line > visualLineBeforeSoftWrapAppliance + lineFeedsFromCurrentSoftWrap) { + if (targetVisualPosition.line > visualLineBeforeSoftWrapAppliance + targetSoftWrapLines) { symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c); symbolsOnCurrentLogicalLine++; return null; @@ -909,10 +920,16 @@ public class SoftWrapModelImpl implements SoftWrapModelEx { } private LogicalPosition build(int softWrapColumnDiff) { - int logicalLine = softWrapUnawareLogicalPosition.line - softWrapIntroducedLines - lineFeedsFromCurrentSoftWrap; + int logicalLine = softWrapUnawareLogicalPosition.line - softWrapLinesBefore + - softWrapLinesOnCurrentLineBeforeTargetSoftWrap - targetSoftWrapLines; LogicalPosition foldingUnawareResult = new LogicalPosition( - logicalLine, symbolsOnCurrentLogicalLine, softWrapIntroducedLines + lineFeedsFromCurrentSoftWrap, lineFeedsFromCurrentSoftWrap, - softWrapColumnDiff, 0, 0 + logicalLine, + symbolsOnCurrentLogicalLine, + softWrapLinesBefore, + softWrapLinesOnCurrentLineBeforeTargetSoftWrap + targetSoftWrapLines, + softWrapColumnDiff, + 0, + 0 ); return adjustFoldingData(foldingModel, foldingUnawareResult); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java new file mode 100644 index 000000000000..6a76ebca2e3c --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java @@ -0,0 +1,146 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.editor.impl.softwrap; + +import com.intellij.openapi.editor.TextChange; +import gnu.trove.TIntHashSet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Holds registered soft wraps and provides monitoring and management facilities for them. + *

    + * Not thread-safe. + * + * @author Denis Zhdanov + * @since Jun 29, 2010 3:04:20 PM + */ +public class SoftWrapsStorage { + + private static final Comparator SOFT_WRAPS_BY_OFFSET_COMPARATOR = new Comparator() { + public int compare(TextChange c1, TextChange c2) { + return c1.getStart() - c2.getEnd(); + } + }; + + private final List myWraps = new ArrayList(); + private final List myWrapsView = Collections.unmodifiableList(myWraps); + + /** + * @return true if there is at least one soft wrap registered at the current storage; false otherwise + */ + public boolean isEmpty() { + return myWraps.isEmpty(); + } + + public boolean hasSoftWrapAt(int offset) { + return getSoftWrapIndex(offset) >= 0; + } + + @Nullable + public TextChange getSoftWrap(int offset) { + int i = getSoftWrapIndex(offset); + return i >= 0 ? myWraps.get(i) : null; + } + + /** + * @return view for registered soft wraps sorted by offset in ascending order if any; empty collection otherwise + */ + @NotNull + public List getSoftWraps() { + return myWrapsView; + } + + /** + * Tries to find index of the target soft wrap stored at {@link #myWraps} collection. 'Target' soft wrap is the one + * that starts at the given offset. + * + * @param offset target offset + * @return index that conforms to {@link Collections#binarySearch(List, Object)} contract, i.e. non-negative returned + * index points to soft wrap that starts at the given offset; '-(negative value) - 1' points + * to position at {@link #myWraps} collection where soft wrap for the given index should be inserted + */ + public int getSoftWrapIndex(int offset) { + TextChange searchKey = new TextChange("", offset); + return Collections.binarySearch(myWraps, searchKey, SOFT_WRAPS_BY_OFFSET_COMPARATOR); + } + + /** + * Inserts given soft wrap to {@link #myWraps} collection at the given index. + * + * @param softWrap soft wrap to store + * @return previous soft wrap object stored for the same offset if any; null otherwise + */ + @Nullable + public TextChange storeSoftWrap(TextChange softWrap) { + int i = Collections.binarySearch(myWraps, softWrap, SOFT_WRAPS_BY_OFFSET_COMPARATOR); + if (i >= 0) { + return myWraps.set(i, softWrap); + } + + i = -i - 1; + myWraps.add(i, softWrap); + return null; + } + + /** + * Asks current storage to remove soft wrap registered for the current index if any (soft wraps are stored at collection + * ordered by soft wrap start offsets). + * + * @param index target soft wrap index + * @return removed soft wrap if the one was found for the given index; null otherwise + */ + @Nullable + public TextChange removeByIndex(int index) { + if (index < 0 || index >= myWraps.size()) { + return null; + } + return myWraps.remove(index); + } + + /** + * Removes all soft wraps with offsets at [start; end] range registered at the current storage if any. + * + * @param start start range offset (inclusive) + * @param end end range offset (exclusive) + */ + public void removeInRange(int start, int end) { + int startIndex = getSoftWrapIndex(start); + if (startIndex < 0) { + startIndex = -startIndex - 1; + } + int endIndex = startIndex; + for (; endIndex < myWraps.size(); endIndex++) { + TextChange softWrap = myWraps.get(endIndex); + if (softWrap.getStart() >= end) { + break; + } + } + myWraps.subList(startIndex, endIndex).clear(); + } + + /** + * Removes all soft wraps registered at the current storage. + */ + public void removeAll() { + myWraps.clear(); + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentSoftWrapModel.java b/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentSoftWrapModel.java index 682b7a75a17c..802b20aa31cb 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentSoftWrapModel.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/textarea/TextComponentSoftWrapModel.java @@ -50,4 +50,9 @@ public class TextComponentSoftWrapModel implements SoftWrapModel { @Override public void beforeDocumentChange(@NotNull VisualPosition position) { } + + @Override + public boolean isInsideSoftWrap(@NotNull VisualPosition position) { + return false; + } } diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/SoftWrapModelImplTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/SoftWrapModelImplTest.java new file mode 100644 index 000000000000..d1c97b570825 --- /dev/null +++ b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/SoftWrapModelImplTest.java @@ -0,0 +1,620 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.editor.impl; + +import com.intellij.mock.MockFoldRegion; +import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.impl.softwrap.SoftWrapsStorage; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import gnu.trove.TIntHashSet; +import org.jetbrains.annotations.Nullable; +import org.jmock.Expectations; +import org.jmock.api.Invocation; +import org.jmock.lib.action.CustomAction; +import org.junit.Test; +import org.junit.Before; +import org.junit.After; +import org.jmock.integration.junit4.JUnit4Mockery; +import org.jmock.Mockery; +import org.jmock.lib.legacy.ClassImposteriser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * @author Denis Zhdanov + * @since 06/29/2010 + */ +public class SoftWrapModelImplTest { + + private static final Comparator LOGICAL_POSITIONS_COMPARATOR = new Comparator() { + @Override + public int compare(DataEntry o1, DataEntry o2) { + LogicalPosition logical1 = o1.logical; + LogicalPosition logical2 = o2.logical; + if (logical1.line != logical2.line) { + return logical1.line - logical2.line; + } + + // There is a possible case that multiple logical positions match to the same visual position (e.g. logical + // positions for folded text match to the same visual position). We want to match to the logical position of + // folding region start if we search by logical position from folded text. + if (o1.foldedSpace & o2.foldedSpace && logical1.column + logical1.foldingColumnDiff == logical2.foldingColumnDiff) { + return o1.foldedSpace ? 1 : -1; + } + return logical1.column - logical2.column; + } + }; + + private static final Comparator OFFSETS_COMPARATOR = new Comparator() { + @Override + public int compare(DataEntry o1, DataEntry o2) { + // There are numerous situations when multiple visual positions share the same offset (e.g. all soft wrap-introduced virtual + // spaces share offset with the first document symbol after soft wrap or all virtual spaces after line end share the same offset + // as the last line symbol). We want to ignore such positions during lookup by offset. + if (o1.offset == o2.offset && o1.virtualSpace ^ o2.virtualSpace) { + return o1.virtualSpace ? 1 : -1; + } + return o1.offset - o2.offset; + } + }; + + private static final String SOFT_WRAP_START_MARKER = ""; + private static final String SOFT_WRAP_END_MARKER = ""; + private static final String FOLDING_START_MARKER = ""; + private static final String FOLDING_END_MARKER = ""; + private static final int TAB_SIZE = 4; + + /** Holds expected mappings between visual and logical positions and offset. */ + private final List myExpectedData = new ArrayList(); + private final List myLineRanges = new ArrayList(); + /** Holds document offsets that are considered to be folded. */ + private final TIntHashSet myFoldedOffsets = new TIntHashSet(); + private final List myFoldRegions = new ArrayList(); + + private SoftWrapModelImpl myModel; + private Mockery myMockery; + private ScrollingModel myScrollingModel; + private EditorEx myEditor; + private Document myDocument; + private SoftWrapsStorage myStorage; + private FoldingModel myFoldingModel; + + @Before + public void setUp() { + myMockery = new JUnit4Mockery() {{ + setImposteriser(ClassImposteriser.INSTANCE); + }}; + myStorage = new SoftWrapsStorage(); + myEditor = myMockery.mock(EditorEx.class); + myDocument = myMockery.mock(Document.class); + final EditorSettings settings = myMockery.mock(EditorSettings.class); + myScrollingModel = myMockery.mock(ScrollingModel.class); + myFoldingModel = myMockery.mock(FoldingModel.class); + final Project project = myMockery.mock(Project.class); + + myMockery.checking(new Expectations() {{ + // Document + allowing(myEditor).getDocument();will(returnValue(myDocument)); + allowing(myDocument).getLineCount(); will(new CustomAction("getLineCount()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return myLineRanges.size(); + } + }); + allowing(myDocument).getLineNumber(with(any(int.class))); will(new CustomAction("getLineNumber()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return getLineNumber((Integer)invocation.getParameter(0)); + } + }); + allowing(myDocument).getLineStartOffset(with(any(int.class))); will(new CustomAction("getLineStart()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return getLineStartOffset((Integer)invocation.getParameter(0)); + } + }); + allowing(myDocument).getLineEndOffset(with(any(int.class))); will(new CustomAction("getLineEnd()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return getLineEndOffset((Integer)invocation.getParameter(0)); + } + }); + + // Settings. + allowing(myEditor).getSettings();will(returnValue(settings)); + allowing(settings).isUseSoftWraps();will(returnValue(true)); + allowing(settings).getTabSize(project);will(returnValue(TAB_SIZE)); + allowing(myEditor).getProject();will(returnValue(project)); + + // Scrolling + ignoring(myScrollingModel); + allowing(myEditor).getScrollingModel();will(returnValue(myScrollingModel)); + + // Folding. + allowing(myEditor).getFoldingModel();will(returnValue(myFoldingModel)); + allowing(myFoldingModel).isOffsetCollapsed(with(any(int.class))); will(new CustomAction("isOffsetCollapsed()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return myFoldedOffsets.contains((Integer)invocation.getParameter(0)); + } + }); + allowing(myFoldingModel).getCollapsedRegionAtOffset(with(any(int.class))); will(new CustomAction("getCollapsedRegionAtOffset()") { + @Nullable + @Override + public Object invoke(Invocation invocation) throws Throwable { + return getCollapsedFoldRegion((Integer)invocation.getParameter(0)); + } + }); + allowing(myFoldingModel).getAllFoldRegions(); will(new CustomAction("getAllFoldRegions()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return myFoldRegions.toArray(new FoldRegion[myFoldRegions.size()]); + } + }); + + // Soft wrap-unaware conversions. + allowing(myEditor).logicalToVisualPosition(with(any(LogicalPosition.class))); will(new CustomAction("logical2visual()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return logicalToVisual((LogicalPosition)invocation.getParameter(0)); + } + }); + allowing(myEditor).logicalPositionToOffset(with(any(LogicalPosition.class))); will(new CustomAction("logical2offset()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return logicalToOffset((LogicalPosition)invocation.getParameter(0)); + } + }); + allowing(myEditor).offsetToLogicalPosition(with(any(int.class))); will(new CustomAction("offset2logical()") { + @Override + public Object invoke(Invocation invocation) throws Throwable { + return offsetToLogical((Integer)invocation.getParameter(0)); + } + }); + }}); + + myModel = new SoftWrapModelImpl(myEditor, myStorage); + } + + @After + public void checkExpectations() { + myMockery.assertIsSatisfied(); + } + + @Test + public void softWrapHasSymbolBeforeFirstLineFeed() { + String document = + "public class Test {\n" + + " public void foo(int[] data) {\n" + + " bar(data[0], data[1], data[2], data[3], \n" + + " data[4], data[5], data[6], \n" + + " data[7], data[8]); \n" + + " }\n" + + " public void bar(int ... i) {\n" + + " }\n" + + "}"; + test(document); + } + + @Test + public void multipleSoftWrappedLogicalLines() { + String document = + "public class Test {\n" + + " public void foo(int[] data) {\n" + + " bar(data[0], data[1], \n" + + " data[2], data[3], \n" + + " data[4], data[5], \n" + + " data[6], data[7], \n" + + " data[8], data[9], \n" + + " data[10], data[11], \n" + + " data[12], data[13]); \n" + + " }\n" + + " public void bar(int ... i) {\n" + + " }\n" + + "}"; + test(document); + } + + @Test + public void softWrapAndFoldedLines() { + String document = + "public class Test {\n" + + " public void foo(int[] data) {\n" + + " bar(data[0], data[1], \n" + + " data[2], data[3], \n" + + " data[4], data[5], \n" + + " data[6], data[7], \n" + + " data[8], data[9], \n" + + " data[10], data[11], \n" + + " data[12], data[13], \n" + + " data[14], data[15], \n" + + " data[16], data[17], \n" + + " data[18], data[19], \n" + + " data[20], data[21]); \n" + + " }\n" + + " public void bar(int ... i) {\n" + + " }\n" + + "}"; + test(document); + } + + private int getLineNumber(int offset) { + int line = 0; + for (TextRange range : myLineRanges) { + if (offset >= range.getStartOffset() && offset <= range.getEndOffset()) { + return line; + } + line++; + } + throw new AssertionError(String.format("Can't find test document line for offset %d. Registered lines: %s", offset, myLineRanges)); + } + + private int getLineStartOffset(int line) { + checkLine(line); + return myLineRanges.get(line).getStartOffset(); + } + + private int getLineEndOffset(int line) { + checkLine(line); + return myLineRanges.get(line).getEndOffset(); + } + + private void checkLine(int line) { + if (line < 0 || line >= myLineRanges.size()) { + throw new AssertionError(String.format("Can't retrieve target data for the given line (%d). Reason - it's not within allowed " + + "bounds ([0; %d])", line, myLineRanges.size() - 1)); + } + } + + @Nullable + private FoldRegion getCollapsedFoldRegion(int offset) { + for (FoldRegion region : myFoldRegions) { + if (region.getStartOffset() == offset) { + return region; + } + } + return null; + } + + private VisualPosition logicalToVisual(LogicalPosition position) { + DataEntry dataEntry = myExpectedData.get(findIndex(new DataEntry(null, position, 0, false), LOGICAL_POSITIONS_COMPARATOR)); + return toSoftWrapUnawareVisual(dataEntry); + } + + private int logicalToOffset(LogicalPosition position) { + DataEntry dataEntry = myExpectedData.get(findIndex(new DataEntry(null, position, 0, false), LOGICAL_POSITIONS_COMPARATOR)); + return dataEntry.offset; + } + + private LogicalPosition offsetToLogical(int offset) { + DataEntry dataEntry = myExpectedData.get(findIndex(new DataEntry(null, null, offset, false), OFFSETS_COMPARATOR)); + return toSoftWrapUnawareLogicalByOffset(dataEntry); + } + + private int findIndex(DataEntry key, Comparator comparator) { + int i = Collections.binarySearch(myExpectedData, key, comparator); + if (i < 0 || i >= myExpectedData.size()) { + throw new AssertionError(String.format("Can't find pre-configured data entry for the given key (%s). " + + "Available data: %s", key, myExpectedData)); + } + return i; + } + + private static VisualPosition toSoftWrapUnawareVisual(DataEntry dataEntry) { + LogicalPosition logical = dataEntry.logical; + return new VisualPosition(logical.line - logical.foldedLines, logical.column + logical.foldingColumnDiff); + } + + private static LogicalPosition toSoftWrapUnawareLogicalByVisual(DataEntry dataEntry) { + LogicalPosition logical = dataEntry.logical; + return new LogicalPosition( + logical.line + logical.softWrapLinesBeforeCurrentLogicalLine + logical.softWrapLinesOnCurrentLogicalLine, + logical.column + ); + } + + private static LogicalPosition toSoftWrapUnawareLogicalByOffset(DataEntry dataEntry) { + LogicalPosition logical = dataEntry.logical; + return new LogicalPosition(logical.line, logical.column); + } + + private void test(String documentText) { + init(documentText); + for (DataEntry data : myExpectedData) { + + // Check logical by visual. + LogicalPosition actualLogicalByVisual = myModel.adjustLogicalPosition(toSoftWrapUnawareLogicalByVisual(data), data.visual); + // We don't want to perform the check for logical positions that correspond to the folded space because all of them relate to + // the same logical position of the folding start. + if (!data.foldedSpace && !equals(data.logical, actualLogicalByVisual)) { + throw new AssertionError( + String.format("Detected unmatched logical position by visual (%s). Expected: '%s', actual: '%s'. Calculation was performed " + + "against soft wrap-unaware logical: '%s'", + data.visual, data.logical, actualLogicalByVisual, toSoftWrapUnawareLogicalByVisual(data)) + ); + } + + // Check logical by offset. + LogicalPosition actualLogicalByOffset = myModel.adjustLogicalPosition(toSoftWrapUnawareLogicalByOffset(data), data.offset); + // We don't to perform the check for the data that points to soft wrap location here. The reason is that it shares offset + // with the first document symbol after soft wrap, hence, examination always fails. + if (!data.virtualSpace && !equals(data.logical, actualLogicalByOffset)) { + throw new AssertionError( + String.format("Detected unmatched logical position by offset. Expected: '%s', actual: '%s'. Calculation was performed " + + "against offset: '%d' and soft wrap-unaware logical: '%s'", + data.logical, actualLogicalByOffset, data.offset, toSoftWrapUnawareLogicalByOffset(data)) + ); + } + + // Check visual by logical. + VisualPosition actualVisual = myModel.adjustVisualPosition(data.logical, toSoftWrapUnawareVisual(data)); + if (!actualVisual.equals(data.visual)) { + throw new AssertionError( + String.format("Detected unmatched visual position by logical. Expected: '%s', actual: '%s'. Calculation was performed " + + "against logical position: '%s' and soft wrap-unaware visual: '%s'", + data.visual, actualVisual, data.logical, toSoftWrapUnawareVisual(data)) + ); + } + } + } + + private static boolean equals(LogicalPosition expected, LogicalPosition actual) { + return expected.equals(actual) && expected.softWrapLinesBeforeCurrentLogicalLine == actual.softWrapLinesBeforeCurrentLogicalLine + && expected.softWrapLinesOnCurrentLogicalLine == actual.softWrapLinesOnCurrentLogicalLine + && expected.softWrapColumnDiff == actual.softWrapColumnDiff && expected.foldedLines == actual.foldedLines + && expected.foldingColumnDiff == actual.foldingColumnDiff; + } + + @SuppressWarnings({"AssignmentToForLoopParameter"}) + private void init(String documentText) { + final Context context = new Context(); + for (int i = 0; i < documentText.length(); i++) { + if (isSoftWrapStart(documentText, i)) { + context.onSoftWrapStart(); + i += SOFT_WRAP_START_MARKER.length() - 1; // Subtract 1 because 'i' is incremented by 1 on every iteration + continue; + } + + if (isSoftWrapEnd(documentText, i)) { + context.onSoftWrapEnd(); + i += SOFT_WRAP_END_MARKER.length() - 1; // Subtract 1 because 'i' is incremented by 1 on every iteration + continue; + } + + if (isFoldingStart(documentText, i)) { + context.onFoldingStart(); + i += FOLDING_START_MARKER.length() - 1; // Subtract 1 because 'i' is incremented by 1 on every iteration + continue; + } + + if (isFoldingEnd(documentText, i)) { + context.onFoldingEnd(); + i += FOLDING_END_MARKER.length() - 1; // Subtract 1 because 'i' is incremented by 1 on every iteration + continue; + } + + context.onNewSymbol(documentText.charAt(i)); + } + + myLineRanges.add(new TextRange(context.logicalLineStartOffset, context.document.length())); + + myMockery.checking(new Expectations() {{ + allowing(myDocument).getCharsSequence(); will(returnValue(context.document)); + allowing(myDocument).getTextLength(); will(returnValue(context.document.length())); + }}); + } + + private static boolean isSoftWrapStart(String document, int index) { + return matches(document, index, SOFT_WRAP_START_MARKER); + } + + private static boolean isSoftWrapEnd(String document, int index) { + return matches(document, index, SOFT_WRAP_END_MARKER); + } + + private static boolean isFoldingStart(String document, int index) { + return matches(document, index, FOLDING_START_MARKER); + } + + private static boolean isFoldingEnd(String document, int index) { + return matches(document, index, FOLDING_END_MARKER); + } + + private static boolean matches(String document, int index, String pattern) { + if (index + pattern.length() > document.length()) { + return false; + } + return pattern.equals(document.substring(index, index + pattern.length())); + } + + private static class DataEntry { + public final VisualPosition visual; + public final LogicalPosition logical; + public final int offset; + public final boolean foldedSpace; + public final boolean virtualSpace; + + DataEntry(VisualPosition visual, LogicalPosition logical, int offset, boolean foldedSpace) { + this(visual, logical, offset, foldedSpace, false); + } + + DataEntry(VisualPosition visual, LogicalPosition logical, int offset, boolean foldedSpace, boolean virtualSpace) { + this.visual = visual; + this.logical = logical; + this.offset = offset; + this.foldedSpace = foldedSpace; + this.virtualSpace = virtualSpace; + } + + @Override + public String toString() { + return "offset: " + offset + ", logical: " + logical + ", visual: " + visual + ", folded: " + foldedSpace + + ", virtual: " + virtualSpace; + } + } + + private class Context { + private final StringBuilder mySoftWrapBuffer = new StringBuilder(); + final StringBuilder document = new StringBuilder(); + + boolean virtualSpace; + boolean insideFolding; + int logicalLineStartOffset; + int logicalLine; + int logicalColumn; + int visualLine; + int visualColumn; + int softWrapStartOffset; + int softWrapLinesBeforeCurrentLogical; + int softWrapLinesOnCurrentLogical; + int softWrapSymbolsOnCurrentVisualLine; + int foldingStartOffset; + int foldingStartLogicalColumn; + int foldingStartVisualColumn; + int foldingColumnDiff; + int foldedLines; + int offset; + + public void onSoftWrapStart() { + softWrapStartOffset = offset; + virtualSpace = true; + } + + public void onSoftWrapEnd() { + myStorage.storeSoftWrap(new TextChange(mySoftWrapBuffer.toString(), softWrapStartOffset)); + mySoftWrapBuffer.setLength(0); + virtualSpace = false; + } + + public void onFoldingStart() { + foldingStartOffset = offset; + foldingStartLogicalColumn = logicalColumn; + foldingStartVisualColumn = visualColumn; + insideFolding = true; + } + + public void onFoldingEnd() { + visualColumn += 3; // For '...' folding + foldingColumnDiff = visualColumn - logicalColumn; + insideFolding = false; + myFoldRegions.add(new MockFoldRegion(foldingStartOffset, offset)); + } + + public void onNewSymbol(char c) { + addData(); + if (insideFolding) { + myFoldedOffsets.add(offset); + onNonSoftWrapSymbol(c); + if (c == '\n') { + foldedLines++; + } + foldingColumnDiff = foldingStartVisualColumn - logicalColumn; + return; + } + + // Symbol inside soft wrap. + if (virtualSpace) { + mySoftWrapBuffer.append(c); + if (c == '\n') { + // Emulate the situation when the user works with a virtual space after document line end (add such virtual + // positions two symbols behind the end). + visualColumn++; + addData(true); + visualColumn++; + addData(true); + + visualLine++; + softWrapLinesOnCurrentLogical++; + visualColumn = 0; + softWrapSymbolsOnCurrentVisualLine = 0; + } + else { + visualColumn++; + softWrapSymbolsOnCurrentVisualLine++; + } + return; + } + + // Symbol outside soft wrap and folding. + onNonSoftWrapSymbol(c); + if (c == '\n') { + visualLine++; + visualColumn = 0; + softWrapLinesBeforeCurrentLogical += softWrapLinesOnCurrentLogical; + softWrapLinesOnCurrentLogical = 0; + softWrapSymbolsOnCurrentVisualLine = 0; + foldingColumnDiff = 0; + } + else { + visualColumn++; + } + } + + private void onNonSoftWrapSymbol(char c) { + document.append(c); + if (c == '\n') { + myLineRanges.add(new TextRange(logicalLineStartOffset, offset)); + + // Emulate the situation when the user works with a virtual space after document line end (add such virtual + // positions two symbols behind the end). + if (!insideFolding) { + visualColumn++; + logicalColumn++; + addData(true); + visualColumn++; + logicalColumn++; + addData(true); + } + + logicalLineStartOffset = offset + 1; + logicalLine++; + logicalColumn = 0; + } + else { + logicalColumn++; + } + offset++; + } + + private void addData() { + addData(virtualSpace); + } + + private void addData(boolean virtualSpace) { + myExpectedData.add(new DataEntry( + buildVisualPosition(), buildLogicalPosition(), offset, insideFolding && offset != foldingStartOffset, virtualSpace + )); + } + + private LogicalPosition buildLogicalPosition() { + //int softWrapColumnDiff + int softWrapColumnDiff = visualColumn - logicalColumn - foldingColumnDiff; + return new LogicalPosition( + logicalLine, logicalColumn, softWrapLinesBeforeCurrentLogical, softWrapLinesOnCurrentLogical, softWrapColumnDiff, + foldedLines, foldingColumnDiff + ); + } + + private VisualPosition buildVisualPosition() { + return new VisualPosition(visualLine, visualColumn); + } + } +} diff --git a/platform/testFramework/src/com/intellij/mock/MockFoldRegion.java b/platform/testFramework/src/com/intellij/mock/MockFoldRegion.java new file mode 100644 index 000000000000..44658d04c29b --- /dev/null +++ b/platform/testFramework/src/com/intellij/mock/MockFoldRegion.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2010 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.mock; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.FoldRegion; +import com.intellij.openapi.editor.FoldingGroup; +import com.intellij.openapi.util.UserDataHolderBase; +import org.jetbrains.annotations.NotNull; + +/** + * @author Denis Zhdanov + * @since Jun 30, 2010 1:55:53 PM + */ +public class MockFoldRegion extends UserDataHolderBase implements FoldRegion { + + private final int myStartOffset; + private final int myEndOffset; + + public MockFoldRegion(int startOffset, int endOffset) { + myStartOffset = startOffset; + myEndOffset = endOffset; + } + + @Override + public boolean isExpanded() { + return false; + } + + @Override + public void setExpanded(boolean expanded) { + } + + @NotNull + @Override + public String getPlaceholderText() { + return "..."; + } + + @Override + public Editor getEditor() { + throw new UnsupportedOperationException("FoldRegion.getEditor() is not implemented yet at " + getClass()); + } + + @Override + public FoldingGroup getGroup() { + throw new UnsupportedOperationException("FoldRegion.getGroup() is not implemented yet at " + getClass()); + } + + @NotNull + @Override + public Document getDocument() { + throw new UnsupportedOperationException("FoldRegion.getDocument() is not implemented yet at " + getClass()); + } + + @Override + public int getStartOffset() { + return myStartOffset; + } + + @Override + public int getEndOffset() { + return myEndOffset; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public void setGreedyToLeft(boolean greedy) { + throw new UnsupportedOperationException("FoldRegion.setGreedyToLeft() is not implemented yet at " + getClass()); + } + + @Override + public void setGreedyToRight(boolean greedy) { + throw new UnsupportedOperationException("FoldRegion.setGreedyToRight() is not implemented yet at " + getClass()); + } + + @Override + public boolean isGreedyToRight() { + throw new UnsupportedOperationException("FoldRegion.isGreedyToRight() is not implemented yet at " + getClass()); + } + + @Override + public boolean isGreedyToLeft() { + throw new UnsupportedOperationException("FoldRegion.isGreedyToLeft() is not implemented yet at " + getClass()); + } + + @Override + public String toString() { + return myStartOffset + "-" + myEndOffset; + } +}