diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorCoordinateMapper.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorCoordinateMapper.java index 89256fe2edb8..560d29d25acc 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorCoordinateMapper.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorCoordinateMapper.java @@ -73,7 +73,7 @@ class EditorCoordinateMapper { int visualLine = offsetToVisualLine(offset, beforeSoftWrap); int maxVisualColumn = 0; int maxLogicalColumn = 0; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap, null)) { if (!pos.leansForward && offset == fragment.getVisualLineStartOffset()) { return new VisualPosition(visualLine, fragment.getStartVisualColumn()); } @@ -125,7 +125,7 @@ class EditorCoordinateMapper { int maxVisualColumn = 0; int maxLogicalColumn = 0; int maxOffset = offset; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false, null)) { int minColumn = fragment.getStartVisualColumn(); int maxColumn = fragment.getEndVisualColumn(); if (column < minColumn || column == minColumn && !pos.leansRight) { @@ -240,7 +240,7 @@ class EditorCoordinateMapper { if (visualLine < myView.getEditor().getVisibleLineCount()) { int visualLineStartOffset = visualLineToOffset(visualLine); int maxOffset = 0; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false, null)) { if (p.x <= fragment.getStartX()) { int markerWidth = myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP); float indent = fragment.getStartX() - markerWidth; @@ -286,7 +286,7 @@ class EditorCoordinateMapper { if (visualLine < myView.getEditor().getVisibleLineCount()) { int visualLineStartOffset = visualLineToOffset(visualLine); int maxOffset = 0; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false, null)) { if (column < fragment.getStartVisualColumn()) { break; } @@ -317,7 +317,7 @@ class EditorCoordinateMapper { float x = getStartX(logicalLine); if (myDocument.getTextLength() > 0) { boolean firstFragment = true; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap, null)) { if (firstFragment && offset == visualLineStartOffset && !leanTowardsLargerOffsets) { x = fragment.getStartX(); break; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorPainter.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorPainter.java index 8752e9c18c6e..56a9a68e662e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorPainter.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorPainter.java @@ -584,7 +584,7 @@ class EditorPainter implements TextDrawingCallback { } } else { - int maxWidth = myView.getMaxWidthInLineRange(startPosition.line, endPosition.line, false) - 1; + int maxWidth = myView.getMaxWidthInLineRange(startPosition.line, endPosition.line, null) - 1; TFloatArrayList leadingRanges = adjustedLogicalRangeToVisualRanges(startOffset, myView.visualPositionToOffset(new VisualPosition( startPosition.line, Integer.MAX_VALUE, true))); @@ -691,7 +691,7 @@ class EditorPainter implements TextDrawingCallback { private TFloatArrayList logicalRangeToVisualRanges(int startOffset, int endOffset) { assert startOffset <= endOffset; TFloatArrayList result = new TFloatArrayList(); - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, startOffset, false, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, startOffset, false, null)) { int minOffset = fragment.getMinOffset(); int maxOffset = fragment.getMaxOffset(); if (startOffset == endOffset) { @@ -772,7 +772,7 @@ class EditorPainter implements TextDrawingCallback { int targetVisualColumn = caret.getVisualPosition().column; for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, caret.getVisualLineStart(), - false, false)) { + false, null)) { int startVisualColumn = fragment.getStartVisualColumn(); int endVisualColumn = fragment.getEndVisualColumn(); if (startVisualColumn < targetVisualColumn && endVisualColumn > targetVisualColumn || @@ -810,7 +810,7 @@ class EditorPainter implements TextDrawingCallback { int prevEndOffset = -1; boolean firstFragment = true; int maxColumn = 0; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false, null)) { int fragmentStartOffset = fragment.getStartOffset(); int start = fragmentStartOffset; int end = fragment.getEndOffset(); diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorSizeManager.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorSizeManager.java index aa46a4aaeb85..b9ce1d8684f9 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorSizeManager.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/EditorSizeManager.java @@ -29,6 +29,7 @@ import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.editor.impl.softwrap.mapping.IncrementalCacheUpdateEvent; import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapAwareDocumentParsingListenerAdapter; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import gnu.trove.TIntArrayList; @@ -43,14 +44,15 @@ import java.util.List; * Calculates width (in pixels) of editor contents. */ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, FoldingListener { - private static final int UNKNOWN_WIDTH = -1; + private static final int UNKNOWN_WIDTH = Integer.MAX_VALUE; private final EditorView myView; private final EditorImpl myEditor; private final DocumentEx myDocument; private final TIntArrayList myLineWidths = new TIntArrayList(); // cached widths of visual lines (in pixels) - // UNKNOWN_WIDTH(-1) means no value + // negative value means an estimated (not precise) width + // UNKNOWN_WIDTH(Integer.MAX_VALUE) means no value private int myWidthInPixels; private int myMaxLineWithExtensionWidth; @@ -175,7 +177,14 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold for (int i = 0; i < lineCount; i++) { int width = myLineWidths.get(i); if (width == UNKNOWN_WIDTH) { - width = myView.getMaxWidthInLineRange(i, i, true); + final Ref approximateValue = new Ref(Boolean.FALSE); + width = myView.getMaxWidthInLineRange(i, i, new Runnable() { + @Override + public void run() { + approximateValue.set(Boolean.TRUE); + } + }); + if (approximateValue.get()) width = -width; myLineWidths.set(i, width); } maxWidth = Math.max(maxWidth, Math.abs(width)); @@ -230,11 +239,17 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold try { int startVisualLine = myView.offsetToVisualLine(startOffset, false); int endVisualLine = myView.offsetToVisualLine(endOffset, true); + boolean sizeInvalidated = false; for (int i = startVisualLine; i <= endVisualLine; i++) { - myLineWidths.set(i, UNKNOWN_WIDTH); + if (myLineWidths.get(i) < 0) { + myLineWidths.set(i, UNKNOWN_WIDTH); + sizeInvalidated = true; + } + } + if (sizeInvalidated) { + myWidthInPixels = -1; + myEditor.getContentComponent().revalidate(); } - myWidthInPixels = -1; - myEditor.getContentComponent().revalidate(); } finally { myEditor.setPurePaintingMode(purePaintingMode); 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 44399311da19..acd53826e845 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 @@ -34,6 +34,7 @@ import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.awt.font.FontRenderContext; @@ -238,17 +239,22 @@ public class EditorView implements TextDrawingCallback, Disposable { public int getMaxWidthInRange(int startOffset, int endOffset) { assertIsDispatchThread(); - return getMaxWidthInLineRange(offsetToVisualLine(startOffset, false), offsetToVisualLine(endOffset, true), false); + return getMaxWidthInLineRange(offsetToVisualLine(startOffset, false), offsetToVisualLine(endOffset, true), null); } - - int getMaxWidthInLineRange(int startVisualLine, int endVisualLine, boolean quick) { + + /** + * If quickEvaluationListener is provided, quick approximate size evaluation becomes enabled, listener will be invoked + * if approximation will in fact be used during width calculation. + */ + int getMaxWidthInLineRange(int startVisualLine, int endVisualLine, @Nullable Runnable quickEvaluationListener) { int maxWidth = 0; endVisualLine = Math.min(endVisualLine, myEditor.getVisibleLineCount() - 1); for (int i = startVisualLine; i <= endVisualLine; i++) { int startOffset = myMapper.visualLineToOffset(i); float x = 0; int maxOffset = 0; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(this, startOffset, false, quick)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(this, startOffset, false, + quickEvaluationListener)) { x = fragment.getEndX(); maxOffset = Math.max(maxOffset, fragment.getMaxOffset()); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java index 9c54b2b9851f..72554793b234 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineLayout.java @@ -240,13 +240,17 @@ class LineLayout { }; } + /** + * If quickEvaluationListener is provided, quick approximate iteration becomes enabled, listener will be invoked + * if approximation will in fact be used during width calculation. + */ Iterable getFragmentsInVisualOrder(@NotNull final EditorView view, final int lineStartOffset, final float startX, final int startVisualColumn, final int startOffset, int endOffset, - boolean quick) { + @Nullable Runnable quickEvaluationListener) { assert startOffset <= endOffset; final BidiRun[] runs; if (startOffset == endOffset) { @@ -257,7 +261,7 @@ class LineLayout { for (BidiRun run : myBidiRunsInLogicalOrder) { if (run.endOffset <= startOffset) continue; if (run.startOffset >= endOffset) break; - runList.add(run.subRun(view, lineStartOffset, startOffset, endOffset, quick)); + runList.add(run.subRun(view, lineStartOffset, startOffset, endOffset, quickEvaluationListener)); } runs = runList.toArray(new BidiRun[runList.size()]); reorderRunsVisually(runs); @@ -344,7 +348,8 @@ class LineLayout { return chunks; } - private BidiRun subRun(@NotNull EditorView view, int lineStartOffset, int targetStartOffset, int targetEndOffset, boolean quick) { + private BidiRun subRun(@NotNull EditorView view, int lineStartOffset, int targetStartOffset, int targetEndOffset, + @Nullable Runnable quickEvaluationListener) { assert targetStartOffset < endOffset; assert targetEndOffset > startOffset; int start = Math.max(startOffset, targetStartOffset); @@ -354,15 +359,15 @@ class LineLayout { for (Chunk chunk : getChunks()) { if (chunk.endOffset <= start) continue; if (chunk.startOffset >= end) break; - subChunks.add(chunk.subChunk(view, this, lineStartOffset, start, end, quick)); + subChunks.add(chunk.subChunk(view, this, lineStartOffset, start, end, quickEvaluationListener)); } subRun.chunks = subChunks.toArray(new Chunk[subChunks.size()]); return subRun; } } - private static class Chunk { - private final List fragments = new ArrayList(); // in logical order + static class Chunk { + final List fragments = new ArrayList(); // in logical order private int startOffset; private int endOffset; @@ -372,6 +377,9 @@ class LineLayout { } private void ensureLayout(@NotNull EditorView view, BidiRun run, int lineStartOffset) { + if (isReal()) { + view.getTextLayoutCache().onChunkAccess(this); + } if (!fragments.isEmpty()) return; int start = lineStartOffset + startOffset; int end = lineStartOffset + endOffset; @@ -387,17 +395,15 @@ class LineLayout { assert !fragments.isEmpty(); } - private Chunk subChunk(EditorView view, BidiRun run, int lineStartOffset, int targetStartOffset, int targetEndOffset, boolean quick) { + private Chunk subChunk(EditorView view, BidiRun run, int lineStartOffset, int targetStartOffset, int targetEndOffset, + @Nullable Runnable quickEvaluationListener) { assert targetStartOffset < endOffset; assert targetEndOffset > startOffset; int start = Math.max(startOffset, targetStartOffset); int end = Math.min(endOffset, targetEndOffset); - if (quick && fragments.isEmpty()) { - Chunk chunk = new Chunk(start, end); - int startColumn = view.offsetToLogicalPosition(lineStartOffset + start).column; - int endColumn = view.offsetToLogicalPosition(lineStartOffset + end).column; - chunk.fragments.add(new ApproximationFragment(end - start, endColumn - startColumn, view.getMaxCharWidth())); - return chunk; + if (quickEvaluationListener != null && fragments.isEmpty()) { + quickEvaluationListener.run(); + return new ApproximationChunk(view, lineStartOffset, start, end); } if (start == startOffset && end == this.endOffset) { return this; @@ -415,6 +421,28 @@ class LineLayout { } return chunk; } + + boolean isReal() { + return true; + } + + void clearCache() { + fragments.clear(); + } + } + + private static class ApproximationChunk extends Chunk { + private ApproximationChunk(@NotNull EditorView view, int lineStartOffset, int start, int end) { + super(start, end); + int startColumn = view.offsetToLogicalPosition(lineStartOffset + start).column; + int endColumn = view.offsetToLogicalPosition(lineStartOffset + end).column; + fragments.add(new ApproximationFragment(end - start, endColumn - startColumn, view.getMaxCharWidth())); + } + + @Override + boolean isReal() { + return false; + } } private static class VisualOrderIterator implements Iterator { diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextLayoutCache.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextLayoutCache.java index 1e30841f26dc..baba4fd59e87 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextLayoutCache.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextLayoutCache.java @@ -20,11 +20,13 @@ 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.ArrayList; -import java.util.Collections; +import java.util.*; /** * Editor text layout storage. Layout is stored on a per-logical-line basis, @@ -33,17 +35,39 @@ import java.util.Collections; * @see LineLayout */ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { + 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 myLines = new ArrayList(); private int myDocumentChangeOldEndLine; + + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private LinkedHashMap myLaidOutChunks = + new LinkedHashMap(MAX_CHUNKS_IN_ACTIVE_EDITOR, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > getChunkCacheSizeLimit()) { + eldest.getKey().clearCache(); + return true; + } + return false; + } + }; TextLayoutCache(EditorView view) { myView = view; myDocument = view.getEditor().getDocument(); myDocument.addDocumentListener(this, this); myBidiNotRequiredMarker = new LineLayout(view, "", Font.PLAIN); + Disposer.register(this, new UiNotifyConnector(view.getEditor().getContentComponent(), new Activatable.Adapter() { + @Override + public void hideNotify() { + trimChunkCache(); + } + })); } @Override @@ -66,12 +90,13 @@ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { @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); @@ -80,7 +105,7 @@ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { void invalidateLines(int startLine, int endLine) { invalidateLines(startLine, endLine, endLine, true); } - + private void invalidateLines(int startLine, int oldEndLine, int newEndLine, boolean keepBidiNotRequiredState) { checkDisposed(); int endLine = Math.min(oldEndLine, newEndLine); @@ -115,6 +140,26 @@ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { return new LineLayout(myView, lineStart, lineEnd, skipBidiLayout); } + 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 trimChunkCache() { + int limit = getChunkCacheSizeLimit(); + if (myLaidOutChunks.size() > limit) { + Iterator it = myLaidOutChunks.keySet().iterator(); + while (myLaidOutChunks.size() > limit) { + LineLayout.Chunk chunk = it.next(); + chunk.clearCache(); + it.remove(); + } + } + } + private void checkDisposed() { if (myLines == null) throw new IllegalStateException("Editor is already disposed"); } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/VisualLineFragmentsIterator.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/VisualLineFragmentsIterator.java index 2afdcb30e755..0e2344a1be4e 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/VisualLineFragmentsIterator.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/VisualLineFragmentsIterator.java @@ -22,6 +22,7 @@ import com.intellij.openapi.editor.ex.FoldingModelEx; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.editor.impl.SoftWrapModelImpl; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.util.Iterator; @@ -34,11 +35,16 @@ import java.util.NoSuchElementException; */ class VisualLineFragmentsIterator implements Iterator { - static Iterable create(final EditorView view, final int offset, final boolean beforeSoftWrap, final boolean quick) { + /** + * If quickEvaluationListener is provided, quick approximate iteration mode becomes enabled, listener will be invoked + * if approximation will in fact be used during width calculation. + */ + static Iterable create(final EditorView view, final int offset, final boolean beforeSoftWrap, + @Nullable final Runnable quickEvaluationListener) { return new Iterable() { @Override public Iterator iterator() { - return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap, quick); + return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap, quickEvaluationListener); } }; } @@ -48,7 +54,7 @@ class VisualLineFragmentsIterator implements Iterator