/*
* Copyright 2000-2016 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.lang.CodeDocumentationAwareCommenter;
import com.intellij.lang.Commenter;
import com.intellij.lang.LanguageCommenters;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.bidi.BidiRegionsSeparator;
import com.intellij.openapi.editor.bidi.LanguageBidiRegionsSeparator;
import com.intellij.openapi.editor.colors.FontPreferences;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.impl.ComplementaryFontsRegistry;
import com.intellij.openapi.editor.impl.FontInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.StringEscapesTokenTypes;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.BitUtil;
import com.intellij.util.DocumentUtil;
import com.intellij.util.text.CharArrayUtil;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.text.Bidi;
import java.util.*;
import java.util.List;
import java.util.stream.Stream;
/**
* 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.
*/
abstract class LineLayout {
private static final Logger LOG = Logger.getInstance(LineLayout.class);
private LineLayout() {}
/**
* Creates a layout for a fragment of text from editor.
*/
@NotNull
static LineLayout create(@NotNull EditorView view, int line, boolean skipBidiLayout) {
List<BidiRun> runs = createFragments(view, line, skipBidiLayout);
return createLayout(view, runs, null, line);
}
/**
* Creates a layout for an arbitrary piece of text (using a common font style).
*/
@NotNull
static LineLayout create(@NotNull EditorView view, @NotNull CharSequence text, @JdkConstants.FontStyle int fontStyle) {
List<BidiRun> runs = createFragments(view, text, fontStyle);
LineLayout delegate = createLayout(view, runs, text, 0);
return new WithSize(delegate);
}
private static LineLayout createLayout(@NotNull EditorView view, @NotNull List<BidiRun> runs, @Nullable CharSequence text, int line) {
if (runs.isEmpty()) return new SingleChunk(null);
if (runs.size() == 1) {
BidiRun run = runs.get(0);
if (run.level == 0 && run.getChunkCount() == 1) {
Chunk chunk = run.chunks == null ? new Chunk(0, run.endOffset) : run.chunks[0];
return new SingleChunk(chunk);
}
}
BidiRun[] runArray = new BidiRun[runs.size()];
int prevColumn = 0;
for (int i = 0; i < runs.size(); i++) {
BidiRun run = runs.get(i);
assert i == 0 || run.startOffset == runs.get(i - 1).endOffset;
int startColumn = prevColumn;
int endColumn = text == null
? view.getLogicalPositionCache().offsetToLogicalColumn(line, run.endOffset)
: LogicalPositionCache.calcColumn(text, run.startOffset, prevColumn, run.endOffset, view.getTabSize());
run.visualStartLogicalColumn = run.isRtl() ? endColumn : startColumn;
prevColumn = endColumn;
runArray[i] = run;
}
return new MultiChunk(runArray);
}
// runs are supposed to be in logical order initially
private static void reorderRunsVisually(BidiRun[] bidiRuns) {
byte[] levels = new byte[bidiRuns.length];
for (int i = 0; i < bidiRuns.length; i++) {
levels[i] = bidiRuns[i].level;
}
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 line, boolean skipBidiLayout) {
Document document = view.getEditor().getDocument();
int lineStartOffset = document.getLineStartOffset(line);
int lineEndOffset = document.getLineEndOffset(line);
if (lineEndOffset <= lineStartOffset) return Collections.emptyList();
if (skipBidiLayout) return Collections.singletonList(new BidiRun(lineEndOffset - lineStartOffset));
CharSequence text = document.getImmutableCharSequence().subSequence(lineStartOffset, lineEndOffset);
char[] chars = CharArrayUtil.fromSequence(text);
return createRuns(view, chars, lineStartOffset);
}
private static List<BidiRun> createFragments(@NotNull EditorView view, @NotNull CharSequence text,
@JdkConstants.FontStyle int fontStyle) {
if (text.length() == 0) return Collections.emptyList();
FontRenderContext fontRenderContext = view.getFontRenderContext();
FontPreferences fontPreferences = view.getEditor().getColorsScheme().getFontPreferences();
char[] chars = CharArrayUtil.fromSequence(text);
List<BidiRun> runs = createRuns(view, chars, -1);
for (BidiRun run : runs) {
for (Chunk chunk : run.getChunks(text, 0)) {
chunk.fragments = new ArrayList<>();
addFragments(run, chunk, chars, chunk.startOffset, chunk.endOffset, fontStyle, fontPreferences, fontRenderContext, null);
}
}
return runs;
}
private static List<BidiRun> createRuns(EditorView view, char[] text, int startOffsetInEditor) {
int textLength = text.length;
if (view.getEditor().myDisableRtl || !Bidi.requiresBidi(text, 0, textLength)) {
return Collections.singletonList(new BidiRun(textLength));
}
List<BidiRun> runs = new ArrayList<>();
int flags = view.getBidiFlags();
if (startOffsetInEditor >= 0) {
// running bidi algorithm separately for text fragments corresponding to different lexer tokens
int relLastOffset = 0;
IElementType lastToken = null;
HighlighterIterator iterator = view.getEditor().getHighlighter().createIterator(startOffsetInEditor);
while (!iterator.atEnd() && (iterator.getStart() - startOffsetInEditor) < textLength) {
int iteratorRelStart = alignToCodePointBoundary(text, iterator.getStart() - startOffsetInEditor);
int iteratorRelEnd = alignToCodePointBoundary(text, iterator.getEnd() - startOffsetInEditor);
IElementType currentToken = iterator.getTokenType();
int relStartOffset = Math.max(0, iteratorRelStart);
String lcPrefix = getLineCommentPrefix(currentToken);
// for line comments we process prefix and following text separately
if (!StringUtil.isEmpty(lcPrefix) && lcPrefix.length() <= (iteratorRelEnd - iteratorRelStart) &&
CharArrayUtil.regionMatches(text, relStartOffset, relStartOffset + lcPrefix.length(), lcPrefix) &&
!isInsideSurrogatePair(text, relStartOffset + lcPrefix.length())) {
addRuns(runs, text, relLastOffset, relStartOffset, flags);
int textStartOffset = Math.min(textLength, Math.min(iteratorRelEnd,
CharArrayUtil.shiftForward(text, relStartOffset + lcPrefix.length(), " \t")));
relLastOffset = Math.min(iteratorRelEnd, textLength);
lastToken = null;
addRuns(runs, text, relStartOffset, textStartOffset, flags);
addRuns(runs, text, textStartOffset, relLastOffset, flags);
}
else if (distinctTokens(lastToken, currentToken)) {
addRuns(runs, text, relLastOffset, relStartOffset, flags);
lastToken = currentToken;
relLastOffset = relStartOffset;
}
iterator.advance();
}
addRuns(runs, text, relLastOffset, textLength, flags);
}
else {
addRuns(runs, text, 0, textLength, flags);
}
for (BidiRun run : runs) {
assert !isInsideSurrogatePair(text, run.startOffset);
assert !isInsideSurrogatePair(text, run.endOffset);
}
return runs;
}
private static boolean isInsideSurrogatePair(char[] text, int offset) {
return offset > 0 && offset < text.length && Character.isHighSurrogate(text[offset - 1]) && Character.isLowSurrogate(text[offset]);
}
private static int alignToCodePointBoundary(char[] text, int offset) {
return isInsideSurrogatePair(text, offset) ? offset - 1 : offset;
}
private static int alignToCodePointBoundary(CharSequence text, int offset) {
return offset > 0 && offset < text.length() &&
Character.isHighSurrogate(text.charAt(offset - 1)) && Character.isLowSurrogate(text.charAt(offset)) ? offset - 1 : offset;
}
private static String getLineCommentPrefix(IElementType token) {
if (token == null) return null;
Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(token.getLanguage());
return (commenter instanceof CodeDocumentationAwareCommenter) &&
token.equals(((CodeDocumentationAwareCommenter)commenter).getLineCommentTokenType()) ?
commenter.getLineCommentPrefix() : null;
}
private static boolean distinctTokens(@Nullable IElementType token1, @Nullable IElementType token2) {
if (token1 == token2) return false;
if (token1 == null || token2 == null) return true;
if (StringEscapesTokenTypes.STRING_LITERAL_ESCAPES.contains(token1) ||
StringEscapesTokenTypes.STRING_LITERAL_ESCAPES.contains(token2)) return false;
if (!token1.getLanguage().is(token2.getLanguage())) return true;
BidiRegionsSeparator separator = LanguageBidiRegionsSeparator.INSTANCE.forLanguage(token1.getLanguage());
return separator.createBorderBetweenTokens(token1, token2);
}
private static void addRuns(List<BidiRun> runs, char[] text, int start, int end, int flags) {
if (start < end && !Bidi.requiresBidi(text, start, end)) {
addOrMergeRun(runs, new BidiRun((byte)0, start, end));
return;
}
int afterLastTabPosition = start;
for (int i = start; i < end; i++) {
if (text[i] == '\t') {
addRunsNoTabs(runs, text, afterLastTabPosition, i, flags);
afterLastTabPosition = i + 1;
addOrMergeRun(runs, new BidiRun((byte)0, i, i + 1));
}
}
addRunsNoTabs(runs, text, afterLastTabPosition, end, flags);
}
private static void addRunsNoTabs(List<BidiRun> runs, char[] text, int start, int end, int flags) {
if (start >= end) return;
Bidi bidi = new Bidi(text, start, null, 0, end - start, flags);
int runCount = bidi.getRunCount();
for (int i = 0; i < runCount; i++) {
addOrMergeRun(runs, new BidiRun((byte)bidi.getRunLevel(i), start + bidi.getRunStart(i), start + bidi.getRunLimit(i)));
}
}
private static void addOrMergeRun(List<BidiRun> runs, BidiRun run) {
int size = runs.size();
if (size > 0 && runs.get(size - 1).level == 0 && run.level == 0) {
BidiRun lastRun = runs.remove(size - 1);
assert lastRun.endOffset == run.startOffset;
runs.add(new BidiRun((byte)0, lastRun.startOffset, run.endOffset));
}
else {
runs.add(run);
}
}
@SuppressWarnings("AssignmentToForLoopParameter")
private static void addFragments(BidiRun run, Chunk chunk, char[] text, int start, int end, int fontStyle,
FontPreferences fontPreferences, FontRenderContext fontRenderContext,
@Nullable TabFragment tabFragment) {
assert start < end;
FontInfo currentFontInfo = 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(chunk, text, currentIndex, i, currentFontInfo, false);
chunk.fragments.add(tabFragment);
currentFontInfo = null;
currentIndex = i + 1;
}
else {
boolean surrogatePair = false;
int codePoint = c;
if (Character.isHighSurrogate(c) && (i + 1 < end)) {
char nextChar = text[i + 1];
if (Character.isLowSurrogate(nextChar)) {
codePoint = Character.toCodePoint(c, nextChar);
surrogatePair = true;
}
}
FontInfo fontInfo = ComplementaryFontsRegistry.getFontAbleToDisplay(codePoint, fontStyle, fontPreferences, fontRenderContext);
if (!fontInfo.equals(currentFontInfo)) {
addTextFragmentIfNeeded(chunk, text, currentIndex, i, currentFontInfo, run.isRtl());
currentFontInfo = fontInfo;
currentIndex = i;
}
if (surrogatePair) i++;
}
}
addTextFragmentIfNeeded(chunk, text, currentIndex, end, currentFontInfo, run.isRtl());
assert !chunk.fragments.isEmpty();
}
private static void addTextFragmentIfNeeded(Chunk chunk, char[] chars, int from, int to, FontInfo fontInfo, boolean isRtl) {
if (to > from) {
assert fontInfo != null;
chunk.fragments.add(TextFragmentFactory.createTextFragment(chars, from, to, isRtl, fontInfo));
}
}
Iterable<VisualFragment> getFragmentsInVisualOrder(final float startX) {
return new Iterable<VisualFragment>() {
@Override
public Iterator<VisualFragment> iterator() {
return new VisualOrderIterator(null, 0, startX, 0, 0, getRunsInVisualOrder());
}
};
}
/**
* If <code>quickEvaluationListener</code> is provided, quick approximate iteration becomes enabled, listener will be invoked
* if approximation will in fact be used during width calculation.
*/
Iterator<VisualFragment> getFragmentsInVisualOrder(@NotNull final EditorView view,
final int line,
final float startX,
final int startVisualColumn,
final int startOffset,
int endOffset,
@Nullable Runnable quickEvaluationListener) {
assert startOffset <= endOffset;
Document document = view.getEditor().getDocument();
int lineStartOffset = document.getLineStartOffset(line);
assert !DocumentUtil.isInsideSurrogatePair(document, lineStartOffset + startOffset);
assert !DocumentUtil.isInsideSurrogatePair(document, lineStartOffset + endOffset);
final BidiRun[] runs;
if (startOffset == endOffset) {
runs = BidiRun.EMPTY_ARRAY;
}
else {
List<BidiRun> runList = new ArrayList<>();
for (BidiRun run : getRunsInLogicalOrder()) {
if (run.endOffset <= startOffset) continue;
if (run.startOffset >= endOffset) break;
runList.add(run.subRun(view, line, startOffset, endOffset, quickEvaluationListener));
}
runs = runList.toArray(new BidiRun[runList.size()]);
if (runs.length > 1) {
reorderRunsVisually(runs);
}
}
return new VisualOrderIterator(view, line, startX, startVisualColumn, startOffset, runs);
}
abstract Stream<Chunk> getChunksInLogicalOrder();
float getWidth() {
throw new RuntimeException("This LineLayout instance doesn't have precalculated width");
}
abstract boolean isLtr();
abstract boolean isRtlLocation(int offset, boolean leanForward);
abstract int findNearestDirectionBoundary(int offset, boolean lookForward);
abstract BidiRun[] getRunsInLogicalOrder();
abstract BidiRun[] getRunsInVisualOrder();
private static class SingleChunk extends LineLayout {
private final Chunk myChunk;
private SingleChunk(Chunk chunk) {
myChunk = chunk;
}
@Override
Stream<Chunk> getChunksInLogicalOrder() {
return myChunk == null ? Stream.empty() : Stream.of(myChunk);
}
@Override
boolean isLtr() {
return true;
}
@Override
boolean isRtlLocation(int offset, boolean leanForward) {
return false;
}
@Override
int findNearestDirectionBoundary(int offset, boolean lookForward) {
return -1;
}
@Override
BidiRun[] getRunsInLogicalOrder() {
return createRuns();
}
@Override
BidiRun[] getRunsInVisualOrder() {
return createRuns();
}
private BidiRun[] createRuns() {
if (myChunk == null) return BidiRun.EMPTY_ARRAY;
BidiRun run = new BidiRun(myChunk.endOffset);
run.chunks = new Chunk[] {myChunk};
return new BidiRun[] {run};
}
}
private static class MultiChunk extends LineLayout {
private final BidiRun[] myBidiRunsInLogicalOrder;
private final BidiRun[] myBidiRunsInVisualOrder;
private MultiChunk(BidiRun[] bidiRunsInLogicalOrder) {
myBidiRunsInLogicalOrder = bidiRunsInLogicalOrder;
if (bidiRunsInLogicalOrder.length > 1) {
myBidiRunsInVisualOrder = myBidiRunsInLogicalOrder.clone();
reorderRunsVisually(myBidiRunsInVisualOrder);
}
else {
myBidiRunsInVisualOrder = bidiRunsInLogicalOrder;
}
}
@Override
Stream<Chunk> getChunksInLogicalOrder() {
return Stream.of(myBidiRunsInLogicalOrder).flatMap((BidiRun r) -> r.chunks == null ? Stream.empty() : Stream.of(r.chunks));
}
@Override
boolean isLtr() {
return myBidiRunsInLogicalOrder.length == 0 || myBidiRunsInLogicalOrder.length == 1 && !myBidiRunsInLogicalOrder[0].isRtl();
}
@Override
boolean isRtlLocation(int offset, boolean leanForward) {
if (offset == 0 && !leanForward) return false;
for (BidiRun run : myBidiRunsInLogicalOrder) {
if (offset < run.endOffset || offset == run.endOffset && !leanForward) return run.isRtl();
}
return false;
}
@Override
int findNearestDirectionBoundary(int offset, boolean lookForward) {
if (lookForward) {
byte originLevel = -1;
for (BidiRun run : myBidiRunsInLogicalOrder) {
if (originLevel >= 0) {
if (run.level != originLevel) return run.startOffset;
}
else if (run.endOffset > offset) {
originLevel = run.level;
}
}
return originLevel > 0 ? myBidiRunsInLogicalOrder[myBidiRunsInLogicalOrder.length - 1].endOffset : -1;
}
else {
byte originLevel = -1;
for (int i = myBidiRunsInLogicalOrder.length - 1; i >= 0; i--) {
BidiRun run = myBidiRunsInLogicalOrder[i];
if (originLevel >= 0) {
if (run.level != originLevel) return run.endOffset;
}
else if (run.startOffset < offset) {
originLevel = run.level;
}
}
return originLevel > 0 ? 0 : -1;
}
}
@Override
BidiRun[] getRunsInLogicalOrder() {
return myBidiRunsInLogicalOrder;
}
@Override
BidiRun[] getRunsInVisualOrder() {
return myBidiRunsInVisualOrder;
}
}
private static class WithSize extends LineLayout {
private final LineLayout myDelegate;
private final float myWidth;
private WithSize(@NotNull LineLayout delegate) {
myDelegate = delegate;
myWidth = calculateWidth();
}
private float calculateWidth() {
float x = 0;
for (VisualFragment fragment : getFragmentsInVisualOrder(x)) {
x = fragment.getEndX();
}
return x;
}
@Override
Stream<Chunk> getChunksInLogicalOrder() {
return myDelegate.getChunksInLogicalOrder();
}
@Override
float getWidth() {
return myWidth;
}
@Override
boolean isLtr() {
return myDelegate.isLtr();
}
@Override
boolean isRtlLocation(int offset, boolean leanForward) {
return myDelegate.isRtlLocation(offset, leanForward);
}
@Override
int findNearestDirectionBoundary(int offset, boolean lookForward) {
return myDelegate.findNearestDirectionBoundary(offset, lookForward);
}
@Override
BidiRun[] getRunsInLogicalOrder() {
return myDelegate.getRunsInLogicalOrder();
}
@Override
BidiRun[] getRunsInVisualOrder() {
return myDelegate.getRunsInVisualOrder();
}
}
private static class BidiRun {
public static final BidiRun[] EMPTY_ARRAY = new BidiRun[0];
private static final int CHUNK_CHARACTERS = 1024;
private final byte level;
private final int startOffset;
private final int endOffset;
private int visualStartLogicalColumn;
private Chunk[] chunks; // in logical order
private BidiRun(int length) {
this((byte)0, 0, length);
}
private BidiRun(byte level, int startOffset, int endOffset) {
this.level = level;
this.startOffset = startOffset;
this.endOffset = endOffset;
}
private boolean isRtl() {
return BitUtil.isSet(level, 1);
}
private Chunk[] getChunks(CharSequence text, int startOffsetInText) {
if (chunks == null) {
int chunkCount = getChunkCount();
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(alignToCodePointBoundary(text, from + startOffsetInText) - startOffsetInText,
alignToCodePointBoundary(text, to + startOffsetInText) - startOffsetInText);
chunks[i] = chunk;
}
}
return chunks;
}
private int getChunkCount() {
return (endOffset - startOffset + CHUNK_CHARACTERS - 1) / CHUNK_CHARACTERS;
}
private BidiRun subRun(@NotNull EditorView view, int line, int targetStartOffset, int targetEndOffset,
@Nullable Runnable quickEvaluationListener) {
assert targetStartOffset < endOffset;
assert targetEndOffset > startOffset;
int start = Math.max(startOffset, targetStartOffset);
int end = Math.min(endOffset, targetEndOffset);
BidiRun subRun = new BidiRun(level, start, end);
List<Chunk> subChunks = new ArrayList<>();
Document document = view.getEditor().getDocument();
for (Chunk chunk : getChunks(document.getImmutableCharSequence(), document.getLineStartOffset(line))) {
if (chunk.endOffset <= start) continue;
if (chunk.startOffset >= end) break;
subChunks.add(chunk.subChunk(view, this, line, start, end, quickEvaluationListener));
}
subRun.chunks = subChunks.toArray(new Chunk[subChunks.size()]);
subRun.visualStartLogicalColumn = (subRun.isRtl() ? end == endOffset : start == startOffset) ? visualStartLogicalColumn :
view.getLogicalPositionCache().offsetToLogicalColumn(line, subRun.isRtl() ? end : start);
return subRun;
}
}
static class Chunk {
List<LineFragment> fragments; // 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, BidiRun run, int line) {
if (isReal()) {
view.getTextLayoutCache().onChunkAccess(this);
}
if (fragments != null) return;
assert isReal();
fragments = new ArrayList<>();
int lineStartOffset = view.getEditor().getDocument().getLineStartOffset(line);
int start = lineStartOffset + startOffset;
int end = lineStartOffset + endOffset;
if (LOG.isDebugEnabled()) LOG.debug("Text layout for " + view.getEditor().getVirtualFile() + " (" + start + "-" + end + ")");
IterationState it = new IterationState(view.getEditor(), start, end, null, false, true, false, false);
FontPreferences fontPreferences = view.getEditor().getColorsScheme().getFontPreferences();
char[] chars = CharArrayUtil.fromSequence(view.getEditor().getDocument().getImmutableCharSequence(), start, end);
int currentFontType = 0;
Color currentColor = null;
int currentStart = start;
while (!it.atEnd()) {
int fontType = it.getMergedAttributes().getFontType();
Color color = it.getMergedAttributes().getForegroundColor();
if (fontType != currentFontType || !color.equals(currentColor)) {
int tokenStart = it.getStartOffset();
if (tokenStart > currentStart) {
addFragments(run, this, chars, currentStart - start, tokenStart - start,
currentFontType, fontPreferences, view.getFontRenderContext(), view.getTabFragment());
}
currentStart = tokenStart;
currentFontType = fontType;
currentColor = color;
}
it.advance();
}
if (end > currentStart) {
addFragments(run, this, chars, currentStart - start, end - start,
currentFontType, fontPreferences, view.getFontRenderContext(), view.getTabFragment());
}
view.getSizeManager().textLayoutPerformed(start, end);
assert !fragments.isEmpty();
}
private Chunk subChunk(EditorView view, BidiRun run, int line, int targetStartOffset, int targetEndOffset,
@Nullable Runnable quickEvaluationListener) {
assert isReal();
assert targetStartOffset < endOffset;
assert targetEndOffset > startOffset;
int start = Math.max(startOffset, targetStartOffset);
int end = Math.min(endOffset, targetEndOffset);
if (quickEvaluationListener != null && fragments == null) {
quickEvaluationListener.run();
Chunk chunk = new SyntheticChunk(start, end);
int startColumn = view.getLogicalPositionCache().offsetToLogicalColumn(line, start);
int endColumn = view.getLogicalPositionCache().offsetToLogicalColumn(line, end);
chunk.fragments = Collections.singletonList(new ApproximationFragment(end - start, endColumn - startColumn,
view.getMaxCharWidth()));
return chunk;
}
if (start == startOffset && end == this.endOffset) {
return this;
}
ensureLayout(view, run, line);
Chunk chunk = new SyntheticChunk(start, end);
chunk.fragments = new ArrayList<>();
int offset = startOffset;
for (LineFragment fragment : fragments) {
if (end <= offset) break;
int endOffset = offset + fragment.getLength();
if (start < endOffset) {
chunk.fragments.add(fragment.subFragment(Math.max(start, offset) - offset, Math.min(end, endOffset) - offset));
}
offset = endOffset;
}
return chunk;
}
boolean isReal() {
return true;
}
void clearCache() {
fragments = null;
}
}
private static class SyntheticChunk extends Chunk {
private SyntheticChunk(int startOffset, int endOffset) {
super(startOffset, endOffset);
}
@Override
boolean isReal() {
return false;
}
}
private static class VisualOrderIterator implements Iterator<VisualFragment> {
private final EditorView myView;
private final CharSequence myText;
private final int myLine;
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(EditorView view, int line,
float startX, int startVisualColumn, int startOffset, BidiRun[] runsInVisualOrder) {
myView = view;
myText = view == null ? null : view.getEditor().getDocument().getImmutableCharSequence();
myLine = line;
myLineStartOffset = view == null ? 0 : view.getEditor().getDocument().getLineStartOffset(line);
myRuns = runsInVisualOrder;
myFragment.startX = startX;
myFragment.startVisualColumn = startVisualColumn;
myFragment.startOffset = startOffset;
}
@Override
public boolean hasNext() {
if (myRunIndex >= myRuns.length) return false;
BidiRun run = myRuns[myRunIndex];
Chunk[] chunks = run.getChunks(myText, myLineStartOffset);
if (myChunkIndex >= chunks.length) return false;
Chunk chunk = chunks[run.isRtl() ? chunks.length - 1 - myChunkIndex : myChunkIndex];
if (myView != null) {
chunk.ensureLayout(myView, run, myLine);
}
return myFragmentIndex < chunk.fragments.size();
}
@Override
public VisualFragment next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
BidiRun run = myRuns[myRunIndex];
if (myRunIndex == 0 && myChunkIndex == 0 && myFragmentIndex == 0) {
myFragment.startLogicalColumn = run.visualStartLogicalColumn;
}
else {
if (myChunkIndex == 0 && myFragmentIndex == 0) {
myFragment.startLogicalColumn = run.visualStartLogicalColumn;
}
else {
myFragment.startLogicalColumn = myFragment.getEndLogicalColumn();
}
myFragment.startVisualColumn = myFragment.getEndVisualColumn();
myFragment.startX = myFragment.getEndX();
}
myFragment.isRtl = run.isRtl();
Chunk[] chunks = run.getChunks(myText, myLineStartOffset);
Chunk chunk = chunks[run.isRtl() ? chunks.length - 1 - myChunkIndex : myChunkIndex];
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 >= chunk.fragments.size()) {
myFragmentIndex = 0;
myChunkIndex++;
if (myChunkIndex >= chunks.length) {
myChunkIndex = 0;
myOffsetInsideRun = 0;
myRunIndex++;
}
}
return myFragment;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
static class VisualFragment {
private LineFragment delegate;
private int startOffset;
private int startLogicalColumn;
private int startVisualColumn;
private float startX;
private boolean isRtl;
boolean isRtl() {
return isRtl;
}
int getMinOffset() {
return isRtl ? startOffset - getLength() : startOffset;
}
int getMaxOffset() {
return isRtl ? startOffset : startOffset + getLength();
}
int getStartOffset() {
return startOffset;
}
int getEndOffset() {
return isRtl ? startOffset - getLength() : startOffset + getLength();
}
int getLength() {
return delegate.getLength();
}
int getStartLogicalColumn() {
return startLogicalColumn;
}
int getEndLogicalColumn() {
return isRtl ? startLogicalColumn - getLogicalColumnCount() : startLogicalColumn + getLogicalColumnCount();
}
int getMinLogicalColumn() {
return isRtl ? startLogicalColumn - getLogicalColumnCount() : startLogicalColumn;
}
int getMaxLogicalColumn() {
return isRtl ? startLogicalColumn : startLogicalColumn + getLogicalColumnCount();
}
int getStartVisualColumn() {
return startVisualColumn;
}
int getEndVisualColumn() {
return startVisualColumn + getVisualColumnCount();
}
int getLogicalColumnCount() {
// there's no need to calculate start column for RTL case - it makes sense only for TabFragment, which cannot be part of RTL run
return delegate.getLogicalColumnCount(isRtl ? 0 : getMinLogicalColumn());
}
int getVisualColumnCount() {
return delegate.getVisualColumnCount(startX);
}
float getStartX() {
return startX;
}
float getEndX() {
return delegate.offsetToX(startX, 0, getLength());
}
float getWidth() {
return getEndX() - getStartX();
}
// column is expected to be between minLogicalColumn and maxLogicalColumn for this fragment
int logicalToVisualColumn(int column) {
return startVisualColumn + delegate.logicalToVisualColumn(startX, getMinLogicalColumn(),
isRtl ? startLogicalColumn - column : column - startLogicalColumn);
}
// column is expected to be between startVisualColumn and endVisualColumn for this fragment
int visualToLogicalColumn(int column) {
int relativeLogicalColumn = delegate.visualToLogicalColumn(startX, getMinLogicalColumn(), column - startVisualColumn);
return isRtl ? startLogicalColumn - relativeLogicalColumn : startLogicalColumn + relativeLogicalColumn;
}
// offset is expected to be between minOffset and maxOffset for this fragment
float offsetToX(int offset) {
return delegate.offsetToX(startX, 0, getRelativeOffset(offset));
}
// both startOffset and offset are expected to be between minOffset and maxOffset for this fragment
float offsetToX(float startX, int startOffset, int offset) {
return delegate.offsetToX(startX, getRelativeOffset(startOffset), getRelativeOffset(offset));
}
// x is expected to be between startX and endX for this fragment
// returns array of two elements
// - first one is visual column,
// - second one is 1 if target location is closer to larger columns and 0 otherwise
int[] xToVisualColumn(float x) {
int[] column = delegate.xToVisualColumn(startX, x);
column[0] += startVisualColumn;
return column;
}
// column is expected to be between startVisualColumn and endVisualColumn for this fragment
float visualColumnToX(int column) {
return delegate.visualColumnToX(startX, column - startVisualColumn);
}
void draw(Graphics2D g, float x, float y) {
delegate.draw(g, x, y, 0, getLength());
}
// columns are visual (relative to fragment's start)
void draw(Graphics2D g, float x, float y, int startRelativeColumn, int endRelativeColumn) {
delegate.draw(g, x, y, startRelativeColumn, endRelativeColumn);
}
private int getRelativeOffset(int offset) {
return isRtl ? startOffset - offset : offset - startOffset;
}
}
}