blob: fd9e6dfe0ad6a820373447f4c82d25c8afb9e04b [file] [log] [blame]
/*
* Copyright 2000-2013 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.codeHighlighting.TextEditorHighlightingPass;
import com.intellij.codeInsight.daemon.DaemonBundle;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.daemon.impl.analysis.CustomHighlightInfoHolder;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingLevelManager;
import com.intellij.codeInsight.problems.ProblemImpl;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.problems.Problem;
import com.intellij.problems.WolfTheProblemSolver;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.PsiTodoSearchHelper;
import com.intellij.psi.search.TodoItem;
import com.intellij.util.NotNullProducer;
import com.intellij.util.SmartList;
import com.intellij.util.containers.Stack;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
public class GeneralHighlightingPass extends ProgressableTextEditorHighlightingPass implements DumbAware {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.GeneralHighlightingPass");
static final String PRESENTABLE_NAME = DaemonBundle.message("pass.syntax");
private static final Key<Boolean> HAS_ERROR_ELEMENT = Key.create("HAS_ERROR_ELEMENT");
protected static final Condition<PsiFile> FILE_FILTER = new Condition<PsiFile>() {
@Override
public boolean value(PsiFile file) {
return HighlightingLevelManager.getInstance(file.getProject()).shouldHighlight(file);
}
};
protected final int myStartOffset;
protected final int myEndOffset;
protected final boolean myUpdateAll;
protected final ProperTextRange myPriorityRange;
protected final Editor myEditor;
protected final List<HighlightInfo> myHighlights = new ArrayList<HighlightInfo>();
protected volatile boolean myHasErrorElement;
private volatile boolean myErrorFound;
private static final Comparator<HighlightVisitor> VISITOR_ORDER_COMPARATOR = new Comparator<HighlightVisitor>() {
@Override
public int compare(final HighlightVisitor o1, final HighlightVisitor o2) {
return o1.order() - o2.order();
}
};
protected final EditorColorsScheme myGlobalScheme;
private NotNullProducer<HighlightVisitor[]> myHighlightVisitorProducer = new NotNullProducer<HighlightVisitor[]>() {
@NotNull
@Override
public HighlightVisitor[] produce() {
return cloneHighlightVisitors();
}
};
public GeneralHighlightingPass(@NotNull Project project,
@NotNull PsiFile file,
@NotNull Document document,
int startOffset,
int endOffset,
boolean updateAll,
@NotNull ProperTextRange priorityRange,
@Nullable Editor editor,
@NotNull HighlightInfoProcessor highlightInfoProcessor) {
super(project, document, PRESENTABLE_NAME, file, editor, TextRange.create(startOffset, endOffset), true, highlightInfoProcessor);
myStartOffset = startOffset;
myEndOffset = endOffset;
myUpdateAll = updateAll;
myPriorityRange = priorityRange;
myEditor = editor;
LOG.assertTrue(file.isValid());
boolean wholeFileHighlighting = isWholeFileHighlighting();
myHasErrorElement = !wholeFileHighlighting && Boolean.TRUE.equals(myFile.getUserData(HAS_ERROR_ELEMENT));
final DaemonCodeAnalyzerEx daemonCodeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(myProject);
FileStatusMap fileStatusMap = daemonCodeAnalyzer.getFileStatusMap();
myErrorFound = !wholeFileHighlighting && fileStatusMap.wasErrorFound(myDocument);
// initial guess to show correct progress in the traffic light icon
setProgressLimit(document.getTextLength()/2); // approx number of PSI elements = file length/2
myGlobalScheme = EditorColorsManager.getInstance().getGlobalScheme();
}
private static final Key<AtomicInteger> HIGHLIGHT_VISITOR_INSTANCE_COUNT = new Key<AtomicInteger>("HIGHLIGHT_VISITOR_INSTANCE_COUNT");
@NotNull
private HighlightVisitor[] cloneHighlightVisitors() {
int oldCount = incVisitorUsageCount(1);
HighlightVisitor[] highlightVisitors = Extensions.getExtensions(HighlightVisitor.EP_HIGHLIGHT_VISITOR, myProject);
if (oldCount != 0) {
HighlightVisitor[] clones = new HighlightVisitor[highlightVisitors.length];
for (int i = 0; i < highlightVisitors.length; i++) {
HighlightVisitor highlightVisitor = highlightVisitors[i];
HighlightVisitor cloned = highlightVisitor.clone();
assert cloned.getClass() == highlightVisitor.getClass() : highlightVisitor.getClass()+".clone() must return a copy of "+highlightVisitor.getClass()+"; but got: "+cloned+" of "+cloned.getClass();
clones[i] = cloned;
}
highlightVisitors = clones;
}
return highlightVisitors;
}
@NotNull
private HighlightVisitor[] filterVisitors(@NotNull HighlightVisitor[] highlightVisitors, @NotNull PsiFile psiFile) {
final List<HighlightVisitor> visitors = new ArrayList<HighlightVisitor>(highlightVisitors.length);
List<HighlightVisitor> list = Arrays.asList(highlightVisitors);
for (HighlightVisitor visitor : DumbService.getInstance(myProject).filterByDumbAwareness(list)) {
if (visitor.suitableForFile(psiFile)) {
visitors.add(visitor);
}
}
LOG.assertTrue(!visitors.isEmpty(), list);
HighlightVisitor[] visitorArray = visitors.toArray(new HighlightVisitor[visitors.size()]);
Arrays.sort(visitorArray, VISITOR_ORDER_COMPARATOR);
return visitorArray;
}
public void setHighlightVisitorProducer(@NotNull NotNullProducer<HighlightVisitor[]> highlightVisitorProducer) {
myHighlightVisitorProducer = highlightVisitorProducer;
}
@NotNull
protected HighlightVisitor[] getHighlightVisitors(@NotNull PsiFile psiFile) {
return filterVisitors(myHighlightVisitorProducer.produce(), psiFile);
}
// returns old value
public int incVisitorUsageCount(int delta) {
AtomicInteger count = myProject.getUserData(HIGHLIGHT_VISITOR_INSTANCE_COUNT);
if (count == null) {
count = ((UserDataHolderEx)myProject).putUserDataIfAbsent(HIGHLIGHT_VISITOR_INSTANCE_COUNT, new AtomicInteger(0));
}
int old = count.getAndAdd(delta);
assert old + delta >= 0 : old +";" + delta;
return old;
}
@Override
protected void collectInformationWithProgress(@NotNull final ProgressIndicator progress) {
final List<HighlightInfo> outsideResult = new ArrayList<HighlightInfo>(100);
final List<HighlightInfo> insideResult = new ArrayList<HighlightInfo>(100);
final DaemonCodeAnalyzerEx daemonCodeAnalyzer = DaemonCodeAnalyzerEx.getInstanceEx(myProject);
final HighlightVisitor[] filteredVisitors = getHighlightVisitors(myFile);
final List<PsiElement> insideElements = new ArrayList<PsiElement>();
final List<PsiElement> outsideElements = new ArrayList<PsiElement>();
try {
List<ProperTextRange> insideRanges = new ArrayList<ProperTextRange>();
List<ProperTextRange> outsideRanges = new ArrayList<ProperTextRange>();
Divider.divideInsideAndOutside(myFile, myStartOffset, myEndOffset, myPriorityRange, insideElements, insideRanges, outsideElements,
outsideRanges, false, FILE_FILTER);
setProgressLimit((long)(insideElements.size()+outsideElements.size()));
final boolean forceHighlightParents = forceHighlightParents();
if (!isDumbMode()) {
highlightTodos(myFile, myDocument.getCharsSequence(), myStartOffset, myEndOffset, progress, myPriorityRange, insideResult,
outsideResult);
}
Runnable after1 = new Runnable() {
@Override
public void run() {
final TextRange priorityIntersection = myPriorityRange.intersection(myRestrictRange);
if ((!insideElements.isEmpty() || !insideResult.isEmpty()) &&
priorityIntersection != null) { // do not apply when there were no elements to highlight
myHighlightInfoProcessor.highlightsInsideVisiblePartAreProduced(myHighlightingSession, insideResult, myPriorityRange, myRestrictRange);
}
}
};
collectHighlights(insideElements, insideRanges, after1, outsideElements, outsideRanges, progress, filteredVisitors, insideResult, outsideResult, forceHighlightParents);
myHighlightInfoProcessor.highlightsOutsideVisiblePartAreProduced(myHighlightingSession, outsideResult, myPriorityRange, myRestrictRange);
if (myUpdateAll) {
daemonCodeAnalyzer.getFileStatusMap().setErrorFoundFlag(myDocument, myErrorFound);
}
}
finally {
incVisitorUsageCount(-1);
}
myHighlights.addAll(insideResult);
myHighlights.addAll(outsideResult);
}
protected boolean isFailFastOnAcquireReadAction() {
return true;
}
private boolean isWholeFileHighlighting() {
return myUpdateAll && myStartOffset == 0 && myEndOffset == myDocument.getTextLength();
}
@Override
protected void applyInformationWithProgress() {
myFile.putUserData(HAS_ERROR_ELEMENT, myHasErrorElement);
if (myUpdateAll) {
reportErrorsToWolf();
}
}
@Override
@NotNull
public List<HighlightInfo> getInfos() {
return new ArrayList<HighlightInfo>(myHighlights);
}
private void collectHighlights(@NotNull final List<PsiElement> elements1,
@NotNull final List<ProperTextRange> ranges1,
@NotNull final Runnable after1,
@NotNull final List<PsiElement> elements2,
@NotNull final List<ProperTextRange> ranges2,
@NotNull final ProgressIndicator progress,
@NotNull final HighlightVisitor[] visitors,
@NotNull final List<HighlightInfo> insideResult,
@NotNull final List<HighlightInfo> outsideResult,
final boolean forceHighlightParents) {
final Set<PsiElement> skipParentsSet = new THashSet<PsiElement>();
// TODO - add color scheme to holder
final HighlightInfoHolder holder = createInfoHolder(myFile);
final int chunkSize = Math.max(1, (elements1.size()+elements2.size()) / 100); // one percent precision is enough
final Runnable action = new Runnable() {
@Override
public void run() {
Stack<Pair<TextRange, List<HighlightInfo>>> nested = new Stack<Pair<TextRange, List<HighlightInfo>>>();
boolean failed = false;
List<ProperTextRange> ranges = ranges1;
//noinspection unchecked
for (List<PsiElement> elements : new List[]{elements1, elements2}) {
nested.clear();
int nextLimit = chunkSize;
for (int i = 0; i < elements.size(); i++) {
PsiElement element = elements.get(i);
progress.checkCanceled();
PsiElement parent = element.getParent();
if (element != myFile && !skipParentsSet.isEmpty() && element.getFirstChild() != null && skipParentsSet.contains(element)) {
skipParentsSet.add(parent);
continue;
}
if (element instanceof PsiErrorElement) {
myHasErrorElement = true;
}
holder.clear();
for (final HighlightVisitor visitor : visitors) {
try {
visitor.visit(element);
}
catch (ProcessCanceledException e) {
throw e;
}
catch (IndexNotReadyException e) {
throw e;
}
catch (Exception e) {
if (!failed) {
LOG.error(e);
}
failed = true;
}
}
if (i == nextLimit) {
advanceProgress(chunkSize);
nextLimit = i + chunkSize;
}
TextRange elementRange = ranges.get(i);
List<HighlightInfo> infosForThisRange = holder.size() == 0 ? null : new ArrayList<HighlightInfo>(holder.size());
for (int j = 0; j < holder.size(); j++) {
final HighlightInfo info = holder.get(j);
assert info != null;
if (!myRestrictRange.containsRange(info.getStartOffset(), info.getEndOffset())) continue;
List<HighlightInfo> result = myPriorityRange.containsRange(info.getStartOffset(), info.getEndOffset()) ? insideResult : outsideResult;
// have to filter out already obtained highlights
if (!result.add(info)) continue;
boolean isError = info.getSeverity() == HighlightSeverity.ERROR;
if (isError) {
if (!forceHighlightParents) {
skipParentsSet.add(parent);
}
myErrorFound = true;
}
// if this highlight info range is exactly the same as the element range we are visiting
// that means we can clear this highlight as soon as visitors won't produce any highlights during visiting the same range next time.
info.setBijective(elementRange.equalsToRange(info.startOffset, info.endOffset));
myHighlightInfoProcessor.infoIsAvailable(myHighlightingSession, info);
//myTransferToEDTQueue.offer(info);
infosForThisRange.add(info);
}
// include infos which we got while visiting nested elements with the same range
while (true) {
if (!nested.isEmpty() && elementRange.contains(nested.peek().first)) {
Pair<TextRange, List<HighlightInfo>> old = nested.pop();
if (elementRange.equals(old.first)) {
if (infosForThisRange == null) {
infosForThisRange = old.second;
}
else if (old.second != null){
infosForThisRange.addAll(old.second);
}
}
}
else {
break;
}
}
nested.push(Pair.create(elementRange, infosForThisRange));
if (parent == null || !Comparing.equal(elementRange, parent.getTextRange())) {
myHighlightInfoProcessor.allHighlightsForRangeAreProduced(myHighlightingSession, elementRange, infosForThisRange);
//killAbandonedHighlightsUnder(elementRange, infosForThisRange, progress);
}
}
advanceProgress(elements.size() - (nextLimit-chunkSize));
if (elements == elements1) {
after1.run();
ranges = ranges2;
}
}
}
};
analyzeByVisitors(progress, visitors, holder, 0, action);
}
private void analyzeByVisitors(@NotNull final ProgressIndicator progress,
@NotNull final HighlightVisitor[] visitors,
@NotNull final HighlightInfoHolder holder,
final int i,
@NotNull final Runnable action) {
if (i == visitors.length) {
action.run();
}
else {
if (!visitors[i].analyze(myFile, myUpdateAll, holder, new Runnable() {
@Override
public void run() {
analyzeByVisitors(progress, visitors, holder, i+1, action);
}
})) {
cancelAndRestartDaemonLater(progress, myProject, this);
}
}
}
static void cancelAndRestartDaemonLater(@NotNull ProgressIndicator progress,
@NotNull final Project project,
@NotNull TextEditorHighlightingPass passCalledFrom) throws ProcessCanceledException {
progress.cancel();
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(100));
}
catch (InterruptedException e) {
LOG.error(e);
}
DaemonCodeAnalyzer.getInstance(project).restart();
}
}, project.getDisposed());
throw new ProcessCanceledException();
}
private boolean forceHighlightParents() {
boolean forceHighlightParents = false;
for(HighlightRangeExtension extension: Extensions.getExtensions(HighlightRangeExtension.EP_NAME)) {
if (extension.isForceHighlightParents(myFile)) {
forceHighlightParents = true;
break;
}
}
return forceHighlightParents;
}
protected HighlightInfoHolder createInfoHolder(final PsiFile file) {
final HighlightInfoFilter[] filters = HighlightInfoFilter.EXTENSION_POINT_NAME.getExtensions();
return new CustomHighlightInfoHolder(file, getColorsScheme(), filters);
}
protected static void highlightTodos(@NotNull PsiFile file,
@NotNull CharSequence text,
int startOffset,
int endOffset,
@NotNull ProgressIndicator progress,
@NotNull ProperTextRange priorityRange,
@NotNull Collection<HighlightInfo> insideResult,
@NotNull Collection<HighlightInfo> outsideResult) {
PsiTodoSearchHelper helper = PsiTodoSearchHelper.SERVICE.getInstance(file.getProject());
if (helper == null) return;
TodoItem[] todoItems = helper.findTodoItems(file, startOffset, endOffset);
if (todoItems.length == 0) return;
for (TodoItem todoItem : todoItems) {
progress.checkCanceled();
TextRange range = todoItem.getTextRange();
String description = text.subSequence(range.getStartOffset(), range.getEndOffset()).toString();
TextAttributes attributes = todoItem.getPattern().getAttributes().getTextAttributes();
HighlightInfo.Builder builder = HighlightInfo.newHighlightInfo(HighlightInfoType.TODO).range(range);
builder.textAttributes(attributes);
builder.descriptionAndTooltip(description);
HighlightInfo info = builder.createUnconditionally();
(priorityRange.containsRange(info.getStartOffset(), info.getEndOffset()) ? insideResult : outsideResult).add(info);
}
}
private void reportErrorsToWolf() {
if (!myFile.getViewProvider().isPhysical()) return; // e.g. errors in evaluate expression
Project project = myFile.getProject();
if (!PsiManager.getInstance(project).isInProject(myFile)) return; // do not report problems in libraries
VirtualFile file = myFile.getVirtualFile();
if (file == null) return;
List<Problem> problems = convertToProblems(getInfos(), file, myHasErrorElement);
WolfTheProblemSolver wolf = WolfTheProblemSolver.getInstance(project);
boolean hasErrors = DaemonCodeAnalyzerEx.hasErrors(project, getDocument());
if (!hasErrors || isWholeFileHighlighting()) {
wolf.reportProblems(file, problems);
}
else {
wolf.weHaveGotProblems(file, problems);
}
}
@Override
public double getProgress() {
// do not show progress of visible highlighters update
return myUpdateAll ? super.getProgress() : -1;
}
private static List<Problem> convertToProblems(@NotNull Collection<HighlightInfo> infos,
@NotNull VirtualFile file,
final boolean hasErrorElement) {
List<Problem> problems = new SmartList<Problem>();
for (HighlightInfo info : infos) {
if (info.getSeverity() == HighlightSeverity.ERROR) {
Problem problem = new ProblemImpl(file, info, hasErrorElement);
problems.add(problem);
}
}
return problems;
}
@Override
public String toString() {
return super.toString() + " updateAll="+myUpdateAll+" range=("+myStartOffset+","+myEndOffset+")";
}
}