/* * 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.diagnostic.Dumpable; import com.intellij.openapi.Disposable; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.PrioritizedDocumentListener; import com.intellij.openapi.editor.impl.EditorDocumentPriorities; import com.intellij.util.DocumentUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; /** * Caches information allowing faster offset<->logicalPosition conversions even for long lines. * 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, Dumpable { private final Document myDocument; private final EditorView myView; private ArrayList<LineData> myLines = new ArrayList<>(); private int myTabSize = -1; private int myDocumentChangeOldEndLine; // application's read-write lock should guarantee that writes to this field (happening under write action) // will be visible for reads (happening under read action) @SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized") private boolean myUpdateInProgress; LogicalPositionCache(EditorView view) { myView = view; myDocument = view.getEditor().getDocument(); myDocument.addDocumentListener(this, this); } @Override public int getPriority() { return EditorDocumentPriorities.LOGICAL_POSITION_CACHE; } @Override public void beforeDocumentChange(DocumentEvent event) { myUpdateInProgress = true; myDocumentChangeOldEndLine = getAdjustedLineNumber(event.getOffset() + event.getOldLength()); } @Override public void documentChanged(DocumentEvent event) { int startLine = myDocument.getLineNumber(event.getOffset()); int newEndLine = getAdjustedLineNumber(event.getOffset() + event.getNewLength()); invalidateLines(startLine, myDocumentChangeOldEndLine, newEndLine, isSimpleText(event.getNewFragment())); myUpdateInProgress = false; } // text for which offset<->logicalColumn conversion is trivial private static boolean isSimpleText(@NotNull CharSequence text) { for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c == '\t' || c >= Character.MIN_SURROGATE && c <= Character.MAX_SURROGATE) return false; } return true; } synchronized void reset(boolean force) { checkDisposed(); int oldTabSize = myTabSize; myTabSize = myView.getTabSize(); if (force || oldTabSize != myTabSize) { invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1, !force && myLines.size() == myDocument.getLineCount()); } } @NotNull synchronized LogicalPosition offsetToLogicalPosition(int offset) { if (myUpdateInProgress) throw new IllegalStateException(); int textLength = myDocument.getTextLength(); if (offset <= 0 || textLength == 0) { return new LogicalPosition(0, 0); } offset = Math.min(offset, textLength); int line = myDocument.getLineNumber(offset); LineData lineData = getLineInfo(line); return new LogicalPosition(line, lineData.offsetToLogicalColumn(myDocument, line, myTabSize, offset)); } synchronized int offsetToLogicalColumn(int line, int intraLineOffset) { if (myUpdateInProgress) throw new IllegalStateException(); if (line < 0 || line >= myDocument.getLineCount()) return 0; LineData lineData = getLineInfo(line); return lineData.offsetToLogicalColumn(myDocument, line, myTabSize, myDocument.getLineStartOffset(line) + intraLineOffset); } synchronized int logicalPositionToOffset(@NotNull LogicalPosition pos) { int line = pos.line; int column = pos.column; if (line >= myDocument.getLineCount()) return myDocument.getTextLength(); if (myUpdateInProgress) { // direct calculation when we cannot use cache // (use case - com.intellij.openapi.editor.impl.CaretImpl.PositionMarker.changedUpdateImpl()) int lineStartOffset = myDocument.getLineStartOffset(line); int lineEndOffset = myDocument.getLineEndOffset(line); return calcOffset(myDocument, column, 0, lineStartOffset, lineEndOffset, myTabSize); } LineData lineData = getLineInfo(line); return lineData.logicalColumnToOffset(myDocument, line, myTabSize, column); } private static int calcOffset(@NotNull Document document, int column, int startColumn, int startOffset, int endOffset, int tabSize) { int currentColumn = startColumn; CharSequence text = document.getImmutableCharSequence(); for (int i = startOffset; i < endOffset; i++) { if (text.charAt(i) == '\t') { currentColumn = (currentColumn / tabSize + 1) * tabSize; } else if (DocumentUtil.isSurrogatePair(document, i)) { if (currentColumn == column) return i; } else { currentColumn++; } if (currentColumn > column) return i; } return endOffset; } static int calcColumn(@NotNull CharSequence text, int startOffset, int startColumn, int offset, int tabSize) { int column = startColumn; for (int i = startOffset; i < offset; i++) { char c = text.charAt(i); if (c == '\t') { column = (column / tabSize + 1) * tabSize; } else if (i + 1 >= text.length() || !Character.isHighSurrogate(c) || !Character.isLowSurrogate(text.charAt(i + 1))) { column++; } } return column; } private int getAdjustedLineNumber(int offset) { return myDocument.getTextLength() == 0 ? -1 : myDocument.getLineNumber(offset); } private synchronized void invalidateLines(int startLine, int oldEndLine, int newEndLine, boolean preserveTrivialLines) { checkDisposed(); 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, preserveTrivialLines ? LineData.TRIVIAL : null)); } else if (oldEndLine > newEndLine) { myLines.subList(newEndLine + 1, oldEndLine + 1).clear(); } } @NotNull private LineData getLineInfo(int line) { checkDisposed(); LineData result = myLines.get(line); if (result == null) { result = LineData.create(myDocument, line, myTabSize); myLines.set(line, result); } return result; } @Override public synchronized void dispose() { myLines = null; } private void checkDisposed() { if (myLines == null) myView.getEditor().throwDisposalError("Editor is already disposed"); } synchronized void validateState() { 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 = LineData.create(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 { validateState(); return "valid"; } catch (Exception e) { return "invalid (" + e.getMessage() + ")"; } } private static class LineData { private static final LineData TRIVIAL = new LineData(null); 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(int[] columnData) { columnCache = columnData; } private static LineData create(@NotNull Document document, int line, int tabSize) { int start = document.getLineStartOffset(line); int end = document.getLineEndOffset(line); int cacheSize = (end - start) / CACHE_FREQUENCY; int[] cache = new int[cacheSize]; CharSequence text = document.getImmutableCharSequence(); int column = 0; boolean hasTabsOrSurrogates = false; for (int i = start; i < end; i++) { if (i > start && (i - start) % CACHE_FREQUENCY == 0) { cache[(i - start) / CACHE_FREQUENCY - 1] = column; } char c = text.charAt(i); if (c == '\t') { column = (column / tabSize + 1) * tabSize; hasTabsOrSurrogates = true; } else { if (Character.isHighSurrogate(c)) { hasTabsOrSurrogates = true; if (i + 1 < text.length() && Character.isLowSurrogate(text.charAt(i + 1))) continue; } else { hasTabsOrSurrogates |= Character.isLowSurrogate(c); } column++; } } if (cacheSize > 0 && (end - start) % CACHE_FREQUENCY == 0) cache[cacheSize - 1] = column; return hasTabsOrSurrogates ? new LineData(cache) : TRIVIAL; } private int offsetToLogicalColumn(@NotNull Document document, int line, int tabSize, int offset) { offset = Math.min(offset, document.getLineEndOffset(line)); int lineStartOffset = document.getLineStartOffset(line); int relOffset = offset - lineStartOffset; if (columnCache == null) return relOffset; int cacheIndex = relOffset / CACHE_FREQUENCY; int startOffset = lineStartOffset + cacheIndex * CACHE_FREQUENCY; int startColumn = cacheIndex == 0 ? 0 : columnCache[cacheIndex - 1]; return calcColumn(document.getImmutableCharSequence(), startOffset, startColumn, offset, tabSize); } private int logicalColumnToOffset(@NotNull Document document, int line, int tabSize, int logicalColumn) { int lineStartOffset = document.getLineStartOffset(line); int lineEndOffset = document.getLineEndOffset(line); if (columnCache == null) { int result = lineStartOffset + logicalColumn; return result < 0 || // guarding over overflow result > lineEndOffset ? lineEndOffset : result; } int pos = Arrays.binarySearch(columnCache, logicalColumn); if (pos >= 0) { int result = lineStartOffset + (pos + 1) * CACHE_FREQUENCY; return DocumentUtil.isInsideSurrogatePair(document, result) ? result - 1 : result; } int startOffset = lineStartOffset + (- pos - 1) * CACHE_FREQUENCY; int column = pos == -1 ? 0 : columnCache[- pos - 2]; return calcOffset(document, logicalColumn, column, startOffset, lineEndOffset, tabSize); } } }