diff --git a/java/java-tests/testSrc/com/intellij/editor/LineSetIncrementalUpdateTest.java b/java/java-tests/testSrc/com/intellij/editor/LineSetIncrementalUpdateTest.java index 1fb719f8f765..7fd258438a7b 100644 --- a/java/java-tests/testSrc/com/intellij/editor/LineSetIncrementalUpdateTest.java +++ b/java/java-tests/testSrc/com/intellij/editor/LineSetIncrementalUpdateTest.java @@ -24,8 +24,11 @@ package com.intellij.editor; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.impl.LineSet; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.testFramework.LightCodeInsightTestCase; +import com.intellij.testFramework.PlatformTestUtil; import org.jetbrains.annotations.NonNls; public class LineSetIncrementalUpdateTest extends LightCodeInsightTestCase { @@ -124,4 +127,18 @@ public class LineSetIncrementalUpdateTest extends LightCodeInsightTestCase { } }.execute().throwException(); } + + public void testTypingInLongLinePerformance() { + String longLine = StringUtil.repeat("a ", 200000); + PlatformTestUtil.startPerformanceTest("Document changes in a long line", 1000, () -> { + Document document = EditorFactory.getInstance().createDocument("a\n" + longLine + "" + longLine + "\n"); + WriteCommandAction.runWriteCommandAction(getProject(), () -> { + for (int i = 0; i < 1000; i++) { + int offset = i * 2 + longLine.length(); + assertEquals(1, document.getLineNumber(offset)); + document.insertString(offset, "b"); + } + }); + }).cpuBound().assertTiming(); + } } \ No newline at end of file diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java index b63988dba706..b4033a107c84 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/LineSet.java @@ -18,6 +18,7 @@ package com.intellij.openapi.editor.impl; import com.intellij.openapi.editor.ex.LineIterator; import com.intellij.openapi.util.text.LineTokenizer; import com.intellij.util.BitUtil; +import com.intellij.util.text.CharArrayUtil; import com.intellij.util.text.MergingCharSequence; import gnu.trove.TByteArrayList; import gnu.trove.TIntArrayList; @@ -65,11 +66,44 @@ public class LineSet{ } @NotNull - LineSet update(@NotNull CharSequence prevText, int _start, int _end, @NotNull CharSequence replacement, boolean wholeTextReplaced) { + LineSet update(@NotNull CharSequence prevText, int start, int end, @NotNull CharSequence replacement, boolean wholeTextReplaced) { if (myLength == 0) { return createLineSet(replacement, !wholeTextReplaced); } + LineSet result = isSingleLineChange(prevText, start, end, replacement) + ? updateInsideOneLine(findLineIndex(start), replacement.length() - (end - start)) + : genericUpdate(prevText, start, end, replacement); + + if (doTest) { + MergingCharSequence newText = new MergingCharSequence( + new MergingCharSequence(prevText.subSequence(0, start), replacement), + prevText.subSequence(end, prevText.length())); + result.checkEquals(createLineSet(newText)); + } + return wholeTextReplaced ? result.clearModificationFlags() : result; + } + + private boolean isSingleLineChange(@NotNull CharSequence prevText, int start, int end, @NotNull CharSequence replacement) { + if (start == 0 && end == myLength && replacement.length() == 0) return false; + + int startLine = findLineIndex(start); + return startLine == findLineIndex(end) && !CharArrayUtil.containLineBreaks(replacement) && !isLastEmptyLine(startLine); + } + + @NotNull + private LineSet updateInsideOneLine(int line, int lengthDelta) { + int[] starts = myStarts.clone(); + for (int i = line + 1; i < starts.length; i++) { + starts[i] += lengthDelta; + } + + byte[] flags = myFlags.clone(); + flags[line] |= MODIFIED_MASK; + return new LineSet(starts, flags, myLength + lengthDelta); + } + + private LineSet genericUpdate(CharSequence prevText, int _start, int _end, CharSequence replacement) { int startOffset = _start; if (replacement.length() > 0 && replacement.charAt(0) == '\n' && startOffset > 0 && prevText.charAt(startOffset - 1) == '\r') { startOffset--; @@ -90,14 +124,7 @@ public class LineSet{ prevText.subSequence(_end, endOffset)); LineSet patch = createLineSet(replacement, true); - LineSet applied = applyPatch(startOffset, endOffset, startLine, endLine, patch); - if (doTest) { - final MergingCharSequence newText = new MergingCharSequence( - new MergingCharSequence(prevText.subSequence(0, startOffset), replacement), - prevText.subSequence(endOffset, prevText.length())); - applied.checkEquals(createLineSet(newText)); - } - return wholeTextReplaced ? applied.clearModificationFlags() : applied; + return applyPatch(startOffset, endOffset, startLine, endLine, patch); } private void checkEquals(@NotNull LineSet fresh) {