From 23daf220f248458013c103b3f5e191a5ff366b70 Mon Sep 17 00:00:00 2001 From: Denis Zhdanov Date: Mon, 12 Jul 2010 19:57:42 +0400 Subject: [PATCH] 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; --- .../openapi/editor/LogicalPosition.java | 2 +- .../editor/ex/SoftWrapChangeListener.java | 10 +- .../openapi/editor/ex/util/EditorUtil.java | 55 +++++- .../openapi/editor/impl/EditorImpl.java | 16 +- .../editor/impl/SoftWrapModelImpl.java | 6 +- .../DefaultSoftWrapApplianceManager.java | 2 +- .../impl/softwrap/SoftWrapDataMapper.java | 149 +++++++++------ .../impl/softwrap/SoftWrapsStorage.java | 10 +- .../impl/softwrap/SoftWrapDataMapperTest.java | 170 +++++++++++++++--- 9 files changed, 305 insertions(+), 115 deletions(-) diff --git a/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java b/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java index 8b917ca98508..04b353d7792d 100644 --- a/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java +++ b/platform/platform-api/src/com/intellij/openapi/editor/LogicalPosition.java @@ -140,7 +140,7 @@ public class LogicalPosition implements Comparable { @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; } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/SoftWrapChangeListener.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/SoftWrapChangeListener.java index 8a4d17f3e6a2..281334ff0b52 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/SoftWrapChangeListener.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/SoftWrapChangeListener.java @@ -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); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java b/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java index b6dc6a097090..e0e517dbbad9 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/ex/util/EditorUtil.java @@ -63,13 +63,20 @@ public class EditorUtil { List 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 [start; end) 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 'x' 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 [1; tab size] (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); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index b935c71b4c0d..6a64a132032b 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -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; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java index de8cbb67c426..f0fdd6eda822 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/SoftWrapModelImpl.java @@ -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; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/DefaultSoftWrapApplianceManager.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/DefaultSoftWrapApplianceManager.java index 63778df5f3f7..36b90dd4f12a 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/DefaultSoftWrapApplianceManager.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/DefaultSoftWrapApplianceManager.java @@ -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) { diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapper.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapper.java index 9281f6e5621d..96a840e27902 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapper.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapper.java @@ -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 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(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java index 5c325f5a7285..627a5ed6ae4e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/softwrap/SoftWrapsStorage.java @@ -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 myWrapsView = Collections.unmodifiableList(myWraps); private final Set myListeners = new CopyOnWriteArraySet(); - private final Document myDocument; - - public SoftWrapsStorage(Document document) { - myDocument = document; - } - /** * @return true if there is at least one soft wrap registered at the current storage; false 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; } diff --git a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapperTest.java b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapperTest.java index 705f55786f1f..016713112e21 100644 --- a/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapperTest.java +++ b/platform/platform-impl/testSrc/com/intellij/openapi/editor/impl/softwrap/SoftWrapDataMapperTest.java @@ -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 OFFSETS_COMPARATOR = new Comparator() { @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{\n" + + " \n}"; + test(document); + } + + @Test + public void tabSymbolsAfterSoftWrap() { + String document = + "class Test {\n" + + " \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 \n" + + " data[2], data[3], \t \t\n" + + " data[4], data[5],\t \t \n" + + " data[6], data[7],\t \t \n" + + " data[8], data[9],\t \t \n" + + " data[10], data[11], \t \t \n" + + " 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 {\n" + + " \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 \n" + + " \t \t data[2], data[3], \t \t\n" + + " \t \t data[4], data[5],\t \t \n" + + " data[6], data[7],\t \t \n" + + " data[8], data[9],\t \t \n" + + "\t \t data[10], data[11], \t \t \n" + + " \t \t 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 )); }