LineSet: move test-only code into the test (replaced with a property-based one), fix found \r treatment bugs

This commit is contained in:
peter
2019-03-01 19:36:21 +01:00
parent 85ff3d541f
commit 0c1aaccc3c
4 changed files with 94 additions and 171 deletions
@@ -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","<caret>");
doInsert();
configureFromFileText("test.jsp","aaa\nb<caret>bb\n");
doInsert();
}
finally {
LineSet.setTestingMode(false);
}
}
public void testDelete() {
try {
configureFromFileText("test.jsp","aaa\n<caret>bbb\n");
doInsert();
LineSet.setTestingMode(true);
doDelete();
LineSet.setTestingMode(false);
configureFromFileText("test.jsp","<caret>");
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 + "<caret>" + 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();
}
}
@@ -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;
}
@@ -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<String> 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 + "<caret>" + 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();
}
}
@@ -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 {