editor new rendering: limit memory used by text layout cache

This commit is contained in:
Dmitry Batrak
2015-10-05 17:14:25 +03:00
parent cf77a58c94
commit dbe2ee8491
7 changed files with 143 additions and 42 deletions
@@ -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;
@@ -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();
@@ -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<Boolean> approximateValue = new Ref<Boolean>(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);
@@ -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 <code>quickEvaluationListener</code> 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());
}
@@ -240,13 +240,17 @@ class LineLayout {
};
}
/**
* If <code>quickEvaluationListener</code> is provided, quick approximate iteration becomes enabled, listener will be invoked
* if approximation will in fact be used during width calculation.
*/
Iterable<VisualFragment> 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<LineFragment> fragments = new ArrayList<LineFragment>(); // in logical order
static class Chunk {
final List<LineFragment> fragments = new ArrayList<LineFragment>(); // 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<VisualFragment> {
@@ -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<LineLayout> myLines = new ArrayList<LineLayout>();
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()) {
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<LineLayout.Chunk> 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");
}
@@ -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<VisualLineFragmentsIterator.Fragment> {
static Iterable<Fragment> create(final EditorView view, final int offset, final boolean beforeSoftWrap, final boolean quick) {
/**
* If <code>quickEvaluationListener</code> is provided, quick approximate iteration mode becomes enabled, listener will be invoked
* if approximation will in fact be used during width calculation.
*/
static Iterable<Fragment> create(final EditorView view, final int offset, final boolean beforeSoftWrap,
@Nullable final Runnable quickEvaluationListener) {
return new Iterable<Fragment>() {
@Override
public Iterator<Fragment> iterator() {
return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap, quick);
return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap, quickEvaluationListener);
}
};
}
@@ -48,7 +54,7 @@ class VisualLineFragmentsIterator implements Iterator<VisualLineFragmentsIterato
private final FoldRegion[] myRegions;
private final Fragment myFragment = new Fragment();
private final int myVisualLineStartOffset;
private final boolean myQuick;
private final Runnable myQuickEvaluationListener;
private int mySegmentStartOffset;
private int mySegmentEndOffset;
@@ -62,8 +68,8 @@ class VisualLineFragmentsIterator implements Iterator<VisualLineFragmentsIterato
private int myCurrentEndLogicalLine;
private int myNextWrapOffset;
private VisualLineFragmentsIterator(EditorView view, int offset, boolean beforeSoftWrap, boolean quick) {
myQuick = quick;
private VisualLineFragmentsIterator(EditorView view, int offset, boolean beforeSoftWrap, @Nullable Runnable quickEvaluationListener) {
myQuickEvaluationListener = quickEvaluationListener;
myView = view;
EditorImpl editor = view.getEditor();
myDocument = editor.getDocument();
@@ -112,7 +118,8 @@ class VisualLineFragmentsIterator implements Iterator<VisualLineFragmentsIterato
int lineStartOffset = myDocument.getLineStartOffset(line);
myFragmentIterator = myView.getTextLayoutCache().getLineLayout(line).
getFragmentsInVisualOrder(myView, lineStartOffset, myCurrentX, myCurrentVisualColumn,
mySegmentStartOffset - lineStartOffset, mySegmentEndOffset - lineStartOffset, myQuick).iterator();
mySegmentStartOffset - lineStartOffset, mySegmentEndOffset - lineStartOffset,
myQuickEvaluationListener).iterator();
}
}