From df3694f0255049a9c6ada940d3f0a1b0fb86de2f Mon Sep 17 00:00:00 2001 From: Dmitry Batrak Date: Fri, 2 Oct 2015 17:42:24 +0300 Subject: [PATCH] editor new rendering: speed up opening and editing for documents with long lines --- .../editor/impl/EditorDocumentPriorities.java | 3 +- .../openapi/editor/impl/EditorImpl.java | 6 +- .../impl/view/ApproximationFragment.java | 88 ++++ .../impl/view/EditorCoordinateMapper.java | 64 +-- .../editor/impl/view/EditorPainter.java | 13 +- .../editor/impl/view/EditorSizeManager.java | 80 ++-- .../openapi/editor/impl/view/EditorView.java | 37 +- .../editor/impl/view/LineFragment.java | 4 - .../openapi/editor/impl/view/LineLayout.java | 442 +++++++----------- .../impl/view/LogicalPositionCache.java | 205 ++++++++ .../openapi/editor/impl/view/TabFragment.java | 10 - .../editor/impl/view/TextFragment.java | 20 - .../editor/impl/view/TextLayoutCache.java | 57 +-- .../view/VisualLineFragmentsIterator.java | 14 +- 14 files changed, 575 insertions(+), 468 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ApproximationFragment.java create mode 100644 platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LogicalPositionCache.java diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/EditorDocumentPriorities.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/EditorDocumentPriorities.java index f105f980091c..157231cbbb36 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/EditorDocumentPriorities.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/EditorDocumentPriorities.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2010 JetBrains s.r.o. + * 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. @@ -33,6 +33,7 @@ public class EditorDocumentPriorities { public static final int RANGE_MARKER = 40; public static final int FOLD_MODEL = 60; + public static final int LOGICAL_POSITION_CACHE = 65; public static final int EDITOR_TEXT_LAYOUT_CACHE = 70; public static final int EDITOR_TEXT_WIDTH_CACHE = 75; public static final int LEXER_EDITOR = 80; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index f5c57cd067dc..0f816cdc9d0f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -328,7 +328,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi assertIsDispatchThread(); myProject = project; myDocument = (DocumentEx)document; - if (myDocument instanceof DocumentImpl) { + if (myDocument instanceof DocumentImpl && !myUseNewRendering) { ((DocumentImpl)myDocument).requestTabTracking(); } myScheme = createBoundColorSchemeDelegate(null); @@ -888,7 +888,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi if (myConnection != null) { myConnection.disconnect(); } - if (myDocument instanceof DocumentImpl) { + if (myDocument instanceof DocumentImpl && !myUseNewRendering) { ((DocumentImpl)myDocument).giveUpTabTracking(); } Disposer.dispose(myDisposable); @@ -1955,7 +1955,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi } public boolean hasTabs() { - return !(myDocument instanceof DocumentImpl) || ((DocumentImpl)myDocument).mightContainTabs(); + return myUseNewRendering || !(myDocument instanceof DocumentImpl) || ((DocumentImpl)myDocument).mightContainTabs(); } public boolean isScrollToCaret() { diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ApproximationFragment.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ApproximationFragment.java new file mode 100644 index 000000000000..143a59c54de0 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ApproximationFragment.java @@ -0,0 +1,88 @@ +/* + * 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 org.jetbrains.annotations.NotNull; + +import java.awt.*; + +/** + * Used for quick estimation of editor's viewable area size (without full text layout) + */ +class ApproximationFragment implements LineFragment { + private final int myLength; + private final int myColumnCount; + private final float myWidth; + + ApproximationFragment(int length, int columnCount, float charWidth) { + myLength = length; + myColumnCount = columnCount; + myWidth = charWidth * columnCount; + } + + @Override + public int getLength() { + return myLength; + } + + @Override + public int getLogicalColumnCount(int startColumn) { + return myColumnCount; + } + + @Override + public int getVisualColumnCount(float startX) { + return myColumnCount; + } + + @Override + public int logicalToVisualColumn(float startX, int startColumn, int column) { + return column; + } + + @Override + public int visualToLogicalColumn(float startX, int startColumn, int column) { + return column; + } + + @Override + public float visualColumnToX(float startX, int column) { + return column < myColumnCount ? 0 : myWidth; + } + + @Override + public int[] xToVisualColumn(float startX, float x) { + float relX = x - startX; + int column = relX < myWidth / 2 ? 0 : myColumnCount; + return new int[] {column, relX <= visualColumnToX(startX, column) ? 0 : 1}; + } + + @Override + public float offsetToX(float startX, int startOffset, int offset) { + return offset < myLength ? 0 : myWidth; + } + + @Override + public void draw(Graphics2D g, float x, float y, int startColumn, int endColumn) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public LineFragment subFragment(int startOffset, int endOffset) { + throw new UnsupportedOperationException(); + } +} 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 1411c2dd1f62..89256fe2edb8 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 @@ -15,7 +15,6 @@ */ package com.intellij.openapi.editor.impl.view; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.*; import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.EditorImpl; @@ -55,58 +54,11 @@ class EditorCoordinateMapper { @NotNull LogicalPosition offsetToLogicalPosition(int offset) { - int textLength = myDocument.getTextLength(); - if (offset <= 0 || textLength == 0) { - return new LogicalPosition(0, 0); - } - offset = Math.min(offset, textLength); - int line = myDocument.getLineNumber(offset); - offset = Math.min(offset, myDocument.getLineEndOffset(line)); - int column = 0; - if (ApplicationManager.getApplication().isDispatchThread()) { - LineLayout lineLayout = myView.getTextLayoutCache().getLineLayout(line); - column = lineLayout.offsetToLogicalColumn(offset - myDocument.getLineStartOffset(line)); - } - else { - CharSequence text = myDocument.getImmutableCharSequence(); - int tabSize = myView.getTabSize(); - for (int i = myDocument.getLineStartOffset(line); i < offset; i++) { - if (text.charAt(i) == '\t') { - column = (column / tabSize + 1) * tabSize; - } - else { - column++; - } - } - } - return new LogicalPosition(line, column); + return myView.getLogicalPositionCache().offsetToLogicalPosition(offset); } int logicalPositionToOffset(@NotNull LogicalPosition pos) { - int line = pos.line; - if (line >= myDocument.getLineCount()) return myDocument.getTextLength(); - - int lineStartOffset = myDocument.getLineStartOffset(line); - if (ApplicationManager.getApplication().isDispatchThread()) { - LineLayout lineLayout = myView.getTextLayoutCache().getLineLayout(line); - return lineStartOffset + lineLayout.logicalColumnToOffset(pos.column); - } - else { - int lineEndOffset = myDocument.getLineEndOffset(line); - CharSequence text = myDocument.getImmutableCharSequence(); - int tabSize = myView.getTabSize(); - int column = 0; - for (int i = lineStartOffset; i < lineEndOffset; i++) { - if (text.charAt(i) == '\t') { - column = (column / tabSize + 1) * tabSize; - } - else { - column++; - } - if (pos.column < column) return i; - } - return lineEndOffset; - } + return myView.getLogicalPositionCache().logicalPositionToOffset(pos); } @NotNull @@ -121,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)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap, false)) { if (!pos.leansForward && offset == fragment.getVisualLineStartOffset()) { return new VisualPosition(visualLine, fragment.getStartVisualColumn()); } @@ -173,7 +125,7 @@ class EditorCoordinateMapper { int maxVisualColumn = 0; int maxLogicalColumn = 0; int maxOffset = offset; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false, false)) { int minColumn = fragment.getStartVisualColumn(); int maxColumn = fragment.getEndVisualColumn(); if (column < minColumn || column == minColumn && !pos.leansRight) { @@ -234,7 +186,7 @@ class EditorCoordinateMapper { return myDocument.getLineNumber(offset) - myFoldingModel.getFoldedLinesCountBefore(offset) + softWrapsBeforeOrAtOffset; } - private int visualLineToOffset(int visualLine) { + int visualLineToOffset(int visualLine) { int start = 0; int end = myDocument.getTextLength(); int current = 0; @@ -288,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)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false, false)) { if (p.x <= fragment.getStartX()) { int markerWidth = myView.getEditor().getSoftWrapModel().getMinDrawingWidthInPixels(SoftWrapDrawingType.AFTER_SOFT_WRAP); float indent = fragment.getStartX() - markerWidth; @@ -334,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)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, visualLineStartOffset, false, false)) { if (column < fragment.getStartVisualColumn()) { break; } @@ -365,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)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, beforeSoftWrap, false)) { 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 4251f7dfb0a6..8752e9c18c6e 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 @@ -465,7 +465,7 @@ class EditorPainter implements TextDrawingCallback { Collection extensions = painter.getLineExtensions(project, virtualFile, line); if (extensions != null) { for (LineExtensionInfo info : extensions) { - LineLayout layout = new LineLayout(myView, info.getText(), info.getFontType(), g.getFontRenderContext()); + LineLayout layout = new LineLayout(myView, info.getText(), info.getFontType()); g.setColor(info.getColor()); x = paintLineLayoutWithEffect(g, layout, x, y, info.getEffectColor(), info.getEffectType()); int currentLineWidth = (int)x; @@ -584,7 +584,7 @@ class EditorPainter implements TextDrawingCallback { } } else { - int maxWidth = myView.getMaxWidthInLineRange(startPosition.line, endPosition.line) - 1; + int maxWidth = myView.getMaxWidthInLineRange(startPosition.line, endPosition.line, false) - 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)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, startOffset, false, false)) { int minOffset = fragment.getMinOffset(); int maxOffset = fragment.getMaxOffset(); if (startOffset == endOffset) { @@ -770,8 +770,9 @@ class EditorPainter implements TextDrawingCallback { g.fillRect(startX, y, width, lineHeight - 1); if (myDocument.getTextLength() > 0 && caret != null) { int targetVisualColumn = caret.getVisualPosition().column; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, - caret.getVisualLineStart(), false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, + caret.getVisualLineStart(), + false, false)) { int startVisualColumn = fragment.getStartVisualColumn(); int endVisualColumn = fragment.getEndVisualColumn(); if (startVisualColumn < targetVisualColumn && endVisualColumn > targetVisualColumn || @@ -809,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)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(myView, offset, false, false)) { 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 9b4b4b1dd717..aa46a4aaeb85 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,36 +29,35 @@ 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.TextRange; import com.intellij.openapi.vfs.VirtualFile; import gnu.trove.TIntArrayList; -import gnu.trove.TIntHashSet; -import gnu.trove.TIntProcedure; import org.jetbrains.annotations.NotNull; import java.awt.*; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; /** * Calculates width (in pixels) of editor contents. */ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, FoldingListener { - private static final int UNKNOWN_WIDTH = Integer.MAX_VALUE; + private static final int UNKNOWN_WIDTH = -1; 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) - // negative value means an estimated (not precise) width - // UNKNOWN_WIDTH(Integer.MAX_VALUE) means no value + // UNKNOWN_WIDTH(-1) means no value private int myWidthInPixels; private int myMaxLineWithExtensionWidth; private int myWidestLineWithExtension; - private boolean myApproximateWidthCalculated; - private final TIntHashSet myLinesWithCreatedLayout = new TIntHashSet(); - + private final List myDeferredRanges = new ArrayList(); + private final SoftWrapAwareDocumentParsingListenerAdapter mySoftWrapChangeListener = new SoftWrapAwareDocumentParsingListenerAdapter() { @Override public void onRecalculationEnd(@NotNull IncrementalCacheUpdateEvent event) { @@ -112,15 +111,11 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold } foldingChangeStartOffset = Integer.MAX_VALUE; foldingChangeEndOffset = Integer.MIN_VALUE; - - myLinesWithCreatedLayout.forEach(new TIntProcedure() { - @Override - public boolean execute(int logicalLine) { - onLineLayoutCreated(logicalLine); - return true; - } - }); - myLinesWithCreatedLayout.clear(); + + for (TextRange range : myDeferredRanges) { + onTextLayoutPerformed(range.getStartOffset(), range.getEndOffset()); + } + myDeferredRanges.clear(); } private void onSoftWrapRecalculationEnd(IncrementalCacheUpdateEvent event) { @@ -128,7 +123,8 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold } Dimension getPreferredSize() { - int width = getPreferredWidth(); + int widthWithoutCaret = getPreferredWidth(); + int width = widthWithoutCaret; if (!myDocument.isInBulkUpdate()) { for (Caret caret : myEditor.getCaretModel().getAllCarets()) { if (caret.isUpToDate()) { @@ -137,10 +133,18 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold } } } - width += myEditor.getSettings().getAdditionalColumnsCount() * myView.getPlainSpaceWidth(); + if (shouldRespectAdditionalColumns(widthWithoutCaret)) { + width += myEditor.getSettings().getAdditionalColumnsCount() * myView.getPlainSpaceWidth(); + } return new Dimension(width, myEditor.getPreferredHeight()); } + private boolean shouldRespectAdditionalColumns(int widthWithoutCaret) { + return !myEditor.getSoftWrapModel().isSoftWrappingEnabled() + || myEditor.getSoftWrapModel().isRespectAdditionalColumns() + || widthWithoutCaret > myEditor.getScrollingModel().getVisibleArea().getWidth(); + } + private int getPreferredWidth() { if (myWidthInPixels < 0) { myWidthInPixels = calculatePreferredWidth(); @@ -171,15 +175,7 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold for (int i = 0; i < lineCount; i++) { int width = myLineWidths.get(i); if (width == UNKNOWN_WIDTH) { - myApproximateWidthCalculated = false; - myView.getTextLayoutCache().enableQuickLayouts(true); - try { - width = myView.getMaxWidthInLineRange(i, i); - } - finally { - myView.getTextLayoutCache().enableQuickLayouts(false); - } - if (myApproximateWidthCalculated) width = - width; + width = myView.getMaxWidthInLineRange(i, i, true); myLineWidths.set(i, width); } maxWidth = Math.max(maxWidth, Math.abs(width)); @@ -217,38 +213,28 @@ class EditorSizeManager implements PrioritizedDocumentListener, Disposable, Fold myMaxLineWithExtensionWidth = width; } - void quickLineLayoutCreated() { - myApproximateWidthCalculated = true; - } - - void lineLayoutCreated(int logicalLine) { + void textLayoutPerformed(int startOffset, int endOffset) { if (myEditor.getFoldingModel().isInBatchFoldingOperation()) { - myLinesWithCreatedLayout.add(logicalLine); + myDeferredRanges.add(new TextRange(startOffset, endOffset)); } else { - onLineLayoutCreated(logicalLine); + onTextLayoutPerformed(startOffset, endOffset); } } - private void onLineLayoutCreated(int logicalLine) { + private void onTextLayoutPerformed(int startOffset, int endOffset) { boolean purePaintingMode = myEditor.isPurePaintingMode(); boolean foldingEnabled = myEditor.getFoldingModel().isFoldingEnabled(); myEditor.setPurePaintingMode(false); myEditor.getFoldingModel().setFoldingEnabled(true); try { - int startVisualLine = myView.offsetToVisualLine(myDocument.getLineStartOffset(logicalLine), false); - int endVisualLine = myView.offsetToVisualLine(myDocument.getLineEndOffset(logicalLine), false); - boolean sizeInvalidated = false; + int startVisualLine = myView.offsetToVisualLine(startOffset, false); + int endVisualLine = myView.offsetToVisualLine(endOffset, true); for (int i = startVisualLine; i <= endVisualLine; i++) { - if (myLineWidths.get(i) < 0) { - myLineWidths.set(i, UNKNOWN_WIDTH); - sizeInvalidated = true; - } - } - if (sizeInvalidated) { - myWidthInPixels = -1; - myEditor.getContentComponent().revalidate(); + myLineWidths.set(i, UNKNOWN_WIDTH); } + 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 7a350d5514bb..44399311da19 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 @@ -55,6 +55,7 @@ public class EditorView implements TextDrawingCallback, Disposable { private final EditorCoordinateMapper myMapper; private final EditorSizeManager mySizeManager; private final TextLayoutCache myTextLayoutCache; + private final LogicalPositionCache myLogicalPositionCache; private final TabFragment myTabFragment; private String myPrefixText; // accessed only in EDT @@ -79,8 +80,10 @@ public class EditorView implements TextDrawingCallback, Disposable { myMapper = new EditorCoordinateMapper(this); mySizeManager = new EditorSizeManager(this); myTextLayoutCache = new TextLayoutCache(this); + myLogicalPositionCache = new LogicalPositionCache(this); myTabFragment = new TabFragment(this); + Disposer.register(this, myLogicalPositionCache); Disposer.register(this, myTextLayoutCache); Disposer.register(this, mySizeManager); } @@ -108,6 +111,10 @@ public class EditorView implements TextDrawingCallback, Disposable { TabFragment getTabFragment() { return myTabFragment; } + + LogicalPositionCache getLogicalPositionCache() { + return myLogicalPositionCache; + } @Override public void dispose() { @@ -191,7 +198,7 @@ public class EditorView implements TextDrawingCallback, Disposable { myPrefixText = prefixText; synchronized (myLock) { myPrefixLayout = prefixText == null || prefixText.isEmpty() ? null : - new LineLayout(this, prefixText, attributes.getFontType(), myFontRenderContext); + new LineLayout(this, prefixText, attributes.getFontType()); } myPrefixAttributes = attributes; mySizeManager.invalidateRange(0, 0); @@ -231,18 +238,17 @@ public class EditorView implements TextDrawingCallback, Disposable { public int getMaxWidthInRange(int startOffset, int endOffset) { assertIsDispatchThread(); - return getMaxWidthInLineRange(offsetToVisualLine(startOffset, false), offsetToVisualLine(endOffset, true)); + return getMaxWidthInLineRange(offsetToVisualLine(startOffset, false), offsetToVisualLine(endOffset, true), false); } - int getMaxWidthInLineRange(int startVisualLine, int endVisualLine) { + int getMaxWidthInLineRange(int startVisualLine, int endVisualLine, boolean quick) { int maxWidth = 0; + endVisualLine = Math.min(endVisualLine, myEditor.getVisibleLineCount() - 1); for (int i = startVisualLine; i <= endVisualLine; i++) { - LogicalPosition startPosition = visualToLogicalPosition(new VisualPosition(i, 0)); - if (startPosition.line >= myDocument.getLineCount()) break; - int startOffset = logicalPositionToOffset(startPosition); + int startOffset = myMapper.visualLineToOffset(i); float x = 0; int maxOffset = 0; - for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(this, startOffset, false)) { + for (VisualLineFragmentsIterator.Fragment fragment : VisualLineFragmentsIterator.create(this, startOffset, false, quick)) { x = fragment.getEndX(); maxOffset = Math.max(maxOffset, fragment.getMaxOffset()); } @@ -264,9 +270,11 @@ public class EditorView implements TextDrawingCallback, Disposable { myMaxCharWidth = -1; myTabSize = -1; } - reset(); - setPrefix(myPrefixText, myPrefixAttributes); // recreate prefix layout + myLogicalPositionCache.reset(false); + myTextLayoutCache.resetToDocumentSize(false); invalidateFoldRegionLayouts(); + setPrefix(myPrefixText, myPrefixAttributes); // recreate prefix layout + mySizeManager.reset(); } public void invalidateRange(int startOffset, int endOffset) { @@ -277,14 +285,18 @@ public class EditorView implements TextDrawingCallback, Disposable { } int startLine = myDocument.getLineNumber(Math.max(0, startOffset)); int endLine = myDocument.getLineNumber(Math.min(textLength, endOffset)); - myTextLayoutCache.invalidateLines(startLine, endLine, endLine); + myTextLayoutCache.invalidateLines(startLine, endLine); mySizeManager.invalidateRange(startOffset, endOffset); } + /** + * Invoked when document might have changed, but no notifications were sent (for a hacky document in EditorTextFieldCellRenderer) + */ public void reset() { assertIsDispatchThread(); + myLogicalPositionCache.reset(true); + myTextLayoutCache.resetToDocumentSize(true); mySizeManager.reset(); - myTextLayoutCache.resetToDocumentSize(); } public boolean isRtlLocation(@NotNull VisualPosition visualPosition) { @@ -422,8 +434,7 @@ public class EditorView implements TextDrawingCallback, Disposable { if (layout == null) { TextAttributes placeholderAttributes = myEditor.getFoldingModel().getPlaceholderAttributes(); layout = new LineLayout(this, foldRegion.getPlaceholderText(), - placeholderAttributes == null ? Font.PLAIN : placeholderAttributes.getFontType(), - myFontRenderContext); + placeholderAttributes == null ? Font.PLAIN : placeholderAttributes.getFontType()); foldRegion.putUserData(FOLD_REGION_TEXT_LAYOUT, layout); } return layout; diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineFragment.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineFragment.java index 28dc9e9fccee..ceb3153c12eb 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineFragment.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LineFragment.java @@ -32,10 +32,6 @@ interface LineFragment { int getLogicalColumnCount(int startColumn); int getVisualColumnCount(float startX); - - int offsetToLogicalColumn(int startColumn, int offset); - - int logicalColumnToOffset(int startColumn, int column); // columns are visual int logicalToVisualColumn(float startX, int startColumn, int column); 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 e45865219098..0d691ab2e388 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 @@ -35,100 +35,84 @@ import java.util.List; /** * Layout of a single line of document text. Consists of a series of BidiRuns, which, in turn, consist of TextFragments. + * TextFragments within BidiRun are grouped into Chunks for performance reasons, glyph layout is performed per-Chunk, and only + * for required Chunks. */ class LineLayout { private final BidiRun[] myBidiRunsInLogicalOrder; private final BidiRun[] myBidiRunsInVisualOrder; private final float myWidth; - - /** - * Creates a dummy layout used for editor size estimations - */ - LineLayout(EditorView view, int lineLength) { - this(createDummyFragment(view, lineLength), false); - } /** * Creates a layout for a fragment of text from editor. */ - LineLayout(@NotNull EditorView view, - int startOffset, int endOffset, - @NotNull FontRenderContext fontRenderContext) { - this(createFragments(view, startOffset, endOffset, fontRenderContext), false); + LineLayout(@NotNull EditorView view, int startOffset, int endOffset, boolean skipBidiLayout) { + this(createFragments(view, startOffset, endOffset, skipBidiLayout), false); } /** * Creates a layout for an arbitrary piece of text (using a common font style). */ - LineLayout(@NotNull EditorView view, - @NotNull CharSequence text, @JdkConstants.FontStyle int fontStyle, - @NotNull FontRenderContext fontRenderContext) { - this(createFragments(view, text, fontStyle, fontRenderContext), true); + LineLayout(@NotNull EditorView view, @NotNull CharSequence text, @JdkConstants.FontStyle int fontStyle) { + this(createFragments(view, text, fontStyle), true); } private LineLayout(@NotNull List runs, boolean calculateWidth) { myBidiRunsInLogicalOrder = runs.toArray(new BidiRun[runs.size()]); myBidiRunsInVisualOrder = myBidiRunsInLogicalOrder.clone(); - - reorderRunsVisually(myBidiRunsInVisualOrder); - int logicalColumn = 0; - for (BidiRun run : runs) { - logicalColumn = run.createIndices(logicalColumn); - } + reorderRunsVisually(myBidiRunsInVisualOrder); + myWidth = calculateWidth ? calculateWidth() : -1; } - - private static void reorderRunsVisually(BidiRun[] bidiRunsInLogicalOrder) { - if (bidiRunsInLogicalOrder.length > 1) { - byte[] levels = new byte[bidiRunsInLogicalOrder.length]; - for (int i = 0; i < bidiRunsInLogicalOrder.length; i++) { - levels[i] = bidiRunsInLogicalOrder[i].level; + + // runs are supposed to be in logical order initially + private static void reorderRunsVisually(BidiRun[] bidiRuns) { + if (bidiRuns.length > 1) { + byte[] levels = new byte[bidiRuns.length]; + for (int i = 0; i < bidiRuns.length; i++) { + levels[i] = bidiRuns[i].level; } - Bidi.reorderVisually(levels, 0, bidiRunsInLogicalOrder, 0, levels.length); + Bidi.reorderVisually(levels, 0, bidiRuns, 0, levels.length); } } + + static boolean isBidiLayoutRequired(@NotNull CharSequence text) { + char[] chars = CharArrayUtil.fromSequence(text); + return Bidi.requiresBidi(chars, 0, chars.length); + } - private static List createFragments(@NotNull EditorView view, int lineStartOffset, int lineEndOffset, - @NotNull FontRenderContext fontRenderContext) { + private static List createFragments(@NotNull EditorView view, int lineStartOffset, int lineEndOffset, boolean skipBidiLayout) { if (lineEndOffset <= lineStartOffset) return Collections.emptyList(); EditorImpl editor = view.getEditor(); - FontPreferences fontPreferences = editor.getColorsScheme().getFontPreferences(); - char[] chars = CharArrayUtil.fromSequence(editor.getDocument().getImmutableCharSequence(), lineStartOffset, lineEndOffset); - List runs = createRuns(editor, chars, lineStartOffset); - for (BidiRun run : runs) { - IterationState it = new IterationState(editor, lineStartOffset + run.startOffset, lineStartOffset + run.endOffset, - false, false, false, false); - while (!it.atEnd()) { - addFragments(run, chars, it.getStartOffset() - lineStartOffset, it.getEndOffset() - lineStartOffset, - it.getMergedAttributes().getFontType(), fontPreferences, fontRenderContext, view.getTabFragment()); - it.advance(); - } - assert !run.fragments.isEmpty(); + List runs; + if (skipBidiLayout) { + runs = Collections.singletonList(new BidiRun((byte)0, 0, lineEndOffset - lineStartOffset)); + } + else { + CharSequence text = editor.getDocument().getImmutableCharSequence().subSequence(lineStartOffset, lineEndOffset); + char[] chars = CharArrayUtil.fromSequence(text); + runs = createRuns(editor, chars, lineStartOffset); } return runs; } private static List createFragments(@NotNull EditorView view, @NotNull CharSequence text, - @JdkConstants.FontStyle int fontStyle, @NotNull FontRenderContext fontRenderContext) { + @JdkConstants.FontStyle int fontStyle) { if (text.length() == 0) return Collections.emptyList(); EditorImpl editor = view.getEditor(); + FontRenderContext fontRenderContext = view.getFontRenderContext(); FontPreferences fontPreferences = editor.getColorsScheme().getFontPreferences(); char[] chars = CharArrayUtil.fromSequence(text); List runs = createRuns(editor, chars, -1); for (BidiRun run : runs) { - addFragments(run, chars, run.startOffset, run.endOffset, fontStyle, fontPreferences, fontRenderContext, null); - assert !run.fragments.isEmpty(); + for (Chunk chunk : run.getChunks()) { + addFragments(chunk, run.isRtl(), chars, chunk.startOffset, chunk.endOffset, fontStyle, fontPreferences, fontRenderContext, null); + } } return runs; } - private static List createDummyFragment(@NotNull EditorView view, int lineLength) { - BidiRun run = new BidiRun((byte)0, 0, lineLength); - run.fragments.add(new DummyLineFragment(lineLength, view.getMaxCharWidth())); - return Collections.singletonList(run); - } - private static List createRuns(EditorImpl editor, char[] text, int startOffsetInEditor) { int textLength = text.length; if (editor.myDisableRtl) return Collections.singletonList(new BidiRun((byte)0, 0, textLength)); @@ -186,37 +170,39 @@ class LineLayout { } } - private static void addFragments(BidiRun run, char[] text, int start, int end, int fontStyle, + private static void addFragments(Chunk chunk, boolean isRtl, char[] text, int start, int end, int fontStyle, FontPreferences fontPreferences, FontRenderContext fontRenderContext, @Nullable TabFragment tabFragment) { + assert start < end; Font currentFont = null; int currentIndex = start; for(int i = start; i < end; i++) { char c = text[i]; if (c == '\t' && tabFragment != null) { - assert run.level == 0; - addTextFragmentIfNeeded(run, text, currentIndex, i, currentFont, fontRenderContext, run.isRtl()); - run.fragments.add(tabFragment); + assert !isRtl; + addTextFragmentIfNeeded(chunk, text, currentIndex, i, currentFont, fontRenderContext, false); + chunk.fragments.add(tabFragment); currentFont = null; currentIndex = i + 1; } else { Font font = ComplementaryFontsRegistry.getFontAbleToDisplay(c, fontStyle, fontPreferences).getFont(); if (!font.equals(currentFont)) { - addTextFragmentIfNeeded(run, text, currentIndex, i, currentFont, fontRenderContext, run.isRtl()); + addTextFragmentIfNeeded(chunk, text, currentIndex, i, currentFont, fontRenderContext, isRtl); currentFont = font; currentIndex = i; } } } - addTextFragmentIfNeeded(run, text, currentIndex, end, currentFont, fontRenderContext, run.isRtl()); + addTextFragmentIfNeeded(chunk, text, currentIndex, end, currentFont, fontRenderContext, isRtl); + assert !chunk.fragments.isEmpty(); } - private static void addTextFragmentIfNeeded(BidiRun run, char[] chars, int from, int to, Font font, + private static void addTextFragmentIfNeeded(Chunk chunk, char[] chars, int from, int to, Font font, FontRenderContext fontRenderContext, boolean isRtl) { if (to > from) { assert font != null; - run.fragments.add(new TextFragment(chars, from, to, isRtl, font, fontRenderContext)); + chunk.fragments.add(new TextFragment(chars, from, to, isRtl, font, fontRenderContext)); } } @@ -233,38 +219,24 @@ class LineLayout { return myWidth; } - int offsetToLogicalColumn(int offset) { - if (offset <= 0) return 0; - for (BidiRun run : myBidiRunsInLogicalOrder) { - if (offset <= run.endOffset) return run.offsetToLogicalColumn(offset); - } - return myBidiRunsInLogicalOrder.length == 0 ? 0 : myBidiRunsInLogicalOrder[myBidiRunsInLogicalOrder.length - 1].endLogicalColumn; - } - - int logicalColumnToOffset(int logicalColumn) { - if (logicalColumn <= 0) return 0; - for (BidiRun run : myBidiRunsInLogicalOrder) { - if (logicalColumn <= run.endLogicalColumn) return run.logicalColumnToOffset(logicalColumn); - } - return myBidiRunsInLogicalOrder.length == 0 ? 0 : myBidiRunsInLogicalOrder[myBidiRunsInLogicalOrder.length - 1].endOffset; - } - Iterable getFragmentsInVisualOrder(final float startX) { return new Iterable() { @Override public Iterator iterator() { - return new VisualOrderIterator(startX, 0, 0, 0, myBidiRunsInVisualOrder); + return new VisualOrderIterator(null, 0, startX, 0, 0, 0, myBidiRunsInVisualOrder); } }; } - Iterable getFragmentsInVisualOrder(final float startX, - final int startVisualColumn, - final int startOffset, - int endOffset) { + Iterable getFragmentsInVisualOrder(@NotNull final EditorView view, + final int lineStartOffset, + final float startX, + final int startVisualColumn, + final int startOffset, + int endOffset, + boolean quick) { assert startOffset <= endOffset; final BidiRun[] runs; - int startLogicalColumn = 0; if (startOffset == endOffset) { runs = new BidiRun[0]; } @@ -273,19 +245,16 @@ class LineLayout { for (BidiRun run : myBidiRunsInLogicalOrder) { if (run.endOffset <= startOffset) continue; if (run.startOffset >= endOffset) break; - runList.add(run.subRun(startOffset, endOffset)); - if (run.startOffset <= startOffset) { - startLogicalColumn = run.offsetToLogicalColumn(startOffset); - } + runList.add(run.subRun(view, lineStartOffset, startOffset, endOffset, quick)); } runs = runList.toArray(new BidiRun[runList.size()]); reorderRunsVisually(runs); } - final int finalStartLogicalColumn = startLogicalColumn; + final int startLogicalColumn = view.offsetToLogicalPosition(lineStartOffset + startOffset).column; return new Iterable() { @Override public Iterator iterator() { - return new VisualOrderIterator(startX, startVisualColumn, finalStartLogicalColumn, startOffset, runs); + return new VisualOrderIterator(view, lineStartOffset, startX, startVisualColumn, startLogicalColumn, startOffset, runs); } }; } @@ -332,17 +301,12 @@ class LineLayout { } private static class BidiRun { + private static final int CHUNK_CHARACTERS = 1024; + private final byte level; private final int startOffset; private final int endOffset; - private final List fragments = new ArrayList(); // in logical order - - // values cached for performance - private static final int INDEX_FREQUENCY = 100; - private int startLogicalColumn; - private int endLogicalColumn; - private int[] offsetIndex; // stores start offsets for each INDEX_FREQUENCY-th fragment, for faster lookup by offset - private int[] logicalColumnIndex; // stores start logical column for each INDEX_FREQUENCY-th fragment, for faster offset <-> logical column conversions + private Chunk[] chunks; // in logical order private BidiRun(byte level, int startOffset, int endOffset) { this.level = level; @@ -353,124 +317,108 @@ class LineLayout { private boolean isRtl() { return (level & 1) != 0; } - - private BidiRun subRun(int targetStartOffset, int targetEndOffset) { - assert targetStartOffset < endOffset; - assert targetEndOffset > startOffset; - if (targetStartOffset <= startOffset && targetEndOffset >= this.endOffset) { - return this; - } - int start = Math.max(startOffset, targetStartOffset); - int end = Math.min(endOffset, targetEndOffset); - BidiRun run = new BidiRun(level, start, end); - int offset = startOffset; - int startFragment = 0; - if (offsetIndex != null) { - int pos = Arrays.binarySearch(offsetIndex, start); - if (pos < 0) pos = - pos - 2; - if (pos >= 0) { - startFragment = (pos + 1) * INDEX_FREQUENCY; - offset = offsetIndex[pos]; + + private Chunk[] getChunks() { + if (chunks == null) { + int chunkCount = (endOffset - startOffset + CHUNK_CHARACTERS - 1) / CHUNK_CHARACTERS; + chunks = new Chunk[chunkCount]; + for (int i = 0; i < chunkCount; i++) { + int from = startOffset + i * CHUNK_CHARACTERS; + int to = (i == chunkCount - 1) ? endOffset : from + CHUNK_CHARACTERS; + Chunk chunk = new Chunk(from, to); + chunks[i] = chunk; } } - for (int i = startFragment; i < fragments.size(); i++) { - LineFragment fragment = fragments.get(i); + return chunks; + } + + private BidiRun subRun(@NotNull EditorView view, int lineStartOffset, int targetStartOffset, int targetEndOffset, boolean quick) { + assert targetStartOffset < endOffset; + assert targetEndOffset > startOffset; + int start = Math.max(startOffset, targetStartOffset); + int end = Math.min(endOffset, targetEndOffset); + BidiRun subRun = new BidiRun(level, start, end); + List subChunks = new ArrayList(); + for (Chunk chunk : getChunks()) { + if (chunk.endOffset <= start) continue; + if (chunk.startOffset >= end) break; + subChunks.add(chunk.subChunk(view, lineStartOffset, isRtl(), start, end, quick)); + } + subRun.chunks = subChunks.toArray(new Chunk[subChunks.size()]); + return subRun; + } + } + + private static class Chunk { + private final List fragments = new ArrayList(); // in logical order + private int startOffset; + private int endOffset; + + private Chunk(int startOffset, int endOffset) { + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + private void ensureLayout(@NotNull EditorView view, int lineStartOffset, boolean isRtl) { + if (!fragments.isEmpty()) return; + int start = lineStartOffset + startOffset; + int end = lineStartOffset + endOffset; + IterationState it = new IterationState(view.getEditor(), start, end, false, false, false, false); + FontPreferences fontPreferences = view.getEditor().getColorsScheme().getFontPreferences(); + char[] chars = CharArrayUtil.fromSequence(view.getEditor().getDocument().getImmutableCharSequence(), start, end); + while (!it.atEnd()) { + addFragments(this, isRtl, chars, it.getStartOffset() - start, it.getEndOffset() - start, + it.getMergedAttributes().getFontType(), fontPreferences, view.getFontRenderContext(), view.getTabFragment()); + it.advance(); + } + view.getSizeManager().textLayoutPerformed(start, end); + assert !fragments.isEmpty(); + } + + private Chunk subChunk(EditorView view, int lineStartOffset, boolean isRtl, int targetStartOffset, int targetEndOffset, boolean quick) { + 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 (start == startOffset && end == this.endOffset) { + return this; + } + ensureLayout(view, lineStartOffset, isRtl); + Chunk chunk = new Chunk(start, end); + int offset = startOffset; + for (LineFragment fragment : fragments) { if (end <= offset) break; int endOffset = offset + fragment.getLength(); if (start < endOffset) { - run.fragments.add(fragment.subFragment(Math.max(start, offset) - offset, Math.min(end, endOffset) - offset)); + chunk.fragments.add(fragment.subFragment(Math.max(start, offset) - offset, Math.min(end, endOffset) - offset)); } offset = endOffset; } - return run; - } - - private int logicalColumnToOffset(int logicalColumn) { - if (logicalColumn <= startLogicalColumn) return startOffset; - if (logicalColumn >= endLogicalColumn) return endOffset; - int currentOffset = startOffset; - int currentLogicalColumn = startLogicalColumn; - int startFragment = 0; - if (offsetIndex != null) { - int pos = Arrays.binarySearch(logicalColumnIndex == null ? offsetIndex : logicalColumnIndex, logicalColumn); - if (pos >= 0) return offsetIndex[pos]; - int beforeInsertPosition = - pos - 2; - currentOffset = beforeInsertPosition < 0 ? startOffset : offsetIndex[beforeInsertPosition]; - currentLogicalColumn = beforeInsertPosition < 0 ? startLogicalColumn : logicalColumnIndex == null ? currentOffset : - logicalColumnIndex[beforeInsertPosition]; - startFragment = INDEX_FREQUENCY * (beforeInsertPosition + 1); - } - for (int i = startFragment; i < fragments.size(); i++) { - LineFragment fragment = fragments.get(i); - int endLogicalColumn = currentLogicalColumn + fragment.getLogicalColumnCount(currentLogicalColumn); - if (logicalColumn <= endLogicalColumn) return currentOffset + fragment.logicalColumnToOffset(currentLogicalColumn, logicalColumn); - currentOffset += fragment.getLength(); - currentLogicalColumn = endLogicalColumn; - } - throw new IllegalStateException(); - } - - private int offsetToLogicalColumn(int offset) { - if (offset <= startOffset) return startLogicalColumn; - if (offset >= endOffset) return endLogicalColumn; - int currentOffset = startOffset; - int currentLogicalColumn = startLogicalColumn; - int startFragment = 0; - if (offsetIndex != null) { - int pos = Arrays.binarySearch(offsetIndex, offset); - if (pos >= 0) return logicalColumnIndex == null ? offset : logicalColumnIndex[pos]; - int beforeInsertPosition = - pos - 2; - currentOffset = beforeInsertPosition < 0 ? startOffset : offsetIndex[beforeInsertPosition]; - currentLogicalColumn = beforeInsertPosition < 0 ? startLogicalColumn : logicalColumnIndex == null ? currentOffset : - logicalColumnIndex[beforeInsertPosition]; - startFragment = INDEX_FREQUENCY * (beforeInsertPosition + 1); - } - for (int i = startFragment; i < fragments.size(); i++) { - LineFragment fragment = fragments.get(i); - int endOffset = currentOffset + fragment.getLength(); - if (offset <= endOffset) return fragment.offsetToLogicalColumn(currentLogicalColumn, offset - currentOffset); - currentOffset = endOffset; - currentLogicalColumn += fragment.getLogicalColumnCount(currentLogicalColumn); - } - throw new IllegalStateException(); - } - - private int createIndices(int startLogicalColumn) { - this.startLogicalColumn = endLogicalColumn = startLogicalColumn; - int indexSize = (fragments.size() - 1) / INDEX_FREQUENCY; - if (indexSize > 0) { - offsetIndex = new int[indexSize]; - } - int i = 0; - int offset = startOffset; - for (LineFragment fragment : fragments) { - if (offsetIndex != null && i > 0 && (i % INDEX_FREQUENCY) == 0) { - int pos = i / INDEX_FREQUENCY - 1; - offsetIndex[pos] = offset; - if (logicalColumnIndex == null && offset != endLogicalColumn) { - logicalColumnIndex = new int[indexSize]; - System.arraycopy(offsetIndex, 0, logicalColumnIndex, 0, pos); - } - if (logicalColumnIndex != null) { - logicalColumnIndex[pos] = endLogicalColumn; - } - } - i++; - offset += fragment.getLength(); - endLogicalColumn += fragment.getLogicalColumnCount(endLogicalColumn); - } - return endLogicalColumn; + return chunk; } } private static class VisualOrderIterator implements Iterator { - private BidiRun[] myRuns; + private final EditorView myView; + private final int myLineStartOffset; + private final BidiRun[] myRuns; private int myRunIndex = 0; + private int myChunkIndex = 0; private int myFragmentIndex = 0; private int myOffsetInsideRun = 0; private VisualFragment myFragment = new VisualFragment(); - private VisualOrderIterator(float startX, int startVisualColumn, int startLogicalColumn, int startOffset, BidiRun[] runsInVisualOrder) { + private VisualOrderIterator(EditorView view, int lineStartOffset, + float startX, int startVisualColumn, int startLogicalColumn, int startOffset, BidiRun[] runsInVisualOrder) { + myView = view; + myLineStartOffset = lineStartOffset; myRuns = runsInVisualOrder; myFragment.startX = startX; myFragment.startVisualColumn = startVisualColumn; @@ -480,7 +428,15 @@ class LineLayout { @Override public boolean hasNext() { - return myRunIndex < myRuns.length && myFragmentIndex < myRuns[myRunIndex].fragments.size(); + if (myRunIndex >= myRuns.length) return false; + BidiRun run = myRuns[myRunIndex]; + Chunk[] chunks = run.getChunks(); + if (myChunkIndex >= chunks.length) return false; + Chunk chunk = chunks[run.isRtl() ? chunks.length - 1 - myChunkIndex : myChunkIndex]; + if (myView != null) { + chunk.ensureLayout(myView, myLineStartOffset, run.isRtl()); + } + return myFragmentIndex < chunk.fragments.size(); } @Override @@ -490,12 +446,12 @@ class LineLayout { } BidiRun run = myRuns[myRunIndex]; - if (myRunIndex == 0 && myFragmentIndex == 0) { + if (myRunIndex == 0 && myChunkIndex == 0 && myFragmentIndex == 0) { myFragment.startLogicalColumn += (run.isRtl() ? run.endOffset : run.startOffset) - myFragment.startOffset; } else { myFragment.startLogicalColumn = myFragment.getEndLogicalColumn(); - if (myFragmentIndex == 0) { + if (myChunkIndex == 0 && myFragmentIndex == 0) { myFragment.startLogicalColumn += (run.isRtl() ? run.endOffset : run.startOffset) - myFragment.getEndOffset(); } myFragment.startVisualColumn = myFragment.getEndVisualColumn(); @@ -503,15 +459,22 @@ class LineLayout { } myFragment.isRtl = run.isRtl(); - myFragment.delegate = run.fragments.get(run.isRtl() ? run.fragments.size() - 1 - myFragmentIndex : myFragmentIndex); + Chunk[] chunks = run.getChunks(); + Chunk chunk = chunks[run.isRtl() ? chunks.length - 1 - myChunkIndex : myChunkIndex]; + assert !chunk.fragments.isEmpty(); + myFragment.delegate = chunk.fragments.get(run.isRtl() ? chunk.fragments.size() - 1 - myFragmentIndex : myFragmentIndex); myFragment.startOffset = run.isRtl() ? run.endOffset - myOffsetInsideRun : run.startOffset + myOffsetInsideRun; myOffsetInsideRun += myFragment.getLength(); myFragmentIndex++; - if (myFragmentIndex >= run.fragments.size()) { + if (myFragmentIndex >= chunk.fragments.size()) { myFragmentIndex = 0; - myOffsetInsideRun = 0; - myRunIndex++; + myChunkIndex++; + if (myChunkIndex >= chunks.length) { + myChunkIndex = 0; + myOffsetInsideRun = 0; + myRunIndex++; + } } return myFragment; @@ -649,77 +612,4 @@ class LineLayout { return isRtl ? startOffset - offset : offset - startOffset; } } - - public static class DummyLineFragment implements LineFragment { - private final int myLength; - private final float myCharWidth; - - public DummyLineFragment(int length, float charWidth) { - myLength = length; - myCharWidth = charWidth; - } - - @Override - public int getLength() { - return myLength; - } - - @Override - public int getLogicalColumnCount(int startColumn) { - return myLength; - } - - @Override - public int getVisualColumnCount(float startX) { - return myLength; - } - - @Override - public int offsetToLogicalColumn(int startColumn, int offset) { - return startColumn + offset; - } - - @Override - public int logicalColumnToOffset(int startColumn, int column) { - return column - startColumn; - } - - @Override - public int logicalToVisualColumn(float startX, int startColumn, int column) { - return column; - } - - @Override - public int visualToLogicalColumn(float startX, int startColumn, int column) { - return column; - } - - @Override - public float visualColumnToX(float startX, int column) { - return column * myCharWidth; - } - - @Override - public int[] xToVisualColumn(float startX, float x) { - float relX = x - startX; - int column = Math.max(0, Math.min(myLength, Math.round(relX / myCharWidth))); - return new int[] {column, relX <= column * myCharWidth ? 0 : 1}; - } - - @Override - public float offsetToX(float startX, int startOffset, int offset) { - return offset * myCharWidth; - } - - @Override - public void draw(Graphics2D g, float x, float y, int startColumn, int endColumn) { - throw new UnsupportedOperationException(); - } - - @NotNull - @Override - public LineFragment subFragment(int startOffset, int endOffset) { - return new DummyLineFragment(endOffset - startOffset, myCharWidth); - } - } } 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 new file mode 100644 index 000000000000..b86b722d8a09 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/LogicalPositionCache.java @@ -0,0 +1,205 @@ +/* + * 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.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.text.CharArrayUtil; +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 { + private final Document myDocument; + private final EditorView myView; + private ArrayList myLines = new ArrayList(); + private int myTabSize = -1; + private int myDocumentChangeOldEndLine; + + 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) { + 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, CharArrayUtil.indexOf(event.getNewFragment(), "\t", 0) == -1); + } + + synchronized void reset(boolean force) { + checkDisposed(); + int oldTabSize = myTabSize; + myTabSize = myView.getTabSize(); + if (force || oldTabSize != myTabSize) { + invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1, true); + } + } + + @NotNull + synchronized LogicalPosition offsetToLogicalPosition(int offset) { + 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 logicalPositionToOffset(@NotNull LogicalPosition pos) { + int line = pos.line; + if (line >= myDocument.getLineCount()) return myDocument.getTextLength(); + LineData lineData = getLineInfo(line); + return lineData.logicalColumnToOffset(myDocument, line, myTabSize, pos.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(); + 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 (oldEndLine < newEndLine) { + myLines.addAll(oldEndLine + 1, Collections.nCopies(newEndLine - oldEndLine, (LineData)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 = new LineData(myDocument, line, myTabSize); + myLines.set(line, result); + } + return result; + } + + @Override + public synchronized void dispose() { + myLines = null; + } + + private void checkDisposed() { + if (myLines == null) throw new IllegalStateException("Editor is already disposed"); + } + + private static class 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(@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 hasTabs = false; + for (int i = start; i < end; i++) { + if (i > start && (i - start) % CACHE_FREQUENCY == 0) { + cache[(i - start) / CACHE_FREQUENCY - 1] = column; + } + if (text.charAt(i) == '\t') { + column = (column / tabSize + 1) * tabSize; + hasTabs = true; + } + else { + column++; + } + } + columnCache = hasTabs ? cache : null; + } + + 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 column = cacheIndex == 0 ? 0 : columnCache[cacheIndex - 1]; + CharSequence text = document.getImmutableCharSequence(); + for (int i = startOffset; i < offset; i++) { + if (text.charAt(i) == '\t') { + column = (column / tabSize + 1) * tabSize; + } + else { + column++; + } + } + return column; + } + + 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) return lineStartOffset + (pos + 1) * CACHE_FREQUENCY; + int startOffset = lineStartOffset + (- pos - 1) * CACHE_FREQUENCY; + int column = pos == -1 ? 0 : columnCache[- pos - 2]; + CharSequence text = document.getImmutableCharSequence(); + for (int i = startOffset; i < lineEndOffset; i++) { + if (text.charAt(i) == '\t') { + column = (column / tabSize + 1) * tabSize; + } + else { + column++; + } + if (logicalColumn < column) return i; + } + return lineEndOffset; + } + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TabFragment.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TabFragment.java index 5dbd1f503ea8..1b2f2135572d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TabFragment.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TabFragment.java @@ -50,16 +50,6 @@ class TabFragment implements LineFragment { return EditorUtil.columnsNumber((int)(x - startX), myView.getPlainSpaceWidth()); } - @Override - public int offsetToLogicalColumn(int startColumn, int offset) { - return startColumn + (offset == 0 ? 0 : getLogicalColumnCount(startColumn)); - } - - @Override - public int logicalColumnToOffset(int startColumn, int column) { - return column < (startColumn + getLogicalColumnCount(startColumn)) ? 0 : 1; - } - @Override public void draw(Graphics2D g, float x, float y, int startColumn, int endColumn) { } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextFragment.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextFragment.java index 2dcb3be2d09f..c11a07a8075c 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextFragment.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/TextFragment.java @@ -72,16 +72,6 @@ class TextFragment implements LineFragment { return myCharPositions.length; } - @Override - public int offsetToLogicalColumn(int startColumn, int offset) { - return startColumn + offset; - } - - @Override - public int logicalColumnToOffset(int startColumn, int column) { - return column - startColumn; - } - @Override public void draw(Graphics2D g, float x, float y, int startColumn, int endColumn) { assert startColumn >= 0; @@ -201,16 +191,6 @@ class TextFragment implements LineFragment { return getLength(); } - @Override - public int offsetToLogicalColumn(int startColumn, int offset) { - return startColumn + offset; - } - - @Override - public int logicalColumnToOffset(int startColumn, int column) { - return column - startColumn; - } - @Override public int logicalToVisualColumn(float startX, int startColumn, int column) { return column; 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 4c6d884fa60a..1e30841f26dc 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 @@ -22,24 +22,28 @@ import com.intellij.openapi.editor.ex.PrioritizedDocumentListener; import com.intellij.openapi.editor.impl.EditorDocumentPriorities; import org.jetbrains.annotations.NotNull; +import java.awt.*; import java.util.ArrayList; import java.util.Collections; /** - * Editor text layout storage. It's cached on a per-line basis, created lazily (when requested), and invalidated on document changes or - * when explicitly requested. + * 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 final EditorView myView; private final Document myDocument; + private final LineLayout myBidiNotRequiredMarker; private ArrayList myLines = new ArrayList(); private int myDocumentChangeOldEndLine; - private boolean myQuickLayoutsEnabled; TextLayoutCache(EditorView view) { myView = view; myDocument = view.getEditor().getDocument(); myDocument.addDocumentListener(this, this); + myBidiNotRequiredMarker = new LineLayout(view, "", Font.PLAIN); } @Override @@ -56,7 +60,7 @@ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { public void documentChanged(DocumentEvent event) { int startLine = myDocument.getLineNumber(event.getOffset()); int newEndLine = getAdjustedLineNumber(event.getOffset() + event.getNewLength()); - invalidateLines(startLine, myDocumentChangeOldEndLine, newEndLine); + invalidateLines(startLine, myDocumentChangeOldEndLine, newEndLine, !LineLayout.isBidiLayoutRequired(event.getNewFragment())); } @Override @@ -67,17 +71,24 @@ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { private int getAdjustedLineNumber(int offset) { return myDocument.getTextLength() == 0 ? -1 : myDocument.getLineNumber(offset); } - - void resetToDocumentSize() { + + void resetToDocumentSize(boolean documentChangedWithoutNotification) { checkDisposed(); - invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1); + invalidateLines(0, myLines.size() - 1, myDocument.getLineCount() - 1, !documentChangedWithoutNotification); + } + + void invalidateLines(int startLine, int endLine) { + invalidateLines(startLine, endLine, endLine, true); } - void invalidateLines(int startLine, int oldEndLine, int newEndLine) { + private void invalidateLines(int startLine, int oldEndLine, int newEndLine, boolean keepBidiNotRequiredState) { checkDisposed(); int endLine = Math.min(oldEndLine, newEndLine); for (int line = startLine; line <= endLine; line++) { - myLines.set(line, null); + LineLayout lineLayout = myLines.get(line); + if (lineLayout != null) { + myLines.set(line, keepBidiNotRequiredState && lineLayout.isLtr() ? myBidiNotRequiredMarker : null); + } } if (oldEndLine < newEndLine) { myLines.addAll(oldEndLine + 1, Collections.nCopies(newEndLine - oldEndLine, (LineLayout)null)); @@ -85,31 +96,25 @@ class TextLayoutCache implements PrioritizedDocumentListener, Disposable { myLines.subList(newEndLine + 1, oldEndLine + 1).clear(); } } - + @NotNull LineLayout getLineLayout(int line) { checkDisposed(); LineLayout result = myLines.get(line); - if (result == null) { - int lineStart = myDocument.getLineStartOffset(line); - int lineEnd = myDocument.getLineEndOffset(line); - if (myQuickLayoutsEnabled) { - result = new LineLayout(myView, lineEnd - lineStart); - myView.getSizeManager().quickLineLayoutCreated(); - } - else { - result = new LineLayout(myView, lineStart, lineEnd, myView.getFontRenderContext()); - myLines.set(line, result); - myView.getSizeManager().lineLayoutCreated(line); - } + if (result == null || result == myBidiNotRequiredMarker) { + result = createLineLayout(line, result == myBidiNotRequiredMarker); + myLines.set(line, result); } return result; } - - void enableQuickLayouts(boolean enabled) { - myQuickLayoutsEnabled = enabled; + + @NotNull + private LineLayout createLineLayout(int line, boolean skipBidiLayout) { + int lineStart = myDocument.getLineStartOffset(line); + int lineEnd = myDocument.getLineEndOffset(line); + return new LineLayout(myView, lineStart, lineEnd, skipBidiLayout); } - + 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 0db5dbcaefc4..2afdcb30e755 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 @@ -34,11 +34,11 @@ import java.util.NoSuchElementException; */ class VisualLineFragmentsIterator implements Iterator { - static Iterable create(final EditorView view, final int offset, final boolean beforeSoftWrap) { + static Iterable create(final EditorView view, final int offset, final boolean beforeSoftWrap, final boolean quick) { return new Iterable() { @Override public Iterator iterator() { - return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap); + return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap, quick); } }; } @@ -48,6 +48,7 @@ class VisualLineFragmentsIterator implements Iterator