blob: 123263a566c867c5810ee2090dc0cf442d942a54 [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.codeInsight.daemon.impl;
import com.intellij.codeInsight.daemon.GutterMark;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.ex.SweepProcessor;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.impl.RangeMarkerTree;
import com.intellij.openapi.editor.impl.RedBlackTree;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.Consumer;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.*;
import java.util.List;
public class UpdateHighlightersUtil {
private static final Comparator<HighlightInfo> BY_START_OFFSET_NODUPS = new Comparator<HighlightInfo>() {
@Override
public int compare(@NotNull HighlightInfo o1, @NotNull HighlightInfo o2) {
int d = o1.getActualStartOffset() - o2.getActualStartOffset();
if (d != 0) return d;
d = o1.getActualEndOffset() - o2.getActualEndOffset();
if (d != 0) return d;
d = Comparing.compare(o1.getSeverity(), o2.getSeverity());
if (d != 0) return -d; // higher severity first, to prevent warnings overlap errors
if (!Comparing.equal(o1.type, o2.type)) {
return String.valueOf(o1.type).compareTo(String.valueOf(o2.type));
}
if (!Comparing.equal(o1.getGutterIconRenderer(), o2.getGutterIconRenderer())) {
return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
}
if (!Comparing.equal(o1.forcedTextAttributes, o2.forcedTextAttributes)) {
return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
}
if (!Comparing.equal(o1.forcedTextAttributesKey, o2.forcedTextAttributesKey)) {
return String.valueOf(o1.getGutterIconRenderer()).compareTo(String.valueOf(o2.getGutterIconRenderer()));
}
return Comparing.compare(o1.getDescription(), o2.getDescription());
}
};
private static boolean isCoveredByOffsets(HighlightInfo info, HighlightInfo coveredBy) {
return coveredBy.startOffset <= info.startOffset && info.endOffset <= coveredBy.endOffset && info.getGutterIconRenderer() == null;
}
static void addHighlighterToEditorIncrementally(@NotNull Project project,
@NotNull Document document,
@NotNull PsiFile file,
int startOffset,
int endOffset,
@NotNull final HighlightInfo info,
@Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
final int group,
@NotNull Map<TextRange, RangeMarker> ranges2markersCache) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (isFileLevelOrGutterAnnotation(info)) return;
if (info.getStartOffset() < startOffset || info.getEndOffset() > endOffset) return;
MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
final SeverityRegistrar severityRegistrar = SeverityRegistrar.getSeverityRegistrar(project);
final boolean myInfoIsError = isSevere(info, severityRegistrar);
Processor<HighlightInfo> otherHighlightInTheWayProcessor = new Processor<HighlightInfo>() {
@Override
public boolean process(HighlightInfo oldInfo) {
if (!myInfoIsError && isCovered(info, severityRegistrar, oldInfo)) {
return false;
}
return oldInfo.getGroup() != group || !oldInfo.equalsByActualOffset(info);
}
};
boolean allIsClear = DaemonCodeAnalyzerEx.processHighlights(document, project,
null, info.getActualStartOffset(), info.getActualEndOffset(),
otherHighlightInTheWayProcessor);
if (allIsClear) {
createOrReuseHighlighterFor(info, colorsScheme, document, group, file, (MarkupModelEx)markup, null, ranges2markersCache, severityRegistrar);
clearWhiteSpaceOptimizationFlag(document);
assertMarkupConsistent(markup, project);
}
}
public static boolean isFileLevelOrGutterAnnotation(HighlightInfo info) {
return info.isFileLevelAnnotation() || info.getGutterIconRenderer() != null;
}
public static void setHighlightersToEditor(@NotNull Project project,
@NotNull Document document,
int startOffset,
int endOffset,
@NotNull Collection<HighlightInfo> highlights,
@Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
int group) {
TextRange range = new TextRange(startOffset, endOffset);
ApplicationManager.getApplication().assertIsDispatchThread();
PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
final DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project);
codeAnalyzer.cleanFileLevelHighlights(project, group, psiFile);
MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
assertMarkupConsistent(markup, project);
setHighlightersInRange(project, document, range, colorsScheme, new ArrayList<HighlightInfo>(highlights), (MarkupModelEx)markup, group);
}
@Deprecated //for teamcity
public static void setHighlightersToEditor(@NotNull Project project,
@NotNull Document document,
int startOffset,
int endOffset,
@NotNull Collection<HighlightInfo> highlights,
int group) {
setHighlightersToEditor(project, document, startOffset, endOffset, highlights, null, group);
}
// set highlights inside startOffset,endOffset but outside priorityRange
static void setHighlightersOutsideRange(@NotNull final Project project,
@NotNull final Document document,
@NotNull final PsiFile psiFile,
@NotNull final List<HighlightInfo> infos,
@Nullable final EditorColorsScheme colorsScheme,
// if null global scheme will be used
final int startOffset,
final int endOffset,
@NotNull final ProperTextRange priorityRange,
final int group) {
ApplicationManager.getApplication().assertIsDispatchThread();
final DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project);
codeAnalyzer.cleanFileLevelHighlights(project, group, psiFile);
final MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
assertMarkupConsistent(markup, project);
final SeverityRegistrar severityRegistrar = SeverityRegistrar.getSeverityRegistrar(project);
final HighlightersRecycler infosToRemove = new HighlightersRecycler();
ContainerUtil.quickSort(infos, BY_START_OFFSET_NODUPS);
Processor<HighlightInfo> processor = new Processor<HighlightInfo>() {
@Override
public boolean process(HighlightInfo info) {
if (info.getGroup() == group) {
RangeHighlighter highlighter = info.highlighter;
int hiStart = highlighter.getStartOffset();
int hiEnd = highlighter.getEndOffset();
if (!info.isFromInjection() && hiEnd < document.getTextLength() && (hiEnd <= startOffset || hiStart >= endOffset)) {
return true; // injections are oblivious to restricting range
}
boolean toRemove = !(hiEnd == document.getTextLength() &&
priorityRange.getEndOffset() == document.getTextLength()) &&
!priorityRange.containsRange(hiStart, hiEnd);
if (toRemove) {
infosToRemove.recycleHighlighter(highlighter);
info.highlighter = null;
}
}
return true;
}
};
DaemonCodeAnalyzerEx.processHighlightsOverlappingOutside(document, project, null, priorityRange.getStartOffset(), priorityRange.getEndOffset(), processor);
final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<TextRange, RangeMarker>(10);
final boolean[] changed = {false};
RangeMarkerTree.sweep(new RangeMarkerTree.Generator<HighlightInfo>(){
@Override
public boolean generateInStartOffsetOrder(@NotNull Processor<HighlightInfo> processor) {
return ContainerUtil.process(infos, processor);
}
}, new SweepProcessor<HighlightInfo>() {
@Override
public boolean process(int offset, @NotNull HighlightInfo info, boolean atStart, @NotNull Collection<HighlightInfo> overlappingIntervals) {
if (!atStart) return true;
if (!info.isFromInjection() && info.getEndOffset() < document.getTextLength() && (info.getEndOffset() <= startOffset || info.getStartOffset()>=endOffset)) return true; // injections are oblivious to restricting range
if (info.isFileLevelAnnotation()) {
codeAnalyzer.addFileLevelHighlight(project, group, info, psiFile);
changed[0] = true;
return true;
}
if (isWarningCoveredByError(info, overlappingIntervals, severityRegistrar)) {
return true;
}
if (info.getStartOffset() < priorityRange.getStartOffset() || info.getEndOffset() > priorityRange.getEndOffset()) {
createOrReuseHighlighterFor(info, colorsScheme, document, group, psiFile, (MarkupModelEx)markup, infosToRemove,
ranges2markersCache, severityRegistrar);
changed[0] = true;
}
return true;
}
});
for (RangeHighlighter highlighter : infosToRemove.forAllInGarbageBin()) {
highlighter.dispose();
changed[0] = true;
}
if (changed[0]) {
clearWhiteSpaceOptimizationFlag(document);
}
assertMarkupConsistent(markup, project);
}
static void setHighlightersInRange(@NotNull final Project project,
@NotNull final Document document,
@NotNull final TextRange range,
@Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
@NotNull final List<HighlightInfo> infos,
@NotNull final MarkupModelEx markup,
final int group) {
ApplicationManager.getApplication().assertIsDispatchThread();
final SeverityRegistrar severityRegistrar = SeverityRegistrar.getSeverityRegistrar(project);
final HighlightersRecycler infosToRemove = new HighlightersRecycler();
DaemonCodeAnalyzerEx
.processHighlights(document, project, null, range.getStartOffset(), range.getEndOffset(), new Processor<HighlightInfo>() {
@Override
public boolean process(HighlightInfo info) {
if (info.getGroup() == group) {
RangeHighlighter highlighter = info.highlighter;
int hiStart = highlighter.getStartOffset();
int hiEnd = highlighter.getEndOffset();
boolean willBeRemoved = hiEnd == document.getTextLength() && range.getEndOffset() == document.getTextLength()
/*|| range.intersectsStrict(hiStart, hiEnd)*/ || range.containsRange(hiStart, hiEnd) /*|| hiStart <= range.getStartOffset() && hiEnd >= range.getEndOffset()*/;
if (willBeRemoved) {
infosToRemove.recycleHighlighter(highlighter);
info.highlighter = null;
}
}
return true;
}
});
ContainerUtil.quickSort(infos, BY_START_OFFSET_NODUPS);
final Map<TextRange, RangeMarker> ranges2markersCache = new THashMap<TextRange, RangeMarker>(10);
final PsiFile psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document);
final DaemonCodeAnalyzerEx codeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(project);
final boolean[] changed = {false};
RangeMarkerTree.sweep(new RangeMarkerTree.Generator<HighlightInfo>(){
@Override
public boolean generateInStartOffsetOrder(@NotNull final Processor<HighlightInfo> processor) {
return ContainerUtil.process(infos, processor);
}
}, new SweepProcessor<HighlightInfo>() {
@Override
public boolean process(int offset, @NotNull HighlightInfo info, boolean atStart, @NotNull Collection<HighlightInfo> overlappingIntervals) {
if (!atStart) {
return true;
}
if (info.isFileLevelAnnotation() && psiFile != null && psiFile.getViewProvider().isPhysical()) {
codeAnalyzer.addFileLevelHighlight(project, group, info, psiFile);
changed[0] = true;
return true;
}
if (isWarningCoveredByError(info, overlappingIntervals, severityRegistrar)) {
return true;
}
if (info.getStartOffset() >= range.getStartOffset() && info.getEndOffset() <= range.getEndOffset() && psiFile != null) {
createOrReuseHighlighterFor(info, colorsScheme, document, group, psiFile, markup, infosToRemove, ranges2markersCache, severityRegistrar);
changed[0] = true;
}
return true;
}
});
for (RangeHighlighter highlighter : infosToRemove.forAllInGarbageBin()) {
highlighter.dispose();
changed[0] = true;
}
if (changed[0]) {
clearWhiteSpaceOptimizationFlag(document);
}
assertMarkupConsistent(markup, project);
}
private static boolean isWarningCoveredByError(@NotNull HighlightInfo info,
@NotNull Collection<HighlightInfo> overlappingIntervals,
@NotNull SeverityRegistrar severityRegistrar) {
if (!isSevere(info, severityRegistrar)) {
for (HighlightInfo overlapping : overlappingIntervals) {
if (isCovered(info, severityRegistrar, overlapping)) return true;
}
}
return false;
}
private static boolean isCovered(@NotNull HighlightInfo warning, @NotNull SeverityRegistrar severityRegistrar, @NotNull HighlightInfo candidate) {
if (!isCoveredByOffsets(warning, candidate)) return false;
HighlightSeverity severity = candidate.getSeverity();
if (severity == HighlightInfoType.SYMBOL_TYPE_SEVERITY) return false; // syntax should not interfere with warnings
return isSevere(candidate, severityRegistrar);
}
private static boolean isSevere(@NotNull HighlightInfo info, @NotNull SeverityRegistrar severityRegistrar) {
HighlightSeverity severity = info.getSeverity();
return severityRegistrar.compare(HighlightSeverity.ERROR, severity) <= 0 || severity == HighlightInfoType.SYMBOL_TYPE_SEVERITY;
}
private static void createOrReuseHighlighterFor(@NotNull final HighlightInfo info,
@Nullable final EditorColorsScheme colorsScheme, // if null global scheme will be used
@NotNull final Document document,
final int group,
@NotNull final PsiFile psiFile,
@NotNull MarkupModelEx markup,
@Nullable HighlightersRecycler infosToRemove,
@NotNull final Map<TextRange, RangeMarker> ranges2markersCache,
@NotNull SeverityRegistrar severityRegistrar) {
int infoStartOffset = info.startOffset;
int infoEndOffset = info.endOffset;
final int docLength = document.getTextLength();
if (infoEndOffset > docLength) {
infoEndOffset = docLength;
infoStartOffset = Math.min(infoStartOffset, infoEndOffset);
}
if (infoEndOffset == infoStartOffset && !info.isAfterEndOfLine()) {
if (infoEndOffset == docLength) return; // empty highlighter beyond file boundaries
infoEndOffset++; //show something in case of empty highlightinfo
}
info.setGroup(group);
int layer = getLayer(info, severityRegistrar);
RangeHighlighterEx highlighter = infosToRemove == null ? null : (RangeHighlighterEx)infosToRemove.pickupHighlighterFromGarbageBin(info.startOffset, info.endOffset, layer);
final TextRange finalInfoRange = new TextRange(infoStartOffset, infoEndOffset);
final TextAttributes infoAttributes = info.getTextAttributes(psiFile, colorsScheme);
Consumer<RangeHighlighterEx> changeAttributes = new Consumer<RangeHighlighterEx>() {
@Override
public void consume(RangeHighlighterEx finalHighlighter) {
finalHighlighter.setTextAttributes(infoAttributes);
info.highlighter = finalHighlighter;
finalHighlighter.setAfterEndOfLine(info.isAfterEndOfLine());
Color color = info.getErrorStripeMarkColor(psiFile, colorsScheme);
finalHighlighter.setErrorStripeMarkColor(color);
if (info != finalHighlighter.getErrorStripeTooltip()) {
finalHighlighter.setErrorStripeTooltip(info);
}
GutterMark renderer = info.getGutterIconRenderer();
finalHighlighter.setGutterIconRenderer((GutterIconRenderer)renderer);
ranges2markersCache.put(finalInfoRange, info.highlighter);
if (info.quickFixActionRanges != null) {
List<Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker>> list =
new ArrayList<Pair<HighlightInfo.IntentionActionDescriptor, RangeMarker>>(info.quickFixActionRanges.size());
for (Pair<HighlightInfo.IntentionActionDescriptor, TextRange> pair : info.quickFixActionRanges) {
TextRange textRange = pair.second;
RangeMarker marker = getOrCreate(document, ranges2markersCache, textRange);
list.add(Pair.create(pair.first, marker));
}
info.quickFixActionMarkers = ContainerUtil.createLockFreeCopyOnWriteList(list);
}
ProperTextRange fixRange = info.getFixTextRange();
if (finalInfoRange.equals(fixRange)) {
info.fixMarker = null; // null means it the same as highlighter'
}
else {
info.fixMarker = getOrCreate(document, ranges2markersCache, fixRange);
}
}
};
if (highlighter == null) {
highlighter = markup.addRangeHighlighterAndChangeAttributes(infoStartOffset, infoEndOffset, layer, null,
HighlighterTargetArea.EXACT_RANGE, false, changeAttributes);
}
else {
markup.changeAttributesInBatch(highlighter, changeAttributes);
}
boolean attributesSet = Comparing.equal(infoAttributes, highlighter.getTextAttributes());
assert attributesSet : "Info: " + infoAttributes +
"; colorsScheme: " + (colorsScheme == null ? "[global]" : colorsScheme.getName()) +
"; highlighter:" + highlighter.getTextAttributes();
}
private static int getLayer(@NotNull HighlightInfo info, @NotNull SeverityRegistrar severityRegistrar) {
final HighlightSeverity severity = info.getSeverity();
int layer;
if (severity == HighlightSeverity.WARNING) {
layer = HighlighterLayer.WARNING;
}
else if (severityRegistrar.compare(severity, HighlightSeverity.ERROR) >= 0) {
layer = HighlighterLayer.ERROR;
}
else if (severity == HighlightInfoType.INJECTED_FRAGMENT_SEVERITY) {
layer = HighlighterLayer.CARET_ROW-1;
}
else if (severity == HighlightInfoType.ELEMENT_UNDER_CARET_SEVERITY) {
layer = HighlighterLayer.ELEMENT_UNDER_CARET;
}
else {
layer = HighlighterLayer.ADDITIONAL_SYNTAX;
}
return layer;
}
private static RangeMarker getOrCreate(@NotNull Document document, @NotNull Map<TextRange, RangeMarker> ranges2markersCache, @NotNull TextRange textRange) {
RangeMarker marker = ranges2markersCache.get(textRange);
if (marker == null) {
marker = document.createRangeMarker(textRange);
ranges2markersCache.put(textRange, marker);
}
return marker;
}
private static final Key<Boolean> TYPING_INSIDE_HIGHLIGHTER_OCCURRED = Key.create("TYPING_INSIDE_HIGHLIGHTER_OCCURRED");
static boolean isWhitespaceOptimizationAllowed(@NotNull Document document) {
return document.getUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED) == null;
}
private static void disableWhiteSpaceOptimization(@NotNull Document document) {
document.putUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED, Boolean.TRUE);
}
private static void clearWhiteSpaceOptimizationFlag(@NotNull Document document) {
document.putUserData(TYPING_INSIDE_HIGHLIGHTER_OCCURRED, null);
}
static void updateHighlightersByTyping(@NotNull Project project, @NotNull DocumentEvent e) {
ApplicationManager.getApplication().assertIsDispatchThread();
final Document document = e.getDocument();
if (document instanceof DocumentEx && ((DocumentEx)document).isInBulkUpdate()) return;
final MarkupModel markup = DocumentMarkupModel.forDocument(document, project, true);
assertMarkupConsistent(markup, project);
final int start = e.getOffset() - 1;
final int end = start + Math.max(e.getOldLength(), e.getNewLength());
final List<HighlightInfo> toRemove = new ArrayList<HighlightInfo>();
DaemonCodeAnalyzerEx.processHighlights(document, project, null, start, end, new Processor<HighlightInfo>() {
@Override
public boolean process(HighlightInfo info) {
RangeHighlighter highlighter = info.highlighter;
boolean remove = false;
if (info.needUpdateOnTyping()) {
int highlighterStart = highlighter.getStartOffset();
int highlighterEnd = highlighter.getEndOffset();
if (info.isAfterEndOfLine()) {
if (highlighterStart < document.getTextLength()) {
highlighterStart += 1;
}
if (highlighterEnd < document.getTextLength()) {
highlighterEnd += 1;
}
}
if (!highlighter.isValid() || start < highlighterEnd && highlighterStart <= end) {
remove = true;
}
}
if (remove) {
toRemove.add(info);
}
return true;
}
});
for (HighlightInfo info : toRemove) {
if (!info.highlighter.isValid() || info.type.equals(HighlightInfoType.WRONG_REF)) {
info.highlighter.dispose();
}
}
assertMarkupConsistent(markup, project);
if (!toRemove.isEmpty()) {
disableWhiteSpaceOptimization(document);
}
}
private static void assertMarkupConsistent(@NotNull final MarkupModel markup, @NotNull Project project) {
if (!RedBlackTree.VERIFY) {
return;
}
Document document = markup.getDocument();
DaemonCodeAnalyzerEx.processHighlights(document, project, null, 0, document.getTextLength(), new Processor<HighlightInfo>() {
@Override
public boolean process(HighlightInfo info) {
assert ((MarkupModelEx)markup).containsHighlighter(info.highlighter);
return true;
}
});
RangeHighlighter[] allHighlighters = markup.getAllHighlighters();
for (RangeHighlighter highlighter : allHighlighters) {
if (!highlighter.isValid()) continue;
Object tooltip = highlighter.getErrorStripeTooltip();
if (!(tooltip instanceof HighlightInfo)) {
continue;
}
final HighlightInfo info = (HighlightInfo)tooltip;
boolean contains = !DaemonCodeAnalyzerEx
.processHighlights(document, project, null, info.getActualStartOffset(), info.getActualEndOffset(), new Processor<HighlightInfo>() {
@Override
public boolean process(HighlightInfo highlightInfo) {
return BY_START_OFFSET_NODUPS.compare(highlightInfo, info) != 0;
}
});
assert contains: info;
}
}
}