diff --git a/java/java-tests/testSrc/com/intellij/java/editor/LineSetIncrementalUpdateTest.java b/java/java-tests/testSrc/com/intellij/java/editor/LineSetIncrementalUpdateTest.java deleted file mode 100644 index 7e9b25a8473f..000000000000 --- a/java/java-tests/testSrc/com/intellij/java/editor/LineSetIncrementalUpdateTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2000-2017 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.java.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 { - @NonNls private static final String STRING1 = "\naaa\n"; - @NonNls private static final String STRING2 = "\n \n"; - @NonNls private static final String STRING3 = " \n \n "; - @NonNls private static final String STRING4 = "\n "; - @NonNls private static final String STRING5 = " \n"; - @NonNls private static final String STRING6 = "\n"; - - public void testInsert() { - LineSet.setTestingMode(true); - try { - configureFromFileText("test.jsp",""); - doInsert(); - - configureFromFileText("test.jsp","aaa\nbbb\n"); - doInsert(); - } - finally { - LineSet.setTestingMode(false); - } - } - - public void testDelete() { - - try { - configureFromFileText("test.jsp","aaa\nbbb\n"); - doInsert(); - LineSet.setTestingMode(true); - - doDelete(); - LineSet.setTestingMode(false); - - configureFromFileText("test.jsp",""); - doInsert(); - LineSet.setTestingMode(true); - doDelete(); - LineSet.setTestingMode(false); - } - finally { - LineSet.setTestingMode(false); - } - } - - private static void doInsert() { - WriteCommandAction.writeCommandAction(getProject()).run(() -> { - Document document = myEditor.getDocument(); - document.insertString(myEditor.getCaretModel().getOffset(), STRING6); - document.insertString(myEditor.getCaretModel().getOffset(), STRING5); - document.insertString(myEditor.getCaretModel().getOffset(), STRING4); - document.insertString(myEditor.getCaretModel().getOffset(), STRING3); - document.insertString(myEditor.getCaretModel().getOffset(), STRING2); - document.insertString(myEditor.getCaretModel().getOffset(), STRING1); - }); - } - - private static void doDelete() { - WriteCommandAction.writeCommandAction(getProject()).run(() -> { - Document document = myEditor.getDocument(); - - document.deleteString( - myEditor.getCaretModel().getOffset(), - myEditor.getCaretModel().getOffset() + STRING1.length() - ); - - document.deleteString( - myEditor.getCaretModel().getOffset(), - myEditor.getCaretModel().getOffset() + STRING2.length() - ); - - document.deleteString( - myEditor.getCaretModel().getOffset(), - myEditor.getCaretModel().getOffset() + STRING3.length() - ); - - document.deleteString( - myEditor.getCaretModel().getOffset(), - myEditor.getCaretModel().getOffset() + STRING4.length() - ); - - document.deleteString( - myEditor.getCaretModel().getOffset(), - myEditor.getCaretModel().getOffset() + STRING5.length() - ); - - document.deleteString( - myEditor.getCaretModel().getOffset(), - myEditor.getCaretModel().getOffset() + STRING6.length() - ); - }); - } - - 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"); - } - }); - }).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 8399ae4ab1fa..0b25a4c33946 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 @@ -25,7 +25,6 @@ import com.intellij.util.text.MergingCharSequence; import gnu.trove.TByteArrayList; import gnu.trove.TIntArrayList; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.TestOnly; import java.util.Arrays; @@ -73,16 +72,31 @@ public class LineSet{ return createLineSet(replacement, !wholeTextReplaced); } + // if we're breaking or creating a '\r\n' pair, expand the changed range to include it fully + CharSequence newText = StringUtil.replaceSubSequence(prevText, start, end, replacement); + if (hasChar(prevText, start - 1, '\r') && + (hasChar(prevText, start, '\n') != hasChar(newText, start, '\n'))) { + replacement = new MergingCharSequence("\r", replacement); + start--; + } + + if (hasChar(prevText, end, '\n') && + (hasChar(prevText, end -1, '\r') != hasChar(newText, start + replacement.length() - 1, '\r'))) { + replacement = new MergingCharSequence(replacement, "\n"); + end++; + } + LineSet result = isSingleLineChange(start, end, replacement) ? updateInsideOneLine(findLineIndex(start), replacement.length() - (end - start)) : genericUpdate(prevText, start, end, replacement); - if (doTest) { - result.checkEquals(createLineSet(StringUtil.replaceSubSequence(prevText, start, end, replacement))); - } return wholeTextReplaced ? result.clearModificationFlags() : result; } + private static boolean hasChar(CharSequence s, int index, char c) { + return index >= 0 && index < s.length() && s.charAt(index) == c; + } + private boolean isSingleLineChange(int start, int end, @NotNull CharSequence replacement) { if (start == 0 && end == myLength && replacement.length() == 0) return false; @@ -103,19 +117,11 @@ public class LineSet{ } 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--; - } - int startLine = findLineIndex(startOffset); - startOffset = getLineStart(startLine); + int startLine = findLineIndex(_start); + int startOffset = getLineStart(startLine); - int endOffset = _end; - if (replacement.length() > 0 && replacement.charAt(replacement.length() - 1) == '\r' && endOffset < prevText.length() && prevText.charAt(endOffset) == '\n') { - endOffset++; - } - int endLine = findLineIndex(endOffset); - endOffset = getLineEnd(endLine); + int endLine = findLineIndex(_end); + int endOffset = getLineEnd(endLine); if (!isLastEmptyLine(endLine)) endLine++; if (startOffset < _start) { @@ -129,20 +135,6 @@ public class LineSet{ return applyPatch(startOffset, endOffset, startLine, endLine, patch); } - private void checkEquals(@NotNull LineSet fresh) { - if (getLineCount() != fresh.getLineCount()) { - throw new AssertionError(); - } - for (int i = 0; i < getLineCount(); i++) { - boolean start = getLineStart(i) != fresh.getLineStart(i); - boolean end = getLineEnd(i) != fresh.getLineEnd(i); - boolean sep = getSeparatorLength(i) != fresh.getSeparatorLength(i); - if (start || end || sep) { - throw new AssertionError(); - } - } - } - @NotNull private LineSet applyPatch(int startOffset, int endOffset, int startLine, int endLine, @NotNull LineSet patch) { int lineShift = patch.myStarts.length - (endLine - startLine); @@ -262,13 +254,6 @@ public class LineSet{ return myStarts.length + (isLastEmptyLine(myStarts.length) ? 1 : 0); } - @TestOnly - public static void setTestingMode(boolean testMode) { - doTest = testMode; - } - - private static boolean doTest; - int getLength() { return myLength; } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/LineSetIncrementalUpdateTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/LineSetIncrementalUpdateTest.java new file mode 100644 index 000000000000..cad3d2567249 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/LineSetIncrementalUpdateTest.java @@ -0,0 +1,66 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.openapi.editor.impl; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.testFramework.PlatformTestUtil; +import com.intellij.testFramework.UsefulTestCase; +import org.jetbrains.jetCheck.Generator; +import org.jetbrains.jetCheck.PropertyChecker; + +public class LineSetIncrementalUpdateTest extends UsefulTestCase { + public void testFuzzUpdate() { + PropertyChecker.customized() + .withIterationCount(1_000) + .checkScenarios(() -> env -> { + Generator strings = Generator.stringsOf("a \n\r"); + String initialText = env.generateValue(strings, null); + int start = env.generateValue(Generator.integers(0, initialText.length()), null); + int end = env.generateValue(Generator.integers(start, initialText.length()), null); + String replacement = env.generateValue(strings, null); + + env.logMessage("checkIncrementalUpdate(\"" + StringUtil.escapeStringCharacters(initialText) + + "\", " + start + ", " + end + ", \"" + StringUtil.escapeStringCharacters(replacement) + "\")"); + + checkIncrementalUpdate(initialText, start, end, replacement); + }); + } + + public void testSlashRIssues() { + checkIncrementalUpdate("\n\r", 0, 0, ""); + checkIncrementalUpdate("\r\n", 0, 1, ""); + checkIncrementalUpdate("\r", 0, 0, "\r"); + } + + public void testClearSingleLineEnd() { + checkIncrementalUpdate("\n", 0, 1, ""); + } + + private static void checkIncrementalUpdate(String initialText, int start, int end, String replacement) { + CharSequence newText = StringUtil.replaceSubSequence(initialText, start, end, replacement); + + LineSet initial = LineSet.createLineSet(initialText); + LineSet updated = initial.update(initialText, start, end, replacement, false); + LineSet fresh = LineSet.createLineSet(newText); + + assertEquals("line count", fresh.getLineCount(), updated.getLineCount()); + for (int i = 0; i < updated.getLineCount(); i++) { + assertEquals("line start " + i, fresh.getLineStart(i), updated.getLineStart(i)); + assertEquals("line end " + i, fresh.getLineEnd(i), updated.getLineEnd(i)); + assertEquals("line feed length " + i, fresh.getSeparatorLength(i), updated.getSeparatorLength(i)); + } + } + + public void testTypingInLongLinePerformance() { + String longLine = StringUtil.repeat("a ", 200000); + PlatformTestUtil.startPerformanceTest("Document changes in a long line", 1000, () -> { + Document document = new DocumentImpl("a\n" + longLine + "" + longLine + "\n", true); + for (int i = 0; i < 1000; i++) { + int offset = i * 2 + longLine.length(); + assertEquals(1, document.getLineNumber(offset)); + document.insertString(offset, "b"); + } + }).assertTiming(); + } + +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index c230655a28fd..7e1fc491a111 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -37,11 +37,15 @@ public class StringUtil extends StringUtilRt { private static final Pattern EOL_SPLIT_PATTERN_WITH_EMPTY = Pattern.compile(" *(\r|\n|\r\n) *"); private static final Pattern EOL_SPLIT_DONT_TRIM_PATTERN = Pattern.compile("(\r|\n|\r\n)+"); + /** + * @return a lightweight CharSequence which results from replacing {@code [start, end)} range in the {@code charSeq} with {@code replacement}. + * Works in O(1), but retains references to the passed char sequences, so please use something else if you want them to be garbage-collected. + */ @NotNull public static MergingCharSequence replaceSubSequence(@NotNull CharSequence charSeq, int start, int end, @NotNull CharSequence replacement) { return new MergingCharSequence( - new MergingCharSequence(charSeq.subSequence(0, start), replacement), - charSeq.subSequence(end, charSeq.length())); + new MergingCharSequence(new CharSequenceSubSequence(charSeq, 0, start), replacement), + new CharSequenceSubSequence(charSeq, end, charSeq.length())); } private static class MyHtml2Text extends HTMLEditorKit.ParserCallback {