blob: 7e855923811cb6d0b93691980ed9053381fb77fb [file] [log] [blame]
/*
* Copyright 2000-2014 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;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Caret;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.FoldRegion;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.ex.*;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.markup.EffectType;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.util.Comparing;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.CommonProcessors;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public final class IterationState {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.IterationState");
private static final Comparator<RangeHighlighterEx> BY_LAYER_THEN_ATTRIBUTES = new Comparator<RangeHighlighterEx>() {
@Override
public int compare(RangeHighlighterEx o1, RangeHighlighterEx o2) {
final int result = LayerComparator.INSTANCE.compare(o1, o2);
if (result != 0) {
return result;
}
// There is a possible case when more than one highlighter target the same region (e.g. 'identifier under caret' and 'identifier').
// We want to prefer the one that defines foreground color to the one that doesn't define (has either fore- or background colors
// while the other one has only foreground color). See IDEA-85697 for concrete example.
final TextAttributes a1 = o1.getTextAttributes();
final TextAttributes a2 = o2.getTextAttributes();
if (a1 == null ^ a2 == null) {
return a1 == null ? 1 : -1;
}
if (a1 == null) {
return result;
}
final Color fore1 = a1.getForegroundColor();
final Color fore2 = a2.getForegroundColor();
if (fore1 == null ^ fore2 == null) {
return fore1 == null ? 1 : -1;
}
final Color back1 = a1.getBackgroundColor();
final Color back2 = a2.getBackgroundColor();
if (back1 == null ^ back2 == null) {
return back1 == null ? 1 : -1;
}
return result;
}
};
private final TextAttributes myMergedAttributes = new TextAttributes();
private final HighlighterIterator myHighlighterIterator;
private final HighlighterSweep myView;
private final HighlighterSweep myDoc;
private int myStartOffset;
private int myEndOffset;
private final int myEnd;
private final int[] mySelectionStarts;
private final int[] mySelectionEnds;
private final int[] myVirtualSelectionStarts;
private final int[] myVirtualSelectionEnds;
private int myCurrentSelectionIndex = 0;
private int myCurrentVirtualSelectionIndex = 0;
private boolean myCurrentLineHasVirtualSelection;
private int myCurrentPastLineEndBackgroundSegment; // 0 - before selection, 1 - in selection, 2 - after selection
private Color myCurrentBackgroundColor;
private final List<RangeHighlighterEx> myCurrentHighlighters = new ArrayList<RangeHighlighterEx>();
private final FoldingModelEx myFoldingModel;
private FoldRegion myCurrentFold = null;
private final TextAttributes myFoldTextAttributes;
private final TextAttributes mySelectionAttributes;
private final TextAttributes myCaretRowAttributes;
private final Color myDefaultBackground;
private final Color myDefaultForeground;
private final int myCaretRowStart;
private final int myCaretRowEnd;
private final List<TextAttributes> myCachedAttributesList = new ArrayList<TextAttributes>(5);
private final DocumentEx myDocument;
private final EditorEx myEditor;
private final Color myReadOnlyColor;
public IterationState(@NotNull EditorEx editor, int start, int end, boolean useCaretAndSelection) {
ApplicationManager.getApplication().assertReadAccessAllowed();
myDocument = editor.getDocument();
myStartOffset = start;
myEnd = end;
myEditor = editor;
LOG.assertTrue(myStartOffset <= myEnd);
myHighlighterIterator = editor.getHighlighter().createIterator(start);
boolean hasSelection = useCaretAndSelection && (editor.getCaretModel().supportsMultipleCarets() || editor.getSelectionModel().hasSelection() || editor.getSelectionModel().hasBlockSelection());
if (!hasSelection) {
mySelectionStarts = ArrayUtilRt.EMPTY_INT_ARRAY;
mySelectionEnds = ArrayUtilRt.EMPTY_INT_ARRAY;
myVirtualSelectionStarts = ArrayUtilRt.EMPTY_INT_ARRAY;
myVirtualSelectionEnds = ArrayUtilRt.EMPTY_INT_ARRAY;
}
else if (editor.getCaretModel().supportsMultipleCarets()) {
List<Caret> carets = editor.getCaretModel().getAllCarets();
mySelectionStarts = new int[carets.size()];
mySelectionEnds = new int[carets.size()];
myVirtualSelectionStarts = new int[carets.size()];
myVirtualSelectionEnds = new int[carets.size()];
for (int i = 0; i < carets.size(); i++) {
Caret caret = carets.get(i);
mySelectionStarts[i] = caret.getSelectionStart();
mySelectionEnds[i] = caret.getSelectionEnd();
myVirtualSelectionStarts[i] = caret.getSelectionStartPosition().column - editor.offsetToVisualPosition(mySelectionStarts[i]).column;
myVirtualSelectionEnds[i] = caret.getSelectionEndPosition().column - editor.offsetToVisualPosition(mySelectionEnds[i]).column;
}
}
else {
mySelectionStarts = editor.getSelectionModel().getBlockSelectionStarts();
mySelectionEnds = editor.getSelectionModel().getBlockSelectionEnds();
myVirtualSelectionStarts = new int[mySelectionStarts.length];
myVirtualSelectionEnds = new int[mySelectionEnds.length];
}
myFoldingModel = editor.getFoldingModel();
myFoldTextAttributes = myFoldingModel.getPlaceholderAttributes();
mySelectionAttributes = editor.getSelectionModel().getTextAttributes();
myReadOnlyColor = myEditor.getColorsScheme().getColor(EditorColors.READONLY_FRAGMENT_BACKGROUND_COLOR);
CaretModel caretModel = editor.getCaretModel();
myCaretRowAttributes = editor.isRendererMode() ? null : caretModel.getTextAttributes();
myDefaultBackground = editor.getColorsScheme().getDefaultBackground();
myDefaultForeground = editor.getColorsScheme().getDefaultForeground();
myCaretRowStart = caretModel.getVisualLineStart();
myCaretRowEnd = caretModel.getVisualLineEnd();
MarkupModelEx editorMarkup = editor.getMarkupModel();
myView = new HighlighterSweep(editorMarkup, start, myEnd);
final MarkupModelEx docMarkup = (MarkupModelEx)DocumentMarkupModel.forDocument(editor.getDocument(), editor.getProject(), true);
myDoc = new HighlighterSweep(docMarkup, start, myEnd);
myEndOffset = myStartOffset;
advance();
}
private class HighlighterSweep {
private RangeHighlighterEx myNextHighlighter;
int i;
private final RangeHighlighterEx[] highlighters;
private HighlighterSweep(@NotNull MarkupModelEx markupModel, int start, int end) {
// we have to get all highlighters in advance and sort them by affected offsets
// since these can be different from the real offsets the highlighters are sorted by in the tree. (See LINES_IN_RANGE perverts)
final List<RangeHighlighterEx> list = new ArrayList<RangeHighlighterEx>();
markupModel.processRangeHighlightersOverlappingWith(start, end, new CommonProcessors.CollectProcessor<RangeHighlighterEx>(list));
highlighters = list.isEmpty() ? RangeHighlighterEx.EMPTY_ARRAY : list.toArray(new RangeHighlighterEx[list.size()]);
Arrays.sort(highlighters, RangeHighlighterEx.BY_AFFECTED_START_OFFSET);
int skipped = 0;
while (i < highlighters.length) {
RangeHighlighterEx highlighter = highlighters[i++];
if (!skipHighlighter(highlighter)) {
myNextHighlighter = highlighter;
break;
}
skipped++;
}
if (skipped > Math.min(1000, markupModel.getDocument().getTextLength())) {
int i = 0;
//LOG.error("Inefficient iteration, limit the number of highlighters to iterate");
}
}
private void advance() {
if (myNextHighlighter != null) {
if (myNextHighlighter.getAffectedAreaStartOffset() > myStartOffset) {
return;
}
myCurrentHighlighters.add(myNextHighlighter);
myNextHighlighter = null;
}
while (i < highlighters.length) {
RangeHighlighterEx highlighter = highlighters[i++];
if (!skipHighlighter(highlighter)) {
if (highlighter.getAffectedAreaStartOffset() > myStartOffset) {
myNextHighlighter = highlighter;
break;
}
else {
myCurrentHighlighters.add(highlighter);
}
}
}
}
private int getMinSegmentHighlighterEnd() {
if (myNextHighlighter != null) {
return myNextHighlighter.getAffectedAreaStartOffset();
}
return Integer.MAX_VALUE;
}
}
private boolean skipHighlighter(@NotNull RangeHighlighterEx highlighter) {
if (!highlighter.isValid() || highlighter.isAfterEndOfLine() || highlighter.getTextAttributes() == null) return true;
final FoldRegion region = myFoldingModel.getCollapsedRegionAtOffset(highlighter.getAffectedAreaStartOffset());
if (region != null && region == myFoldingModel.getCollapsedRegionAtOffset(highlighter.getAffectedAreaEndOffset())) return true;
return !highlighter.getEditorFilter().avaliableIn(myEditor);
}
public void advance() {
myStartOffset = myEndOffset;
advanceSegmentHighlighters();
advanceCurrentSelectionIndex();
advanceCurrentVirtualSelectionIndex();
myCurrentFold = myFoldingModel.fetchOutermost(myStartOffset);
if (myCurrentFold != null) {
myEndOffset = myCurrentFold.getEndOffset();
}
else {
myEndOffset = Math.min(getHighlighterEnd(myStartOffset), getSelectionEnd());
myEndOffset = Math.min(myEndOffset, getMinSegmentHighlightersEnd());
myEndOffset = Math.min(myEndOffset, getFoldRangesEnd(myStartOffset));
myEndOffset = Math.min(myEndOffset, getCaretEnd(myStartOffset));
myEndOffset = Math.min(myEndOffset, getGuardedBlockEnd(myStartOffset));
}
reinit();
}
private int getHighlighterEnd(int start) {
while (!myHighlighterIterator.atEnd()) {
int end = myHighlighterIterator.getEnd();
if (end > start) {
return end;
}
myHighlighterIterator.advance();
}
return myEnd;
}
private int getCaretEnd(int start) {
if (myCaretRowStart > start) {
return myCaretRowStart;
}
if (myCaretRowEnd > start) {
return myCaretRowEnd;
}
return myEnd;
}
private int getGuardedBlockEnd(int start) {
List<RangeMarker> blocks = myDocument.getGuardedBlocks();
int min = myEnd;
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < blocks.size(); i++) {
RangeMarker block = blocks.get(i);
if (block.getStartOffset() > start) {
min = Math.min(min, block.getStartOffset());
}
else if (block.getEndOffset() > start) {
min = Math.min(min, block.getEndOffset());
}
}
return min;
}
private void advanceCurrentSelectionIndex() {
while (myCurrentSelectionIndex < mySelectionEnds.length && myStartOffset >= mySelectionEnds[myCurrentSelectionIndex]) {
myCurrentSelectionIndex++;
}
}
private void advanceCurrentVirtualSelectionIndex() {
while (myCurrentVirtualSelectionIndex < mySelectionEnds.length
&& (myStartOffset > mySelectionEnds[myCurrentVirtualSelectionIndex] || myVirtualSelectionEnds[myCurrentVirtualSelectionIndex] <= 0)) {
myCurrentVirtualSelectionIndex++;
}
}
private int getSelectionEnd() {
if (myCurrentSelectionIndex >= mySelectionStarts.length) {
return myEnd;
}
if (myStartOffset < mySelectionStarts[myCurrentSelectionIndex]) {
return mySelectionStarts[myCurrentSelectionIndex];
}
return mySelectionEnds[myCurrentSelectionIndex];
}
private boolean isInSelection() {
return myCurrentSelectionIndex < mySelectionStarts.length && myStartOffset >= mySelectionStarts[myCurrentSelectionIndex];
}
private void advanceSegmentHighlighters() {
myDoc.advance();
myView.advance();
for (int i = myCurrentHighlighters.size() - 1; i >= 0; i--) {
RangeHighlighterEx highlighter = myCurrentHighlighters.get(i);
if (highlighter.getAffectedAreaEndOffset() <= myStartOffset) {
myCurrentHighlighters.remove(i);
}
}
}
private int getFoldRangesEnd(int startOffset) {
int end = myEnd;
FoldRegion[] topLevelCollapsed = myFoldingModel.fetchTopLevel();
if (topLevelCollapsed != null) {
for (int i = myFoldingModel.getLastCollapsedRegionBefore(startOffset) + 1;
i >= 0 && i < topLevelCollapsed.length;
i++) {
FoldRegion range = topLevelCollapsed[i];
if (!range.isValid()) continue;
int rangeEnd = range.getStartOffset();
if (rangeEnd > startOffset) {
if (rangeEnd < end) {
end = rangeEnd;
}
else {
break;
}
}
}
}
return end;
}
private int getMinSegmentHighlightersEnd() {
int end = myEnd;
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < myCurrentHighlighters.size(); i++) {
RangeHighlighterEx highlighter = myCurrentHighlighters.get(i);
if (highlighter.getAffectedAreaEndOffset() < end) {
end = highlighter.getAffectedAreaEndOffset();
}
}
end = Math.min(end, myDoc.getMinSegmentHighlighterEnd());
end = Math.min(end, myView.getMinSegmentHighlighterEnd());
return end;
}
private void reinit() {
if (myHighlighterIterator.atEnd()) {
return;
}
boolean isInSelection = isInSelection();
boolean isInCaretRow = myStartOffset >= myCaretRowStart && myStartOffset < myCaretRowEnd;
boolean isInGuardedBlock = myDocument.getOffsetGuard(myStartOffset) != null;
TextAttributes syntax = myHighlighterIterator.getTextAttributes();
TextAttributes selection = isInSelection ? mySelectionAttributes : null;
TextAttributes caret = isInCaretRow ? myCaretRowAttributes : null;
TextAttributes fold = myCurrentFold != null ? myFoldTextAttributes : null;
TextAttributes guard = isInGuardedBlock
? new TextAttributes(null, myReadOnlyColor, null, EffectType.BOXED, Font.PLAIN)
: null;
final int size = myCurrentHighlighters.size();
if (size > 1) {
ContainerUtil.quickSort(myCurrentHighlighters, BY_LAYER_THEN_ATTRIBUTES);
}
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < size; i++) {
RangeHighlighterEx highlighter = myCurrentHighlighters.get(i);
if (highlighter.getTextAttributes() == TextAttributes.ERASE_MARKER) {
syntax = null;
}
}
List<TextAttributes> cachedAttributes = myCachedAttributesList;
cachedAttributes.clear();
int selectionAttributesIndex = -1; // a 'would-be' or real position of selection attributes in attributes list
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < size; i++) {
RangeHighlighterEx highlighter = myCurrentHighlighters.get(i);
if (highlighter.getLayer() < HighlighterLayer.SELECTION) {
if (selectionAttributesIndex < 0) {
selectionAttributesIndex = cachedAttributes.size();
}
if (selection != null) {
cachedAttributes.add(selection);
selection = null;
}
}
if (syntax != null && highlighter.getLayer() < HighlighterLayer.SYNTAX) {
if (fold != null) {
cachedAttributes.add(fold);
fold = null;
}
cachedAttributes.add(syntax);
syntax = null;
}
if (guard != null && highlighter.getLayer() < HighlighterLayer.GUARDED_BLOCKS) {
cachedAttributes.add(guard);
guard = null;
}
if (caret != null && highlighter.getLayer() < HighlighterLayer.CARET_ROW) {
cachedAttributes.add(caret);
caret = null;
}
TextAttributes textAttributes = highlighter.getTextAttributes();
if (textAttributes != null && textAttributes != TextAttributes.ERASE_MARKER) {
cachedAttributes.add(textAttributes);
}
}
if (selectionAttributesIndex < 0) {
selectionAttributesIndex = cachedAttributes.size();
}
if (selection != null) cachedAttributes.add(selection);
if (fold != null) cachedAttributes.add(fold);
if (guard != null) cachedAttributes.add(guard);
if (caret != null) cachedAttributes.add(caret);
if (syntax != null) cachedAttributes.add(syntax);
Color fore = null;
Color back = isInGuardedBlock ? myReadOnlyColor : null;
Color effect = null;
EffectType effectType = null;
int fontType = 0;
boolean selectionBackgroundIsPotentiallyVisible = cachedAttributes.isEmpty();
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < cachedAttributes.size(); i++) {
TextAttributes attrs = cachedAttributes.get(i);
if (fore == null) {
fore = ifDiffers(attrs.getForegroundColor(), myDefaultForeground);
}
if (back == null) {
if (isInSelection && i == selectionAttributesIndex || !isInSelection && i >= selectionAttributesIndex) {
selectionBackgroundIsPotentiallyVisible = true;
}
back = ifDiffers(attrs.getBackgroundColor(), myDefaultBackground);
}
if (fontType == Font.PLAIN) {
fontType = attrs.getFontType();
}
if (effect == null) {
effect = attrs.getEffectColor();
effectType = attrs.getEffectType();
}
}
if (fore == null) fore = myDefaultForeground;
if (back == null) back = myDefaultBackground;
if (effectType == null) effectType = EffectType.BOXED;
myMergedAttributes.setAttributes(fore, back, effect, null, effectType, fontType);
myCurrentBackgroundColor = back;
if (selectionBackgroundIsPotentiallyVisible && myCurrentVirtualSelectionIndex < mySelectionStarts.length && myStartOffset == mySelectionEnds[myCurrentVirtualSelectionIndex]) {
myCurrentLineHasVirtualSelection = true;
myCurrentPastLineEndBackgroundSegment = myVirtualSelectionStarts[myCurrentVirtualSelectionIndex] > 0 ? 0 : 1;
}
else {
myCurrentLineHasVirtualSelection = false;
myCurrentPastLineEndBackgroundSegment = 0;
}
}
@Nullable
private static Color ifDiffers(final Color c1, final Color c2) {
return Comparing.equal(c1, c2) ? null : c1;
}
public boolean atEnd() {
return myStartOffset >= myEnd;
}
public int getStartOffset() {
return myStartOffset;
}
public int getEndOffset() {
return myEndOffset;
}
@NotNull
public TextAttributes getMergedAttributes() {
return myMergedAttributes;
}
public FoldRegion getCurrentFold() {
return myCurrentFold;
}
public boolean hasPastLineEndBackgroundSegment() {
return myCurrentLineHasVirtualSelection && myCurrentPastLineEndBackgroundSegment < 2;
}
public int getPastLineEndBackgroundSegmentWidth() {
switch (myCurrentPastLineEndBackgroundSegment) {
case 0: return myVirtualSelectionStarts[myCurrentVirtualSelectionIndex];
case 1: return myVirtualSelectionEnds[myCurrentVirtualSelectionIndex] - myVirtualSelectionStarts[myCurrentVirtualSelectionIndex];
default: return 0;
}
}
@NotNull
public TextAttributes getPastLineEndBackgroundAttributes() {
myMergedAttributes.setBackgroundColor(myCurrentPastLineEndBackgroundSegment == 1 ? mySelectionAttributes.getBackgroundColor() : myCurrentBackgroundColor);
return myMergedAttributes;
}
public void advanceToNextPastLineEndBackgroundSegment() {
myCurrentPastLineEndBackgroundSegment++;
}
public boolean hasPastFileEndBackgroundSegments() {
myCurrentLineHasVirtualSelection = myVirtualSelectionEnds.length > 0
&& myVirtualSelectionEnds[myVirtualSelectionEnds.length - 1] > 0
&& myEndOffset == myEnd
&& mySelectionEnds[mySelectionStarts.length - 1] == myEndOffset;
if (myCurrentLineHasVirtualSelection) {
myCurrentVirtualSelectionIndex = myVirtualSelectionStarts.length - 1;
myCurrentPastLineEndBackgroundSegment = myVirtualSelectionStarts[myCurrentVirtualSelectionIndex] > 0 ? 0 : 1;
myCurrentBackgroundColor = myEndOffset >= myCaretRowStart ? myCaretRowAttributes.getBackgroundColor() : myDefaultBackground;
}
return myCurrentLineHasVirtualSelection;
}
@Nullable
public Color getPastFileEndBackground() {
boolean isInCaretRow = myEditor.getCaretModel().getLogicalPosition().line >= myDocument.getLineCount() - 1;
Color caret = isInCaretRow && myCaretRowAttributes != null ? myCaretRowAttributes.getBackgroundColor() : null;
ContainerUtil.quickSort(myCurrentHighlighters, LayerComparator.INSTANCE);
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < myCurrentHighlighters.size(); i++) {
RangeHighlighterEx highlighter = myCurrentHighlighters.get(i);
if (caret != null && highlighter.getLayer() < HighlighterLayer.CARET_ROW) {
return caret;
}
if (highlighter.getTargetArea() != HighlighterTargetArea.LINES_IN_RANGE
|| myDocument.getLineNumber(highlighter.getEndOffset()) < myDocument.getLineCount() - 1) {
continue;
}
TextAttributes textAttributes = highlighter.getTextAttributes();
if (textAttributes != null) {
Color backgroundColor = textAttributes.getBackgroundColor();
if (backgroundColor != null) return backgroundColor;
}
}
return caret;
}
private static class LayerComparator implements Comparator<RangeHighlighterEx> {
private static final LayerComparator INSTANCE = new LayerComparator();
@Override
public int compare(RangeHighlighterEx o1, RangeHighlighterEx o2) {
int layerDiff = o2.getLayer() - o1.getLayer();
if (layerDiff != 0) {
return layerDiff;
}
// prefer more specific region
int o1Length = o1.getAffectedAreaEndOffset() - o1.getAffectedAreaStartOffset();
int o2Length = o2.getAffectedAreaEndOffset() - o2.getAffectedAreaStartOffset();
return o1Length - o2Length;
}
}
}