blob: d19f5895930ff8dbf38d8221acd544f35dc626c4 [file] [log] [blame]
/*
* Copyright 2000-2012 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.
*/
/*
* @author max
*/
package com.intellij.codeInsight.daemon.impl;
import com.intellij.codeHighlighting.TextEditorHighlightingPass;
import com.intellij.codeInsight.highlighting.BraceMatchingUtil;
import com.intellij.lang.*;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.MarkupModel;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiFile;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.DocumentUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import com.intellij.util.containers.IntStack;
import com.intellij.util.text.CharArrayUtil;
import gnu.trove.TIntIntHashMap;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
public class IndentsPass extends TextEditorHighlightingPass implements DumbAware {
private static final ConcurrentMap<IElementType, String> COMMENT_PREFIXES = ContainerUtil.newConcurrentMap();
private static final String NO_COMMENT_INFO_MARKER = "hopefully, noone uses this string as a comment prefix";
private static final Key<List<RangeHighlighter>> INDENT_HIGHLIGHTERS_IN_EDITOR_KEY = Key.create("INDENT_HIGHLIGHTERS_IN_EDITOR_KEY");
private static final Key<Long> LAST_TIME_INDENTS_BUILT = Key.create("LAST_TIME_INDENTS_BUILT");
private final EditorEx myEditor;
private final PsiFile myFile;
public static final Comparator<TextRange> RANGE_COMPARATOR = new Comparator<TextRange>() {
@Override
public int compare(TextRange o1, TextRange o2) {
if (o1.getStartOffset() == o2.getStartOffset()) {
return o1.getEndOffset() - o2.getEndOffset();
}
return o1.getStartOffset() - o2.getStartOffset();
}
};
private static final CustomHighlighterRenderer RENDERER = new CustomHighlighterRenderer() {
@Override
@SuppressWarnings({"AssignmentToForLoopParameter"})
public void paint(@NotNull Editor editor,
@NotNull RangeHighlighter highlighter,
@NotNull Graphics g)
{
int startOffset = highlighter.getStartOffset();
final Document doc = highlighter.getDocument();
if (startOffset >= doc.getTextLength()) return;
final int endOffset = highlighter.getEndOffset();
final int endLine = doc.getLineNumber(endOffset);
int off;
int startLine = doc.getLineNumber(startOffset);
IndentGuideDescriptor descriptor = editor.getIndentsModel().getDescriptor(startLine, endLine);
final CharSequence chars = doc.getCharsSequence();
do {
int start = doc.getLineStartOffset(startLine);
int end = doc.getLineEndOffset(startLine);
off = CharArrayUtil.shiftForward(chars, start, end, " \t");
startLine--;
}
while (startLine > 1 && off < doc.getTextLength() && chars.charAt(off) == '\n');
final VisualPosition startPosition = editor.offsetToVisualPosition(off);
int indentColumn = startPosition.column;
// It's considered that indent guide can cross not only white space but comments, javadocs etc. Hence, there is a possible
// case that the first indent guide line is, say, single-line comment where comment symbols ('//') are located at the first
// visual column. We need to calculate correct indent guide column then.
int lineShift = 1;
if (indentColumn <= 0 && descriptor != null) {
indentColumn = descriptor.indentLevel;
lineShift = 0;
}
if (indentColumn <= 0) return;
final FoldingModel foldingModel = editor.getFoldingModel();
if (foldingModel.isOffsetCollapsed(off)) return;
final FoldRegion headerRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(doc.getLineNumber(off)));
final FoldRegion tailRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineStartOffset(doc.getLineNumber(endOffset)));
if (tailRegion != null && tailRegion == headerRegion) return;
final boolean selected;
final IndentGuideDescriptor guide = editor.getIndentsModel().getCaretIndentGuide();
if (guide != null) {
final CaretModel caretModel = editor.getCaretModel();
final int caretOffset = caretModel.getOffset();
selected =
caretOffset >= off && caretOffset < endOffset && caretModel.getLogicalPosition().column == indentColumn;
}
else {
selected = false;
}
Point start = editor.visualPositionToXY(new VisualPosition(startPosition.line + lineShift, indentColumn));
final VisualPosition endPosition = editor.offsetToVisualPosition(endOffset);
Point end = editor.visualPositionToXY(new VisualPosition(endPosition.line, endPosition.column));
int maxY = end.y;
if (endPosition.line == editor.offsetToVisualPosition(doc.getTextLength()).line) {
maxY += editor.getLineHeight();
}
Rectangle clip = g.getClipBounds();
if (clip != null) {
if (clip.y >= maxY || clip.y + clip.height <= start.y) {
return;
}
maxY = Math.min(maxY, clip.y + clip.height);
}
final EditorColorsScheme scheme = editor.getColorsScheme();
g.setColor(selected ? scheme.getColor(EditorColors.SELECTED_INDENT_GUIDE_COLOR) : scheme.getColor(EditorColors.INDENT_GUIDE_COLOR));
// There is a possible case that indent line intersects soft wrap-introduced text. Example:
// this is a long line <soft-wrap>
// that| is soft-wrapped
// |
// | <- vertical indent
//
// Also it's possible that no additional intersections are added because of soft wrap:
// this is a long line <soft-wrap>
// | that is soft-wrapped
// |
// | <- vertical indent
// We want to use the following approach then:
// 1. Show only active indent if it crosses soft wrap-introduced text;
// 2. Show indent as is if it doesn't intersect with soft wrap-introduced text;
if (selected) {
g.drawLine(start.x + 2, start.y, start.x + 2, maxY);
}
else {
int y = start.y;
int newY = start.y;
SoftWrapModel softWrapModel = editor.getSoftWrapModel();
int lineHeight = editor.getLineHeight();
for (int i = Math.max(0, startLine + lineShift); i < endLine && newY < maxY; i++) {
List<? extends SoftWrap> softWraps = softWrapModel.getSoftWrapsForLine(i);
int logicalLineHeight = softWraps.size() * lineHeight;
if (i > startLine + lineShift) {
logicalLineHeight += lineHeight; // We assume that initial 'y' value points just below the target line.
}
if (!softWraps.isEmpty() && softWraps.get(0).getIndentInColumns() < indentColumn) {
if (y < newY || i > startLine + lineShift) { // There is a possible case that soft wrap is located on indent start line.
g.drawLine(start.x + 2, y, start.x + 2, newY + lineHeight);
}
newY += logicalLineHeight;
y = newY;
}
else {
newY += logicalLineHeight;
}
FoldRegion foldRegion = foldingModel.getCollapsedRegionAtOffset(doc.getLineEndOffset(i));
if (foldRegion != null && foldRegion.getEndOffset() < doc.getTextLength()) {
i = doc.getLineNumber(foldRegion.getEndOffset());
}
}
if (y < maxY) {
g.drawLine(start.x + 2, y, start.x + 2, maxY);
}
}
}
};
private volatile List<TextRange> myRanges;
private volatile List<IndentGuideDescriptor> myDescriptors;
public IndentsPass(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
super(project, editor.getDocument(), false);
myEditor = (EditorEx)editor;
myFile = file;
}
@Override
public void doCollectInformation(@NotNull ProgressIndicator progress) {
assert myDocument != null;
final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT);
if (stamp != null && stamp.longValue() == nowStamp()) return;
myDescriptors = buildDescriptors();
ArrayList<TextRange> ranges = new ArrayList<TextRange>();
for (IndentGuideDescriptor descriptor : myDescriptors) {
ProgressManager.checkCanceled();
int endOffset =
descriptor.endLine < myDocument.getLineCount() ? myDocument.getLineStartOffset(descriptor.endLine) : myDocument.getTextLength();
ranges.add(new TextRange(myDocument.getLineStartOffset(descriptor.startLine), endOffset));
}
Collections.sort(ranges, RANGE_COMPARATOR);
myRanges = ranges;
}
private long nowStamp() {
if (!myEditor.getSettings().isIndentGuidesShown()) return -1;
assert myDocument != null;
return myDocument.getModificationStamp();
}
@Override
public void doApplyInformationToEditor() {
final Long stamp = myEditor.getUserData(LAST_TIME_INDENTS_BUILT);
if (stamp != null && stamp.longValue() == nowStamp()) return;
List<RangeHighlighter> oldHighlighters = myEditor.getUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY);
final List<RangeHighlighter> newHighlighters = new ArrayList<RangeHighlighter>();
final MarkupModel mm = myEditor.getMarkupModel();
int curRange = 0;
if (oldHighlighters != null) {
int curHighlight = 0;
while (curRange < myRanges.size() && curHighlight < oldHighlighters.size()) {
TextRange range = myRanges.get(curRange);
RangeHighlighter highlighter = oldHighlighters.get(curHighlight);
int cmp = compare(range, highlighter);
if (cmp < 0) {
newHighlighters.add(createHighlighter(mm, range));
curRange++;
}
else if (cmp > 0) {
highlighter.dispose();
curHighlight++;
}
else {
newHighlighters.add(highlighter);
curHighlight++;
curRange++;
}
}
for (; curHighlight < oldHighlighters.size(); curHighlight++) {
RangeHighlighter highlighter = oldHighlighters.get(curHighlight);
highlighter.dispose();
}
}
final int startRangeIndex = curRange;
assert myDocument != null;
DocumentUtil.executeInBulk(myDocument, myRanges.size() > 10000, new Runnable() {
@Override
public void run() {
for (int i = startRangeIndex; i < myRanges.size(); i++) {
newHighlighters.add(createHighlighter(mm, myRanges.get(i)));
}
}
});
myEditor.putUserData(INDENT_HIGHLIGHTERS_IN_EDITOR_KEY, newHighlighters);
myEditor.putUserData(LAST_TIME_INDENTS_BUILT, nowStamp());
myEditor.getIndentsModel().assumeIndents(myDescriptors);
}
private List<IndentGuideDescriptor> buildDescriptors() {
if (!myEditor.getSettings().isIndentGuidesShown()) return Collections.emptyList();
IndentsCalculator calculator = new IndentsCalculator();
calculator.calculate();
int[] lineIndents = calculator.lineIndents;
TIntIntHashMap effectiveCommentColumns = calculator.indentAfterUncomment;
List<IndentGuideDescriptor> descriptors = new ArrayList<IndentGuideDescriptor>();
IntStack lines = new IntStack();
IntStack indents = new IntStack();
lines.push(0);
indents.push(0);
assert myDocument != null;
final CharSequence chars = myDocument.getCharsSequence();
for (int line = 1; line < lineIndents.length; line++) {
ProgressManager.checkCanceled();
int curIndent = lineIndents[line];
while (!indents.empty() && curIndent <= indents.peek()) {
ProgressManager.checkCanceled();
final int level = indents.pop();
int startLine = lines.pop();
if (level > 0) {
boolean addDescriptor = effectiveCommentColumns.contains(startLine); // Indent started at comment
if (!addDescriptor) {
for (int i = startLine; i < line; i++) {
if (level != lineIndents[i] && level != effectiveCommentColumns.get(i)) {
addDescriptor = true;
break;
}
}
}
if (addDescriptor) {
descriptors.add(createDescriptor(level, startLine, line, chars));
}
}
}
int prevLine = line - 1;
int prevIndent = lineIndents[prevLine];
if (curIndent - prevIndent > 1) {
lines.push(prevLine);
indents.push(prevIndent);
}
}
while (!indents.empty()) {
ProgressManager.checkCanceled();
final int level = indents.pop();
int startLine = lines.pop();
if (level > 0) {
descriptors.add(createDescriptor(level, startLine, myDocument.getLineCount(), chars));
}
}
return descriptors;
}
private IndentGuideDescriptor createDescriptor(int level, int startLine, int endLine, CharSequence chars) {
while (startLine > 0 && isBlankLine(startLine, chars)) startLine--;
return new IndentGuideDescriptor(level, startLine, endLine);
}
private boolean isBlankLine(int line, CharSequence chars) {
Document document = myDocument;
if (document == null) {
return true;
}
int startOffset = document.getLineStartOffset(line);
int endOffset = document.getLineEndOffset(line);
return CharArrayUtil.shiftForward(chars, startOffset, endOffset, " \t") >= myDocument.getLineEndOffset(line);
}
/**
* We want to treat comments specially in a way to skip comment prefix on line indent calculation.
* <p/>
* Example:
* <pre>
* if (true) {
* int i1;
* // int i2;
* int i3;
* }
* </pre>
* We want to use 'int i2;' start offset as the third line indent (though it has non-white space comment prefix (//)
* at the first column.
* <p/>
* This method tries to parse comment prefix for the language implied by the given comment type. It uses
* {@link #NO_COMMENT_INFO_MARKER} as an indicator that that information is unavailable
*
* @param commentType target comment type
* @return prefix of the comment denoted by the given type if any;
* {@link #NO_COMMENT_INFO_MARKER} otherwise
*/
@NotNull
private static String getCommentPrefix(@NotNull IElementType commentType) {
Commenter c = LanguageCommenters.INSTANCE.forLanguage(commentType.getLanguage());
if (!(c instanceof CodeDocumentationAwareCommenter)) {
COMMENT_PREFIXES.put(commentType, NO_COMMENT_INFO_MARKER);
return NO_COMMENT_INFO_MARKER;
}
CodeDocumentationAwareCommenter commenter = (CodeDocumentationAwareCommenter)c;
IElementType lineCommentType = commenter.getLineCommentTokenType();
String lineCommentPrefix = commenter.getLineCommentPrefix();
if (lineCommentType != null) {
COMMENT_PREFIXES.put(lineCommentType, lineCommentPrefix == null ? NO_COMMENT_INFO_MARKER : lineCommentPrefix);
}
IElementType blockCommentType = commenter.getBlockCommentTokenType();
String blockCommentPrefix = commenter.getBlockCommentPrefix();
if (blockCommentType != null) {
COMMENT_PREFIXES.put(blockCommentType, blockCommentPrefix == null ? NO_COMMENT_INFO_MARKER : blockCommentPrefix);
}
IElementType docCommentType = commenter.getDocumentationCommentTokenType();
String docCommentPrefix = commenter.getDocumentationCommentPrefix();
if (docCommentType != null) {
COMMENT_PREFIXES.put(docCommentType, docCommentPrefix == null ? NO_COMMENT_INFO_MARKER : docCommentPrefix);
}
COMMENT_PREFIXES.putIfAbsent(commentType, NO_COMMENT_INFO_MARKER);
return COMMENT_PREFIXES.get(commentType);
}
@NotNull
private static RangeHighlighter createHighlighter(MarkupModel mm, TextRange range) {
final RangeHighlighter highlighter =
mm.addRangeHighlighter(range.getStartOffset(), range.getEndOffset(), 0, null, HighlighterTargetArea.EXACT_RANGE);
highlighter.setCustomRenderer(RENDERER);
return highlighter;
}
private static int compare(@NotNull TextRange r, @NotNull RangeHighlighter h) {
int answer = r.getStartOffset() - h.getStartOffset();
return answer != 0 ? answer : r.getEndOffset() - h.getEndOffset();
}
private class IndentsCalculator {
@NotNull public final Map<Language, TokenSet> myComments = ContainerUtilRt.newHashMap();
/**
* We need to treat specially commented lines. Consider a situation like below:
* <pre>
* void test() {
* if (true) {
* int i;
* // int j;
* }
* }
* </pre>
* We don't want to show indent guide after 'int i;' line because un-commented line below ('int j;') would have the same indent
* level. That's why we remember 'indents after un-comment' at this collection.
*/
@NotNull public final TIntIntHashMap/* line -> indent column after un-comment */ indentAfterUncomment = new TIntIntHashMap();
@NotNull public final int[] lineIndents;
@NotNull public final CharSequence myChars;
IndentsCalculator() {
assert myDocument != null;
lineIndents = new int[myDocument.getLineCount()];
myChars = myDocument.getCharsSequence();
}
/**
* Calculates line indents for the {@link #myDocument target document}.
*/
void calculate() {
final FileType fileType = myFile.getFileType();
int prevLineIndent = -1;
for (int line = 0; line < lineIndents.length; line++) {
ProgressManager.checkCanceled();
int lineStart = myDocument.getLineStartOffset(line);
int lineEnd = myDocument.getLineEndOffset(line);
final int nonWhitespaceOffset = CharArrayUtil.shiftForward(myChars, lineStart, lineEnd, " \t");
if (nonWhitespaceOffset == lineEnd) {
lineIndents[line] = -1; // Blank line marker
}
else {
final int column = ((EditorImpl)myEditor).calcColumnNumber(nonWhitespaceOffset, line, true, myChars);
if (prevLineIndent > 0 && prevLineIndent > column) {
lineIndents[line] = calcIndent(line, nonWhitespaceOffset, lineEnd, column);
}
else {
lineIndents[line] = column;
}
prevLineIndent = lineIndents[line];
}
}
int topIndent = 0;
for (int line = 0; line < lineIndents.length; line++) {
ProgressManager.checkCanceled();
if (lineIndents[line] >= 0) {
topIndent = lineIndents[line];
}
else {
int startLine = line;
while (line < lineIndents.length && lineIndents[line] < 0) {
//noinspection AssignmentToForLoopParameter
line++;
}
int bottomIndent = line < lineIndents.length ? lineIndents[line] : topIndent;
int indent = Math.min(topIndent, bottomIndent);
if (bottomIndent < topIndent) {
int lineStart = myDocument.getLineStartOffset(line);
int lineEnd = myDocument.getLineEndOffset(line);
int nonWhitespaceOffset = CharArrayUtil.shiftForward(myChars, lineStart, lineEnd, " \t");
HighlighterIterator iterator = myEditor.getHighlighter().createIterator(nonWhitespaceOffset);
if (BraceMatchingUtil.isRBraceToken(iterator, myChars, fileType)) {
indent = topIndent;
}
}
for (int blankLine = startLine; blankLine < line; blankLine++) {
assert lineIndents[blankLine] == -1;
lineIndents[blankLine] = Math.min(topIndent, indent);
}
//noinspection AssignmentToForLoopParameter
line--; // will be incremented back at the end of the loop;
}
}
}
/**
* Tries to calculate given line's indent column assuming that there might be a comment at the given indent offset
* (see {@link #getCommentPrefix(IElementType)}).
*
* @param line target line
* @param indentOffset start indent offset to use for the given line
* @param lineEndOffset given line's end offset
* @param fallbackColumn column to return if it's not possible to apply comment-specific indent calculation rules
* @return given line's indent column to use
*/
private int calcIndent(int line, int indentOffset, int lineEndOffset, int fallbackColumn) {
final HighlighterIterator it = myEditor.getHighlighter().createIterator(indentOffset);
IElementType tokenType = it.getTokenType();
Language language = tokenType.getLanguage();
TokenSet comments = myComments.get(language);
if (comments == null) {
ParserDefinition definition = LanguageParserDefinitions.INSTANCE.forLanguage(language);
if (definition != null) {
comments = definition.getCommentTokens();
}
if (comments == null) {
return fallbackColumn;
}
else {
myComments.put(language, comments);
}
}
if (comments.contains(tokenType) && indentOffset == it.getStart()) {
String prefix = COMMENT_PREFIXES.get(tokenType);
if (prefix == null) {
prefix = getCommentPrefix(tokenType);
}
if (!NO_COMMENT_INFO_MARKER.equals(prefix)) {
final int indentInsideCommentOffset = CharArrayUtil.shiftForward(myChars, indentOffset + prefix.length(), lineEndOffset, " \t");
if (indentInsideCommentOffset < lineEndOffset) {
int indent = myEditor.calcColumnNumber(indentInsideCommentOffset, line);
indentAfterUncomment.put(line, indent - prefix.length());
return indent;
}
}
}
return fallbackColumn;
}
}
}