IDEA-150208 Text selection is shifted / wrong text selection

This commit is contained in:
Dmitry Batrak
2016-01-19 17:18:24 +03:00
parent 4409e96cd2
commit 414390b541
4 changed files with 200 additions and 10 deletions
@@ -471,6 +471,8 @@ public class EditorView implements TextDrawingCallback, Disposable, Dumpable {
", char height: " + myCharHeight +
", max char width: " + myMaxCharWidth +
", tab size: " + myTabSize +
" ,size manager: " + mySizeManager.dumpState() + "]";
" ,size manager: " + mySizeManager.dumpState() +
" ,logical position cache: " + myLogicalPositionCache.dumpState() +
"]";
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
* Copyright 2000-2016 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.
@@ -15,6 +15,7 @@
*/
package com.intellij.openapi.editor.impl.view;
import com.intellij.diagnostic.Dumpable;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.LogicalPosition;
@@ -33,7 +34,7 @@ import java.util.Collections;
* Requests for conversion can be made from under read action, document changes and cache invalidation should be done in EDT.
*/
@SuppressWarnings("SynchronizeOnThis")
class LogicalPositionCache implements PrioritizedDocumentListener, Disposable {
class LogicalPositionCache implements PrioritizedDocumentListener, Disposable, Dumpable {
private final Document myDocument;
private final EditorView myView;
private ArrayList<LineData> myLines = new ArrayList<LineData>();
@@ -68,7 +69,7 @@ class LogicalPositionCache implements PrioritizedDocumentListener, Disposable {
int oldTabSize = myTabSize;
myTabSize = myView.getTabSize();
if (force || oldTabSize != myTabSize) {
invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1, true);
invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1, !force && myLines.size() == myDocument.getLineCount());
}
}
@@ -103,13 +104,23 @@ class LogicalPositionCache implements PrioritizedDocumentListener, Disposable {
private synchronized void invalidateLines(int startLine, int oldEndLine, int newEndLine, boolean preserveTrivialLines) {
checkDisposed();
int endLine = Math.min(oldEndLine, newEndLine);
for (int line = startLine; line <= endLine; line++) {
LineData data = myLines.get(line);
if (data != null && (!preserveTrivialLines || data.columnCache != null)) myLines.set(line, null);
if (preserveTrivialLines) {
for (int line = startLine; line <= oldEndLine; line++) {
LineData data = myLines.get(line);
if (data == null || data.columnCache != null) {
preserveTrivialLines = false;
break;
}
}
}
if (!preserveTrivialLines) {
int endLine = Math.min(oldEndLine, newEndLine);
for (int line = startLine; line <= endLine; line++) {
myLines.set(line, null);
}
}
if (oldEndLine < newEndLine) {
myLines.addAll(oldEndLine + 1, Collections.nCopies(newEndLine - oldEndLine, (LineData)null));
myLines.addAll(oldEndLine + 1, Collections.nCopies(newEndLine - oldEndLine, preserveTrivialLines ? LineData.TRIVIAL : null));
} else if (oldEndLine > newEndLine) {
myLines.subList(newEndLine + 1, oldEndLine + 1).clear();
}
@@ -134,12 +145,43 @@ class LogicalPositionCache implements PrioritizedDocumentListener, Disposable {
private void checkDisposed() {
if (myLines == null) myView.getEditor().throwDisposalError("Editor is already disposed");
}
void validate() {
int lineCount = myDocument.getLineCount();
int cacheSize = myLines.size();
if (cacheSize != lineCount) throw new IllegalStateException("Line count: " + lineCount + ", cache size: " + cacheSize);
int tabSize = myView.getTabSize();
for (int i = 0; i < cacheSize; i++) {
LineData data = myLines.get(i);
if (data != null) {
LineData actual = new LineData(myDocument, i, tabSize);
if (!Arrays.equals(data.columnCache, actual.columnCache)) throw new IllegalStateException("Wrong cache state at line " + i);
}
}
}
@NotNull
@Override
public String dumpState() {
try {
validate();
return "valid";
}
catch (Exception e) {
return "invalid (" + e.getMessage() + ")";
}
}
private static class LineData {
private static final LineData TRIVIAL = new LineData();
private static final int CACHE_FREQUENCY = 1024; // logical column will be cached for each CACHE_FREQUENCY-th character on the line
private final int[] columnCache;
private LineData() {
columnCache = null;
}
private LineData(@NotNull Document document, int line, int tabSize) {
int start = document.getLineStartOffset(line);
int end = document.getLineEndOffset(line);
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2015 JetBrains s.r.o.
* Copyright 2000-2016 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.
@@ -239,4 +239,10 @@ public class EditorImplTest extends AbstractEditorTest {
((EditorEx)myEditor).setPrefixTextAndAttributes(">", new TextAttributes());
assertEquals(new VisualPosition(0, 0), myEditor.xyToVisualPosition(new Point(1, 0)));
}
public void testLogicalPositionCacheInvalidationAfterLineDeletion() throws Exception {
initText("<caret>\n\t");
delete();
assertEquals(new LogicalPosition(0, 4), myEditor.offsetToLogicalPosition(1));
}
}
@@ -0,0 +1,140 @@
/*
* Copyright 2000-2016 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.openapi.editor.impl.view;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.impl.AbstractEditorTest;
import com.intellij.openapi.editor.impl.EditorViewAccessor;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
public class LogicalPositionCacheStressTest extends AbstractEditorTest {
private static final int ITERATIONS = 10000;
private static final Long SEED_OVERRIDE = null; // set non-null value to run with a specific seed
private static final List<? extends Action> ourActions = Arrays.asList(new AddText(),
new RemoveText(),
new ReplaceText(),
new MoveText());
private final Random myRandom = new Random() {{
//noinspection ConstantConditions
setSeed(mySeed = (SEED_OVERRIDE == null ? nextLong() : SEED_OVERRIDE));
}};
private long mySeed;
public void testRandomActions() {
int i = 0;
try {
initText("");
for (i = 1; i <= ITERATIONS; i++) {
doRandomAction();
checkConsistency(myEditor);
}
}
catch (Throwable t) {
String message = "Failed when run with seed=" + mySeed + " in iteration " + i;
System.out.println(message);
throw new RuntimeException(message, t);
}
}
private void doRandomAction() {
ourActions.get(myRandom.nextInt(ourActions.size())).perform(myEditor, myRandom);
}
protected void checkConsistency(Editor editor) {
checkLogicalPositionCache(editor);
}
private static void checkLogicalPositionCache(Editor editor) {
EditorViewAccessor.getView(editor).getLogicalPositionCache().validate();
}
private static CharSequence generateText(Random random) {
int textLength = random.nextInt(10);
StringBuilder b = new StringBuilder();
for (int i = 0; i < textLength; i++) {
switch (random.nextInt(5)) {
case 0: b.append('\t'); break;
case 1: b.append('\n'); break;
default: b.append(' ');
}
}
return b;
}
interface Action {
void perform(Editor editor, Random random);
}
private static class AddText implements Action {
@Override
public void perform(Editor editor, Random random) {
Document document = editor.getDocument();
int offset = random.nextInt(document.getTextLength() + 1);
CharSequence text = generateText(random);
document.insertString(offset, text);
}
}
private static class RemoveText implements Action {
@Override
public void perform(Editor editor, Random random) {
Document document = editor.getDocument();
int textLength = document.getTextLength();
if (textLength <= 0) return;
int from = random.nextInt(textLength + 1);
int to = random.nextInt(textLength + 1);
document.deleteString(Math.min(from, to), Math.max(from, to));
}
}
private static class ReplaceText implements Action {
@Override
public void perform(Editor editor, Random random) {
Document document = editor.getDocument();
int textLength = document.getTextLength();
if (textLength <= 0) return;
int from = random.nextInt(textLength + 1);
int to = random.nextInt(textLength + 1);
CharSequence text = generateText(random);
document.replaceString(Math.min(from, to), Math.max(from, to), text);
}
}
private static class MoveText implements Action {
@Override
public void perform(Editor editor, Random random) {
Document document = editor.getDocument();
int textLength = document.getTextLength();
if (textLength <= 0) return;
int[] offsets = new int[] {random.nextInt(textLength + 1), random.nextInt(textLength + 1), random.nextInt(textLength + 1)};
Arrays.sort(offsets);
if (offsets[0] == offsets[1] || offsets[1] == offsets[2]) return;
if (random.nextBoolean()) {
((DocumentEx)document).moveText(offsets[0], offsets[1], offsets[2]);
}
else {
((DocumentEx)document).moveText(offsets[1], offsets[2], offsets[0]);
}
}
}
}