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);
This commit is contained in:
Denis Zhdanov
2010-07-01 16:10:15 +04:00
parent 34d9ae68a0
commit e6406586d3
16 changed files with 1221 additions and 205 deletions
@@ -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();
}
+1
View File
@@ -4,6 +4,7 @@
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
@@ -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<TextChange> 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;
}
@@ -44,27 +44,25 @@ public class LogicalPosition implements Comparable<LogicalPosition> {
/**
* 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.
* <p/>
* 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<LogicalPosition> {
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<LogicalPosition> {
* @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<LogicalPosition> {
@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;
}
}
@@ -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 <code>true</code> if given visual position points to soft wrap-introduced virtual space;
* <code>false</code> otherwise
*/
boolean isInsideSoftWrap(@NotNull VisualPosition position);
}
@@ -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));
}
}
}
@@ -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);
}
@@ -99,6 +99,8 @@ public interface EditorEx extends Editor {
void setRendererMode(boolean isRendererMode);
boolean isUnderRepainting();
void setFile(VirtualFile vFile);
DataContext getDataContext();
@@ -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;
}
@@ -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) {
@@ -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);
}
@@ -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<TextChange> SOFT_WRAPS_BY_OFFSET_COMPARATOR = new Comparator<TextChange>() {
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.
* <p/>
* The general idea is to do the following:
* <ul>
@@ -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<TextChange> myWraps = new ArrayList<TextChange>();
private final List<TextChange> 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<TextChange> softWraps = myStorage.getSoftWraps();
List<TextChange> 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. <code>'Target'</code> 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; <code>'-(negative value) - 1'</code> 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; <code>null</code> 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 <code>[start; end)</code> 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<TextChange> getRegisteredSoftWraps() {
return myWrapsView;
return myStorage.getSoftWraps();
}
/**
* Drops information about registered soft wraps if they are marked as <code>'dirty'</code>.
* //TODO den add doc about editor repainting
* <p/>
* 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<TextChange> 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<TextChange> 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<TextChange> 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<TextChange> 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<TextChange> 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<TextChange> 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<TextChange> modified = myWraps.subList(softWrapIndex, myWraps.size());
List<TextChange> toModify = new ArrayList<TextChange>(modified);
modified.clear();
List<TextChange> toModify = new ArrayList<TextChange>(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);
}
@@ -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.
* <p/>
* Not thread-safe.
*
* @author Denis Zhdanov
* @since Jun 29, 2010 3:04:20 PM
*/
public class SoftWrapsStorage {
private static final Comparator<TextChange> SOFT_WRAPS_BY_OFFSET_COMPARATOR = new Comparator<TextChange>() {
public int compare(TextChange c1, TextChange c2) {
return c1.getStart() - c2.getEnd();
}
};
private final List<TextChange> myWraps = new ArrayList<TextChange>();
private final List<TextChange> myWrapsView = Collections.unmodifiableList(myWraps);
/**
* @return <code>true</code> if there is at least one soft wrap registered at the current storage; <code>false</code> 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<TextChange> getSoftWraps() {
return myWrapsView;
}
/**
* Tries to find index of the target soft wrap stored at {@link #myWraps} collection. <code>'Target'</code> 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; <code>'-(negative value) - 1'</code> 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; <code>null</code> 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; <code>null</code> 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();
}
}
@@ -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;
}
}
@@ -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<DataEntry> LOGICAL_POSITIONS_COMPARATOR = new Comparator<DataEntry>() {
@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<DataEntry> OFFSETS_COMPARATOR = new Comparator<DataEntry>() {
@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 = "<WRAP>";
private static final String SOFT_WRAP_END_MARKER = "</WRAP>";
private static final String FOLDING_START_MARKER = "<FOLD>";
private static final String FOLDING_END_MARKER = "</FOLD>";
private static final int TAB_SIZE = 4;
/** Holds expected mappings between visual and logical positions and offset. */
private final List<DataEntry> myExpectedData = new ArrayList<DataEntry>();
private final List<TextRange> myLineRanges = new ArrayList<TextRange>();
/** Holds document offsets that are considered to be folded. */
private final TIntHashSet myFoldedOffsets = new TIntHashSet();
private final List<FoldRegion> myFoldRegions = new ArrayList<FoldRegion>();
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], <WRAP>\n" +
" </WRAP>data[4], data[5], data[6], <WRAP> \n" +
" </WRAP>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], <WRAP>\n" +
" </WRAP>data[2], data[3], <WRAP> \n" +
" </WRAP>data[4], data[5], \n" +
" data[6], data[7], \n" +
" data[8], data[9], <WRAP>\n" +
" </WRAP>data[10], data[11], <WRAP> \n" +
" </WRAP>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], <WRAP>\n" +
" </WRAP>data[2], data[3], <WRAP> \n" +
" </WRAP>data[4], data[5], \n" +
" data[6], data[7], \n" +
" <FOLD>data[8], data[9], \n" +
" data[10], data[11], \n" +
" data[12], data[13], </FOLD>\n" +
" data[14], data[15], \n" +
" data[16], data[17], <WRAP> \n" +
" </WRAP>data[18], data[19], <WRAP> \n" +
" </WRAP>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<DataEntry> 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);
}
}
}
@@ -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;
}
}