mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IDEA-150208 Text selection is shifted / wrong text selection
This commit is contained in:
@@ -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() +
|
||||
"]";
|
||||
}
|
||||
}
|
||||
|
||||
+50
-8
@@ -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);
|
||||
|
||||
+7
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+140
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user