diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java index f344935a9e18..b55ec25ffb18 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorView.java @@ -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() + + "]"; } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LogicalPositionCache.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LogicalPositionCache.java index 975059c75495..d9d744d089f2 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LogicalPositionCache.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LogicalPositionCache.java @@ -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 myLines = new ArrayList(); @@ -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); diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorImplTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorImplTest.java index 95fab8bce878..8801a7ec0e04 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorImplTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/EditorImplTest.java @@ -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("\n\t"); + delete(); + assertEquals(new LogicalPosition(0, 4), myEditor.offsetToLogicalPosition(1)); + } } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/LogicalPositionCacheStressTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/LogicalPositionCacheStressTest.java new file mode 100644 index 000000000000..29911cbdffa4 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/LogicalPositionCacheStressTest.java @@ -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 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]); + } + } + } +}