/* * Copyright 2000-2015 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.Disposable; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.PrioritizedDocumentListener; import com.intellij.openapi.editor.impl.EditorDocumentPriorities; import com.intellij.openapi.util.Disposer; import com.intellij.util.ui.update.Activatable; import com.intellij.util.ui.update.UiNotifyConnector; import org.jetbrains.annotations.NotNull; import java.awt.*; import java.util.*; import java.util.List; /** * Editor text layout storage. Layout is stored on a per-logical-line basis, * it's created lazily (when requested) and invalidated on document changes or when explicitly requested. * * @see LineLayout */ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { private static final Logger LOG = Logger.getInstance(TextLayoutCache.class); private static final int MAX_CHUNKS_IN_ACTIVE_EDITOR = 1000; private static final int MAX_CHUNKS_IN_INACTIVE_EDITOR = 10; private final EditorView myView; private final Document myDocument; private final LineLayout myBidiNotRequiredMarker; private ArrayList<LineLayout> myLines = new ArrayList<>(); private int myDocumentChangeOldEndLine; @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private LinkedHashMap<LineLayout.Chunk, Object> myLaidOutChunks = new LinkedHashMap<LineLayout.Chunk, Object>(MAX_CHUNKS_IN_ACTIVE_EDITOR, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<LineLayout.Chunk, Object> eldest) { if (size() > getChunkCacheSizeLimit()) { if (LOG.isDebugEnabled()) LOG.debug("Clearing chunk for " + myView.getEditor().getVirtualFile()); eldest.getKey().clearCache(); return true; } return false; } }; TextLayoutCache(EditorView view) { myView = view; myDocument = view.getEditor().getDocument(); myDocument.addDocumentListener(this, this); myBidiNotRequiredMarker = LineLayout.create(view, "", Font.PLAIN); Disposer.register(this, new UiNotifyConnector(view.getEditor().getContentComponent(), new Activatable.Adapter() { @Override public void hideNotify() { trimChunkCache(); } })); } @Override public int getPriority() { return EditorDocumentPriorities.EDITOR_TEXT_LAYOUT_CACHE; } @Override public void beforeDocumentChange(DocumentEvent event) { 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, true, LineLayout.isBidiLayoutRequired(event.getNewFragment())); if (myLines.size() != myDocument.getLineCount()) { LOG.error("Error updating text layout cache after " + event, new Attachment("editorState.txt", myView.getEditor().dumpState())); resetToDocumentSize(true); } } @Override public void dispose() { myLines = null; myLaidOutChunks = null; } private int getAdjustedLineNumber(int offset) { return myDocument.getTextLength() == 0 ? -1 : myDocument.getLineNumber(offset); } void resetToDocumentSize(boolean documentChangedWithoutNotification) { checkDisposed(); invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1, documentChangedWithoutNotification, documentChangedWithoutNotification); if (myLines.size() != myDocument.getLineCount()) { LOG.error("Error resetting text layout cache", new Attachment("editorState.txt", myView.getEditor().dumpState())); } } void invalidateLines(int startLine, int endLine) { invalidateLines(startLine, endLine, endLine, false, false); } private void invalidateLines(int startLine, int oldEndLine, int newEndLine, boolean textChanged, boolean bidiRequiredForNewText) { checkDisposed(); if (textChanged) { LineLayout firstOldLine = startLine >= 0 && startLine < myLines.size() ? myLines.get(startLine) : null; LineLayout lastOldLine = oldEndLine >= 0 && oldEndLine < myLines.size() ? myLines.get(oldEndLine) : null; if (firstOldLine == null || lastOldLine == null || !firstOldLine.isLtr() || !lastOldLine.isLtr()) bidiRequiredForNewText = true; } int endLine = Math.min(oldEndLine, newEndLine); for (int line = startLine; line <= endLine; line++) { LineLayout lineLayout = myLines.get(line); if (lineLayout != null) { removeChunksFromCache(lineLayout); myLines.set(line, (textChanged && bidiRequiredForNewText) || !lineLayout.isLtr() ? null : myBidiNotRequiredMarker); } } if (oldEndLine < newEndLine) { myLines.addAll(oldEndLine + 1, Collections.nCopies(newEndLine - oldEndLine, null)); } else if (oldEndLine > newEndLine) { List<LineLayout> layouts = myLines.subList(newEndLine + 1, oldEndLine + 1); for (LineLayout layout : layouts) { if (layout != null) { removeChunksFromCache(layout); } } layouts.clear(); } } @NotNull LineLayout getLineLayout(int line) { checkDisposed(); if (line >= myLines.size()) LOG.error("Unexpected cache state", new Attachment("editorState.txt", myView.getEditor().dumpState())); LineLayout result = myLines.get(line); if (result == null || result == myBidiNotRequiredMarker) { result = LineLayout.create(myView, line, result == myBidiNotRequiredMarker); myLines.set(line, result); } return result; } boolean hasCachedLayoutFor(int line) { LineLayout layout = myLines.get(line); return layout != null && layout != myBidiNotRequiredMarker; } private int getChunkCacheSizeLimit() { return myView.getEditor().getContentComponent().isShowing() ? MAX_CHUNKS_IN_ACTIVE_EDITOR : MAX_CHUNKS_IN_INACTIVE_EDITOR; } void onChunkAccess(LineLayout.Chunk chunk) { myLaidOutChunks.put(chunk, null); } private void removeChunksFromCache(LineLayout layout) { layout.getChunksInLogicalOrder().forEach(myLaidOutChunks::remove); } private void trimChunkCache() { int limit = getChunkCacheSizeLimit(); if (myLaidOutChunks.size() > limit) { Iterator<LineLayout.Chunk> it = myLaidOutChunks.keySet().iterator(); while (myLaidOutChunks.size() > limit) { LineLayout.Chunk chunk = it.next(); if (LOG.isDebugEnabled()) LOG.debug("Clearing chunk for " + myView.getEditor().getVirtualFile()); chunk.clearCache(); it.remove(); } } } private void checkDisposed() { if (myLines == null) myView.getEditor().throwDisposalError("Editor is already disposed"); } }