IDEA-53596 Soft wrap for editors

1. Tab processing with soft wraps is corrected;
2. Soft wraps offsets unit test engine is expanded in order to check tabs processing;
3. Tab-specific soft wraps mapping tests are added;
4. Minor refactorings of soft wrap processing code;
This commit is contained in:
Denis Zhdanov
2010-07-12 19:58:16 +04:00
parent f5d327247f
commit 23daf220f2
9 changed files with 305 additions and 115 deletions
@@ -140,7 +140,7 @@ public class LogicalPosition implements Comparable<LogicalPosition> {
@NonNls
public String toString() {
return "LogicalPosition: line=" + line + " column=" + column + "; visual position aware=" + visualPositionAware
+ " soft wrap: lines=" + (softWrapLinesBeforeCurrentLogicalLine + softWrapLinesOnCurrentLogicalLine) + " (before=" +
+ "; soft wrap: lines=" + (softWrapLinesBeforeCurrentLogicalLine + softWrapLinesOnCurrentLogicalLine) + " (before=" +
softWrapLinesBeforeCurrentLogicalLine + "; current=" + softWrapLinesOnCurrentLogicalLine
+ ") columns diff=" + softWrapColumnDiff + "; folding: lines = " + foldedLines + " columns diff=" + foldingColumnDiff;
}
@@ -15,6 +15,9 @@
*/
package com.intellij.openapi.editor.ex;
import com.intellij.openapi.editor.TextChange;
import org.jetbrains.annotations.NotNull;
/**
* Defines a contract for the callbacks for soft wraps management notifications (addition/removal).
*
@@ -24,10 +27,9 @@ package com.intellij.openapi.editor.ex;
public interface SoftWrapChangeListener {
/**
* This method is assumed to be called every time number of soft wraps registered for the target logical line(s)
* is changed (soft wraps are added or remove).
* This method is assumed to be called every new soft wrap is registered.
*
* @param changedLogicalLine logical line for which number of registered soft wraps is changed
* @param softWrap newly registered soft wrap
*/
void softWrapsStateChanged(int changedLogicalLine);
void softWrapAdded(@NotNull TextChange softWrap);
}
@@ -63,13 +63,20 @@ public class EditorUtil {
List<? extends TextChange> softWraps = editor.getSoftWrapModel().getSoftWrapsForLine(resultLogLine);
for (int i = 0; i < softWraps.size(); i++) {
TextChange softWrap = softWraps.get(i);
CharSequence text = document.getCharsSequence();
if (visualLinesToSkip <= 0) {
int result = editor.offsetToVisualPosition(softWrap.getStart() - 1).column;
// We need to add width of the next symbol because current result column points to the last symbol before the soft wrap.
return result + textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), result);
}
int softWrapLineFeeds = StringUtil.countNewLines(softWrap.getText());
if (softWrapLineFeeds < visualLinesToSkip) {
visualLinesToSkip -= softWrapLineFeeds;
continue;
}
// Target visual column is the one just before line feed introduced by the next line feed.
// Target visual column is located on the last visual line of the current soft wrap.
if (softWrapLineFeeds == visualLinesToSkip) {
if (i >= softWraps.size() - 1) {
return resVisEnd.column;
@@ -78,11 +85,11 @@ public class EditorUtil {
TextChange nextSoftWrap = softWraps.get(i + 1);
int result = editor.offsetToVisualPosition(nextSoftWrap.getStart() - 1).column;
// We need to add '1' because current column points to the last symbol before the next soft wrap;
result++;
// We need to add symbol width because current column points to the last symbol before the next soft wrap;
result += textWidthInColumns(editor, text, nextSoftWrap.getStart() - 1, nextSoftWrap.getStart(), result);
int lineFeedIndex = StringUtil.indexOf(nextSoftWrap.getText(), '\n');
result += calcColumnNumber(editor, nextSoftWrap.getText(), 0, lineFeedIndex);
result += textWidthInColumns(editor, nextSoftWrap.getText(), 0, lineFeedIndex, result);
return result;
}
@@ -103,7 +110,8 @@ public class EditorUtil {
}
}
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
// Target visual column is located on the last visual line of the current soft wrap.
result += textWidthInColumns(editor, text, softWrap.getStart() - 1, softWrap.getStart(), result);
result += calcColumnNumber(editor, softWrap.getText(), softWrapStartOffset, softWrapEndOffset);
return result;
}
@@ -298,6 +306,36 @@ public class EditorUtil {
return (nTabs + 1) * tabSize;
}
/**
* Allows to answer how many columns are used to represent tabulation symbols that is started at the given visual column
* at the given editor.
*
* @param visualColumn visual column where target tabulation symbol starts
* @param editor target editor where tabulation symbol is to be represented
* @return number of visual columns required to represent tabulation symbols that starts at the given column
*/
public static int tabWidthInColumns(@NotNull Editor editor, int visualColumn) {
if (!editor.getSettings().isWhitespacesShown()) {
return 1;
}
int tabSize = getTabSize(editor);
int tabsNumber = visualColumn / tabSize;
return (tabsNumber + 1) * tabSize - visualColumn;
}
public static int textWidthInColumns(@NotNull Editor editor, CharSequence text, int start, int end, int columnOffset) {
int result = 0;
for (int i = start; i < end; i++) {
if (text.charAt(i) == '\t') {
result += tabWidthInColumns(editor, columnOffset + result);
}
else {
result++;
}
}
return result;
}
/**
* Allows to answer what width in pixels is required to draw fragment of the given char array from <code>[start; end)</code> interval
* at the given editor.
@@ -312,9 +350,12 @@ public class EditorUtil {
* @param start offset within the given char array that points to target text start (inclusive)
* @param end offset within the given char array that points to target text end (exclusive)
* @param fontType font type to use for target text representation
* @param x <code>'x'</code> coordinate that should be used as a starting point for target text representation.
* It's necessity is implied by the fact that IDEA editor may represent tabulation symbols in any range
* from <code>[1; tab size]</code> (check {@link #nextTabStop(int, Editor)} for more details)
* @return width in pixels required for target text representation
*/
public static int textWidth(@NotNull Editor editor, char[] text, int start, int end, int fontType) {
public static int textWidth(@NotNull Editor editor, char[] text, int start, int end, int fontType, int x) {
int result = 0;
for (int i = start; i < end; i++) {
char c = text[i];
@@ -325,7 +366,7 @@ public class EditorUtil {
}
if (editor.getSettings().isWhitespacesShown()) {
result += getTabSize(editor) * getSpaceWidth(fontType, editor);
result += nextTabStop(x + result, editor) - result - x;
}
else {
result += getSpaceWidth(fontType, editor);
@@ -329,9 +329,10 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
mySoftWrapModel.addSoftWrapChangeListener(new SoftWrapChangeListener() {
@Override
public void softWrapsStateChanged(int changedLogicalLine) {
public void softWrapAdded(@NotNull TextChange softWrap) {
mySoftWrapsChanged = true;
mySizeContainer.update(changedLogicalLine, changedLogicalLine, changedLogicalLine);
int softWrapLine = myDocument.getLineNumber(softWrap.getStart());
mySizeContainer.update(softWrapLine, softWrapLine, softWrapLine);
}
});
@@ -1061,7 +1062,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
if (i >= 0) {
start = i + 1;
}
return new Point(EditorUtil.textWidth(this, softWrapChars, start, column + 1, Font.PLAIN), y);
return new Point(EditorUtil.textWidth(this, softWrapChars, start, column + 1, Font.PLAIN, 0), y);
}
break;
}
@@ -1712,7 +1713,8 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
myGutterComponent.repaint(0, clip.y, myGutterComponent.getWidth(), myGutterComponent.getHeight() - clip.y);
// Ask caret model to update visual caret position.
getCaretModel().moveToOffset(getCaretModel().getOffset());
//TODO den implement
//getCaretModel().moveToOffset(getCaretModel().getOffset());
}
}
@@ -4627,9 +4629,9 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
private int myMaxWidth = -1;
public synchronized void reset() {
int visLinesCount = getDocument().getLineCount();
myLineWidths = new TIntArrayList(visLinesCount + 300);
int[] values = new int[visLinesCount];
int lineCount = getDocument().getLineCount();
myLineWidths = new TIntArrayList(lineCount + 300);
int[] values = new int[lineCount];
Arrays.fill(values, -1);
myLineWidths.add(values);
myIsDirty = true;
@@ -43,7 +43,7 @@ import java.util.List;
*/
public class SoftWrapModelImpl implements SoftWrapModelEx {
private final SoftWrapDataMapper myDataAdjuster;
private final SoftWrapDataMapper myDataAdjuster;
private final SoftWrapsStorage myStorage;
private final SoftWrapPainter myPainter;
private final SoftWrapApplianceManager myApplianceManager;
@@ -54,7 +54,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx {
private int myActive;
public SoftWrapModelImpl(@NotNull EditorEx editor) {
this(editor, new SoftWrapsStorage(editor.getDocument()), new CompositeSoftWrapPainter(editor));
this(editor, new SoftWrapsStorage(), new CompositeSoftWrapPainter(editor));
}
public SoftWrapModelImpl(@NotNull final EditorEx editor, @NotNull SoftWrapsStorage storage, @NotNull SoftWrapPainter painter) {
@@ -278,7 +278,7 @@ public class SoftWrapModelImpl implements SoftWrapModelEx {
}
if (start < end) {
result += EditorUtil.textWidth(myEditor, chars, start, end, Font.PLAIN);
result += EditorUtil.textWidth(myEditor, chars, start, end, Font.PLAIN, 0);
}
return result;
@@ -214,7 +214,7 @@ public class DefaultSoftWrapApplianceManager implements SoftWrapApplianceManager
int x = myPainter.getMinDrawingWidth(SoftWrapDrawingType.BEFORE_SOFT_WRAP_LINE_FEED);
int prevSoftWrapOffset = start;
for (int i = start; i < end; i++) {
int symbolWidth = EditorUtil.textWidth(myEditor, text, i, i + 1, fontType);
int symbolWidth = EditorUtil.textWidth(myEditor, text, i, i + 1, fontType, x);
if (x + symbolWidth >= myVisibleAreaWidth) {
int offset = calculateSoftWrapOffset(text, i - 1, prevSoftWrapOffset, end);
if (offset >= end || offset <= prevSoftWrapOffset) {
@@ -236,7 +236,7 @@ public class SoftWrapDataMapper {
// Return eagerly if there are no soft wraps before the target offset on a line that contains it.
if (max >= softWraps.size() || softWraps.get(max).getStart() > offset) {
int column = toVisualColumnSymbolsNumber(chars, targetLineStartOffset, offset);
int column = toVisualColumnSymbolsNumber(chars, targetLineStartOffset, offset, 0);
LogicalPosition foldingUnawarePosition = new LogicalPosition(
rawLineStartLogicalPosition.line, column, softWrapIntroducedLinesBefore, 0, 0, 0, 0
);
@@ -280,8 +280,9 @@ public class SoftWrapDataMapper {
}
// Assuming that no line feed is contained before target offset on a line that holds it.
symbolsOnCurrentLogicalLine++;
symbolsOnCurrentVisibleLine++;
int columnsForSymbol = toVisualColumnSymbolsNumber(chars.charAt(i), symbolsOnCurrentVisibleLine);
symbolsOnCurrentLogicalLine += columnsForSymbol;
symbolsOnCurrentVisibleLine += columnsForSymbol;
}
LogicalPosition foldingUnawarePosition = new LogicalPosition(
@@ -304,8 +305,8 @@ public class SoftWrapDataMapper {
int foldColumnDiff = 0;
int softWrapColumnDiff = position.softWrapColumnDiff;
Document document = myEditor.getDocument();
CharSequence text = document.getCharsSequence();
int targetLine = document.getLineNumber(offset);
int lastFoldEndLogicalLine = -1;
for (FoldRegion foldRegion : foldingModel.getAllFoldRegions()) {
if (foldRegion.getStartOffset() >= offset) {
break;
@@ -320,53 +321,52 @@ public class SoftWrapDataMapper {
foldedLines += Math.min(targetLine, foldingEndLine) - foldingStartLine;
// Process situation when target offset is located inside the folded region.
if (offset >= foldRegion.getStartOffset() && offset < foldRegion.getEndOffset()) {
// Our purpose is to define folding data in order to point to the visual folding start.
int visualFoldingStartColumn = calculateVisualFoldingStartColumn(foldRegion);
foldColumnDiff = visualFoldingStartColumn - position.column - softWrapColumnDiff;
break;
}
if (foldingEndLine != position.line) {
continue;
}
// We know here that offset is at the same line where folding ends and is located after it. Hence, we process that as follows:
// 1. Check if the folding is single-line;
// 2.1. Process as follows if the folding is single-line:
// 3.1. Calculate column difference introduced by the folding;
// 2.2. Process as follows if the folding is multi-line:
// 3.2. Calculate visual column of folding start;
// 4.2. Calculate number of columns between target offset and folding end;
// 5.1. Calculate folding placeholder width in columns;
// 6.1. Calculate resulting offset visual column;
// 7.1. Calculate resulting folding column diff;
if (foldingStartLine == foldingEndLine) {
foldColumnDiff = toVisualColumnSymbolsNumber(foldRegion.getPlaceholderText())
- toVisualColumnSymbolsNumber(text, foldRegion.getStartOffset(), foldRegion.getEndOffset());
}
else {
int endOffsetOfLineWithFoldingEnd = document.getLineEndOffset(foldingEndLine);
int columnsBetweenFoldingEndAndOffset = toVisualColumnSymbolsNumber(text, foldRegion.getEndOffset(), endOffsetOfLineWithFoldingEnd);
LogicalPosition foldingEndLineEndPosition = myEditor.offsetToLogicalPosition(endOffsetOfLineWithFoldingEnd);
if (position.column > foldingEndLineEndPosition.column) {
columnsBetweenFoldingEndAndOffset += position.column - foldingEndLineEndPosition.column;
if (offset >= foldRegion.getStartOffset()) {
if (offset < foldRegion.getEndOffset()) {
// Our purpose is to define folding data in order to point to the visual folding start.
int visualFoldingStartColumn = calculateVisualFoldingStartColumn(foldRegion);
int diff = visualFoldingStartColumn - position.column - softWrapColumnDiff;
if (lastFoldEndLogicalLine == foldingStartLine) {
foldColumnDiff += diff;
}
else {
foldColumnDiff = diff;
}
return new LogicalPosition(
position.line, position.column, position.softWrapLinesBeforeCurrentLogicalLine, position.softWrapLinesOnCurrentLogicalLine,
softWrapColumnDiff, foldedLines, foldColumnDiff
);
}
int visualFoldingStartColumn = calculateVisualFoldingStartColumn(foldRegion);
int foldingPlaceholderWidth = toVisualColumnSymbolsNumber(foldRegion.getPlaceholderText());
int visual = columnsBetweenFoldingEndAndOffset + visualFoldingStartColumn + foldingPlaceholderWidth;
foldColumnDiff = visual - position.column;
break;
int diff = getFoldColumnDiff(foldRegion);
if (lastFoldEndLogicalLine == foldingStartLine) {
foldColumnDiff += diff;
}
else {
foldColumnDiff = diff;
}
lastFoldEndLogicalLine = foldingEndLine;
}
}
if (lastFoldEndLogicalLine != position.line) {
foldColumnDiff = 0;
}
return new LogicalPosition(
position.line, position.column, position.softWrapLinesBeforeCurrentLogicalLine, position.softWrapLinesOnCurrentLogicalLine,
softWrapColumnDiff, foldedLines, foldColumnDiff
);
}
private int getFoldColumnDiff(FoldRegion region) {
int visualFoldingStartColumn = calculateVisualFoldingStartColumn(region);
LogicalPosition foldEndLogical = myEditor.offsetToLogicalPosition(region.getEndOffset());
// Assuming that there is no tabulations symbols at placeholder text.
int foldingPlaceholderWidth = region.getPlaceholderText().length();
return visualFoldingStartColumn + foldingPlaceholderWidth - foldEndLogical.column;
}
private int calculateVisualFoldingStartColumn(FoldRegion region) {
Document document = myEditor.getDocument();
int foldingStartOffset = region.getStartOffset();
@@ -380,6 +380,7 @@ public class SoftWrapDataMapper {
List<TextChangeImpl> softWraps = myStorage.getSoftWraps();
int startOffsetOfVisualLineWithFoldingStart = logicalLineStartOffset;
int softWrapOffsetInColumns = 0;
for (; softWrapIndex < softWraps.size(); softWrapIndex++) {
TextChange softWrap = softWraps.get(softWrapIndex);
if (softWrap.getStart() >= foldingStartOffset) {
@@ -387,25 +388,46 @@ public class SoftWrapDataMapper {
}
startOffsetOfVisualLineWithFoldingStart = softWrap.getStart();
softWrapOffsetInColumns = numberOfSymbolsOnLastVisualLine(softWrap);
}
assert startOffsetOfVisualLineWithFoldingStart <= foldingStartOffset;
return toVisualColumnSymbolsNumber(document.getCharsSequence(), startOffsetOfVisualLineWithFoldingStart, foldingStartOffset);
return EditorUtil.textWidthInColumns(
myEditor, document.getCharsSequence(), startOffsetOfVisualLineWithFoldingStart, foldingStartOffset, softWrapOffsetInColumns
);
}
private int toVisualColumnSymbolsNumber(char c) {
private static int numberOfSymbolsOnLastVisualLine(TextChange textChange) {
int result = 0;
for (int i = textChange.getText().length() - 1; i >= 0; i--) {
if (i == '\n') {
return result;
}
else {
result++;
}
}
return result;
}
private int toVisualColumnSymbolsNumber(char c, int offsetInColumns) {
myCharBuffer.clear();
myCharBuffer.put(c);
myCharBuffer.flip();
return toVisualColumnSymbolsNumber(myCharBuffer, 0, 1);
return toVisualColumnSymbolsNumber(myCharBuffer, 0, 1, offsetInColumns);
}
private int toVisualColumnSymbolsNumber(CharSequence text) {
return toVisualColumnSymbolsNumber(text, 0, text.length());
}
private int toVisualColumnSymbolsNumber(CharSequence text, int start, int end) {
return EditorUtil.calcColumnNumber(myEditor, text, start, end);
private int toVisualColumnSymbolsNumber(CharSequence text, int start, int end, int offsetInColumns) {
int result = 0;
for (int i = start; i < end; i++) {
if (text.charAt(i) == '\t') {
result += EditorUtil.tabWidthInColumns(myEditor, offsetInColumns + result);
}
else {
result++;
}
}
return result;
}
private class Context {
@@ -420,6 +442,12 @@ public class SoftWrapDataMapper {
public int targetSoftWrapLines;
public int symbolsOnCurrentLogicalLine;
public int symbolsOnCurrentVisualLine;
/**
* There is a possible case that single tabulation symbols is shown in more than one visual column (IntelliJ editor is configured
* to show white spaces and tabulations and tab size is more than one). We keep track in number of such excessive visual
* columns used to show tabulations on a current visual line.
*/
public int excessiveTabColumns;
Context(LogicalPosition softWrapUnawareLogicalPosition, VisualPosition targetVisualPosition, int softWrapLinesBefore,
int softWrapLinesOnCurrentLineBeforeTargetSoftWrap, int visualLineBeforeSoftWrapAppliance, FoldingModel foldingModel)
@@ -449,13 +477,14 @@ public class SoftWrapDataMapper {
else {
targetSoftWrapLines++;
symbolsOnCurrentVisualLine = 0;
excessiveTabColumns = 0;
return null;
}
}
// Just update information about tracked symbols number if current visual line is too low.
if (targetVisualPosition.line > visualLineBeforeSoftWrapAppliance + targetSoftWrapLines) {
symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c);
symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c, symbolsOnCurrentVisualLine);
return null;
}
@@ -467,7 +496,7 @@ public class SoftWrapDataMapper {
}
// Process non-line feed inside soft wrap.
symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c);
symbolsOnCurrentVisualLine++; // Don't expect tabulation to be used inside soft wrap text.
if (targetVisualPosition.column <= symbolsOnCurrentVisualLine) {
return build();
}
@@ -493,8 +522,9 @@ public class SoftWrapDataMapper {
// Just update information about tracked symbols number if current visual line is too low.
if (targetVisualPosition.line > visualLineBeforeSoftWrapAppliance + targetSoftWrapLines) {
symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c);
symbolsOnCurrentLogicalLine++;
int columnsForSymbol = toVisualColumnSymbolsNumber(c, symbolsOnCurrentVisualLine);
symbolsOnCurrentVisualLine += columnsForSymbol;
symbolsOnCurrentLogicalLine += columnsForSymbol;
return null;
}
@@ -504,9 +534,14 @@ public class SoftWrapDataMapper {
return build();
}
symbolsOnCurrentVisualLine += toVisualColumnSymbolsNumber(c);
symbolsOnCurrentLogicalLine++;
int columnsForSymbol = toVisualColumnSymbolsNumber(c, symbolsOnCurrentVisualLine);
int diffInColumns = targetVisualPosition.column - symbolsOnCurrentVisualLine;
int incrementToUse = columnsForSymbol;
if (columnsForSymbol >= diffInColumns) {
incrementToUse = Math.min(columnsForSymbol, diffInColumns);
}
symbolsOnCurrentVisualLine += incrementToUse;
symbolsOnCurrentLogicalLine += incrementToUse;
if (targetVisualPosition.column <= symbolsOnCurrentVisualLine) {
return build();
@@ -15,7 +15,6 @@
*/
package com.intellij.openapi.editor.impl.softwrap;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.SoftWrapChangeListener;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -40,12 +39,6 @@ public class SoftWrapsStorage {
private final List<TextChangeImpl> myWrapsView = Collections.unmodifiableList(myWraps);
private final Set<SoftWrapChangeListener> myListeners = new CopyOnWriteArraySet<SoftWrapChangeListener>();
private final Document myDocument;
public SoftWrapsStorage(Document document) {
myDocument = document;
}
/**
* @return <code>true</code> if there is at least one soft wrap registered at the current storage; <code>false</code> otherwise
*/
@@ -114,9 +107,8 @@ public class SoftWrapsStorage {
i = -i - 1;
myWraps.add(i, softWrap);
int changedLogicalLine = myDocument.getLineNumber(softWrap.getStart());
for (SoftWrapChangeListener listener : myListeners) {
listener.softWrapsStateChanged(changedLogicalLine);
listener.softWrapAdded(softWrap);
}
return null;
}
@@ -1,6 +1,5 @@
package com.intellij.openapi.editor.impl.softwrap;
import com.intellij.idea.Bombed;
import com.intellij.mock.MockFoldRegion;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.ex.EditorEx;
@@ -9,16 +8,19 @@ 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.api.Invocation;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.lib.action.CustomAction;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author Denis Zhdanov
@@ -38,7 +40,7 @@ public class SoftWrapDataMapperTest {
// 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) {
if (o1.foldedSpace && o2.foldedSpace && logical1.column + logical1.foldingColumnDiff == logical2.foldingColumnDiff) {
return o1.foldedSpace ? 1 : -1;
}
return logical1.column - logical2.column;
@@ -48,13 +50,19 @@ public class SoftWrapDataMapperTest {
private static final Comparator<DataEntry> OFFSETS_COMPARATOR = new Comparator<DataEntry>() {
@Override
public int compare(DataEntry o1, DataEntry o2) {
if (o1.offset != o2.offset) {
return o1.offset - o2.offset;
}
// 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) {
if (o1.virtualSpace ^ o2.virtualSpace) {
return o1.virtualSpace ? 1 : -1;
}
return o1.offset - o2.offset;
if (o1.insideTab ^ o2.insideTab) {
return o1.insideTab ? 1 : -1;
}
return 0;
}
};
@@ -86,7 +94,7 @@ public class SoftWrapDataMapperTest {
myEditor = myMockery.mock(EditorEx.class);
myDocument = myMockery.mock(Document.class);
myStorage = new SoftWrapsStorage(myDocument);
myStorage = new SoftWrapsStorage();
myFoldingModel = myMockery.mock(FoldingModel.class);
final EditorSettings settings = myMockery.mock(EditorSettings.class);
final Project project = myMockery.mock(Project.class);
@@ -123,6 +131,7 @@ public class SoftWrapDataMapperTest {
allowing(myEditor).getSettings();will(returnValue(settings));
allowing(settings).isUseSoftWraps();will(returnValue(true));
allowing(settings).getTabSize(project);will(returnValue(TAB_SIZE));
allowing(settings).isWhitespacesShown();will(returnValue(true));
allowing(myEditor).getProject();will(returnValue(project));
// Folding.
@@ -176,7 +185,6 @@ public class SoftWrapDataMapperTest {
myMockery.assertIsSatisfied();
}
@Bombed(day = 12, month = Calendar.JULY)
@Test
public void softWrapHasSymbolBeforeFirstLineFeed() {
String document =
@@ -192,7 +200,6 @@ public class SoftWrapDataMapperTest {
test(document);
}
@Bombed(day = 12, month = Calendar.JULY)
@Test
public void multipleSoftWrappedLogicalLines() {
String document =
@@ -212,7 +219,6 @@ public class SoftWrapDataMapperTest {
test(document);
}
@Bombed(day = 12, month = Calendar.JULY)
@Test
public void softWrapAndFoldedLines() {
String document =
@@ -236,6 +242,70 @@ public class SoftWrapDataMapperTest {
test(document);
}
@Test
public void tabSymbolsBeforeSoftWrap() {
String document =
"class Test\t\t{<WRAP>\n" +
" </WRAP> \n}";
test(document);
}
@Test
public void tabSymbolsAfterSoftWrap() {
String document =
"class Test {<WRAP>\n" +
" </WRAP> \t\t\n" +
"}";
test(document);
}
@Test
public void multipleTabsAndSoftWraps() {
String document =
"public class \tTest {\n" +
" public void foo(int[] data) {\n" +
" bar(data[0], data[1],\t\t <WRAP>\n" +
" </WRAP>data[2], data[3], <WRAP>\t \t\n" +
" </WRAP>data[4], data[5],\t \t \n" +
" data[6], data[7],\t \t \n" +
" data[8], data[9],\t \t <WRAP>\n" +
" </WRAP>data[10], data[11], <WRAP>\t \t \n" +
" </WRAP>data[12],\t \t data[13]); \n" +
" }\n" +
" public void bar(int ... i) {\n" +
" }\n" +
"}";
test(document);
}
@Test
public void tabBeforeFolding() {
String document =
"class Test\t \t <FOLD>{\n" +
" </FOLD> \t\t\n" +
"}";
test(document);
}
@Test
public void multipleTabsAndFolding() {
String document =
"public class \tTest {\n" +
" public void foo(int[] data) {\n" +
" bar(data[0], data[1],\t\t <FOLD>\n" +
" \t \t </FOLD>data[2], data[3], <FOLD>\t \t\n" +
" \t \t </FOLD>data[4], data[5],\t \t \n" +
" data[6], data[7],\t \t \n" +
" data[8], data[9],\t \t <FOLD>\n" +
"\t \t </FOLD>data[10], data[11], <FOLD>\t \t \n" +
" \t \t </FOLD>data[12],\t \t data[13]); \n" +
" }\n" +
" public void bar(int ... i) {\n" +
" }\n" +
"}";
test(document);
}
private int getLineNumber(int offset) {
int line = 0;
for (TextRange range : myLineRanges) {
@@ -324,7 +394,7 @@ public class SoftWrapDataMapperTest {
LogicalPosition actualLogicalByVisual = myAdjuster.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)) {
if (!data.foldedSpace && !data.insideTab && !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'",
@@ -336,7 +406,7 @@ public class SoftWrapDataMapperTest {
LogicalPosition actualLogicalByOffset = myAdjuster.offsetToLogicalPosition(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)) {
if (!data.virtualSpace && !data.insideTab && !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'",
@@ -432,17 +502,19 @@ public class SoftWrapDataMapperTest {
public final int offset;
public final boolean foldedSpace;
public final boolean virtualSpace;
public final boolean insideTab;
DataEntry(VisualPosition visual, LogicalPosition logical, int offset, boolean foldedSpace) {
this(visual, logical, offset, foldedSpace, false);
this(visual, logical, offset, foldedSpace, false, false);
}
DataEntry(VisualPosition visual, LogicalPosition logical, int offset, boolean foldedSpace, boolean virtualSpace) {
DataEntry(VisualPosition visual, LogicalPosition logical, int offset, boolean foldedSpace, boolean virtualSpace, boolean insideTab) {
this.visual = visual;
this.logical = logical;
this.offset = offset;
this.foldedSpace = foldedSpace;
this.virtualSpace = virtualSpace;
this.insideTab = insideTab;
}
@Override
@@ -458,6 +530,7 @@ public class SoftWrapDataMapperTest {
boolean insideSoftWrap;
boolean insideFolding;
boolean insideTab;
int logicalLineStartOffset;
int logicalLine;
int logicalColumn;
@@ -473,6 +546,7 @@ public class SoftWrapDataMapperTest {
int foldingColumnDiff;
int foldedLines;
int offset;
int tabAnchorColumn;
public void onSoftWrapStart() {
softWrapStartOffset = offset;
@@ -482,7 +556,6 @@ public class SoftWrapDataMapperTest {
public void onSoftWrapEnd() {
myStorage.storeOrReplace(new TextChangeImpl(mySoftWrapBuffer.toString(), softWrapStartOffset));
mySoftWrapBuffer.setLength(0);
visualColumn++; // For the column reserved for soft wrap sign.
insideSoftWrap = false;
}
@@ -507,6 +580,29 @@ public class SoftWrapDataMapperTest {
onNonSoftWrapSymbol(c);
if (c == '\n') {
foldedLines++;
offset++;
tabAnchorColumn = 0;
}
else if (c == '\t') {
int tabsNumber = tabAnchorColumn / TAB_SIZE;
int tabWidthInColumns = ((tabsNumber + 1) * TAB_SIZE) - tabAnchorColumn;
// There is a possible case that single tabulation symbols is shown in more than one visual column at IntelliJ editor.
// We store data entry only for the first tab column without 'inside tab' flag then.
insideTab = true;
for (int i =tabWidthInColumns - 1; i > 0; i--) {
logicalColumn++;
addData(false);
}
insideTab = false;
logicalColumn++;
offset++;
tabAnchorColumn += tabWidthInColumns;
} else {
logicalColumn++;
offset++;
tabAnchorColumn++;
}
foldingColumnDiff = foldingStartVisualColumn - logicalColumn;
return;
@@ -525,12 +621,14 @@ public class SoftWrapDataMapperTest {
visualLine++;
softWrapLinesOnCurrentLogical++;
visualColumn = 0;
visualColumn = 1; // For the column reserved for soft wrap sign.
tabAnchorColumn = 1;
softWrapSymbolsOnCurrentVisualLine = 0;
}
else {
visualColumn++;
softWrapSymbolsOnCurrentVisualLine++;
tabAnchorColumn++;
}
return;
}
@@ -540,13 +638,37 @@ public class SoftWrapDataMapperTest {
if (c == '\n') {
visualLine++;
visualColumn = 0;
tabAnchorColumn = 0;
softWrapLinesBeforeCurrentLogical += softWrapLinesOnCurrentLogical;
softWrapLinesOnCurrentLogical = 0;
softWrapSymbolsOnCurrentVisualLine = 0;
foldingColumnDiff = 0;
offset++;
}
else if (c == '\t') {
int tabsNumber = tabAnchorColumn / TAB_SIZE;
int tabWidthInColumns = ((tabsNumber + 1) * TAB_SIZE) - tabAnchorColumn;
// There is a possible case that single tabulation symbols is shown in more than one visual column at IntelliJ editor.
// We store data entry only for the first tab column without 'inside tab' flag then.
insideTab = true;
for (int i = tabWidthInColumns - 1; i > 0; i--) {
visualColumn++;
logicalColumn++;
addData(false);
}
insideTab = false;
visualColumn++;
logicalColumn++;
offset++;
tabAnchorColumn += tabWidthInColumns;
}
else {
visualColumn++;
logicalColumn++;
offset++;
tabAnchorColumn++;
}
}
@@ -570,10 +692,6 @@ public class SoftWrapDataMapperTest {
logicalLine++;
logicalColumn = 0;
}
else {
logicalColumn++;
}
offset++;
}
private void addData() {
@@ -582,7 +700,7 @@ public class SoftWrapDataMapperTest {
private void addData(boolean virtualSpace) {
myExpectedData.add(new DataEntry(
buildVisualPosition(), buildLogicalPosition(), offset, insideFolding && offset != foldingStartOffset, virtualSpace
buildVisualPosition(), buildLogicalPosition(), offset, insideFolding && offset != foldingStartOffset, virtualSpace, insideTab
));
}