editor new rendering: speed up opening and editing for documents with long lines

This commit is contained in:
Dmitry Batrak
2015-10-02 17:47:41 +03:00
parent b1f772eb31
commit df3694f025
14 changed files with 575 additions and 468 deletions
@@ -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;
@@ -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() {
@@ -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();
}
}
@@ -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;
@@ -465,7 +465,7 @@ class EditorPainter implements TextDrawingCallback {
Collection<LineExtensionInfo> 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();
@@ -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<TextRange> myDeferredRanges = new ArrayList<TextRange>();
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);
@@ -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;
@@ -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);
@@ -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<BidiRun> 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<BidiRun> createFragments(@NotNull EditorView view, int lineStartOffset, int lineEndOffset,
@NotNull FontRenderContext fontRenderContext) {
private static List<BidiRun> 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<BidiRun> 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<BidiRun> 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<BidiRun> 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<BidiRun> 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<BidiRun> 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<BidiRun> 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<VisualFragment> getFragmentsInVisualOrder(final float startX) {
return new Iterable<VisualFragment>() {
@Override
public Iterator<VisualFragment> iterator() {
return new VisualOrderIterator(startX, 0, 0, 0, myBidiRunsInVisualOrder);
return new VisualOrderIterator(null, 0, startX, 0, 0, 0, myBidiRunsInVisualOrder);
}
};
}
Iterable<VisualFragment> getFragmentsInVisualOrder(final float startX,
final int startVisualColumn,
final int startOffset,
int endOffset) {
Iterable<VisualFragment> 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<VisualFragment>() {
@Override
public Iterator<VisualFragment> 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<LineFragment> fragments = new ArrayList<LineFragment>(); // 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<Chunk> subChunks = new ArrayList<Chunk>();
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<LineFragment> fragments = new ArrayList<LineFragment>(); // 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<VisualFragment> {
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);
}
}
}
@@ -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<LineData> myLines = new ArrayList<LineData>();
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;
}
}
}
@@ -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) {
}
@@ -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;
@@ -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<LineLayout> myLines = new ArrayList<LineLayout>();
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");
}
@@ -34,11 +34,11 @@ import java.util.NoSuchElementException;
*/
class VisualLineFragmentsIterator implements Iterator<VisualLineFragmentsIterator.Fragment> {
static Iterable<Fragment> create(final EditorView view, final int offset, final boolean beforeSoftWrap) {
static Iterable<Fragment> create(final EditorView view, final int offset, final boolean beforeSoftWrap, final boolean quick) {
return new Iterable<Fragment>() {
@Override
public Iterator<Fragment> iterator() {
return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap);
return new VisualLineFragmentsIterator(view, offset, beforeSoftWrap, quick);
}
};
}
@@ -48,6 +48,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 int mySegmentStartOffset;
private int mySegmentEndOffset;
@@ -61,7 +62,8 @@ class VisualLineFragmentsIterator implements Iterator<VisualLineFragmentsIterato
private int myCurrentEndLogicalLine;
private int myNextWrapOffset;
private VisualLineFragmentsIterator(EditorView view, int offset, boolean beforeSoftWrap) {
private VisualLineFragmentsIterator(EditorView view, int offset, boolean beforeSoftWrap, boolean quick) {
myQuick = quick;
myView = view;
EditorImpl editor = view.getEditor();
myDocument = editor.getDocument();
@@ -108,9 +110,9 @@ class VisualLineFragmentsIterator implements Iterator<VisualLineFragmentsIterato
int line = myDocument.getLineNumber(mySegmentStartOffset);
mySegmentEndOffset = Math.min(myNextWrapOffset, Math.min(mySegmentEndOffset, myDocument.getLineEndOffset(line)));
int lineStartOffset = myDocument.getLineStartOffset(line);
myFragmentIterator = myView.getTextLayoutCache().getLineLayout(line).getFragmentsInVisualOrder(myCurrentX, myCurrentVisualColumn,
mySegmentStartOffset - lineStartOffset,
mySegmentEndOffset - lineStartOffset).iterator();
myFragmentIterator = myView.getTextLayoutCache().getLineLayout(line).
getFragmentsInVisualOrder(myView, lineStartOffset, myCurrentX, myCurrentVisualColumn,
mySegmentStartOffset - lineStartOffset, mySegmentEndOffset - lineStartOffset, myQuick).iterator();
}
}