blob: fab185e6ddc08b4b60aa80de82ae410620ccbdb4 [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.problems;
import com.intellij.codeInsight.daemon.impl.*;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vfs.*;
import com.intellij.problems.Problem;
import com.intellij.problems.WolfTheProblemSolver;
import com.intellij.psi.*;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author cdr
*/
public class WolfTheProblemSolverImpl extends WolfTheProblemSolver {
private final Map<VirtualFile, ProblemFileInfo> myProblems = new THashMap<VirtualFile, ProblemFileInfo>();
private final Collection<VirtualFile> myCheckingQueue = new THashSet<VirtualFile>(10);
private final Project myProject;
private final List<ProblemListener> myProblemListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final List<Condition<VirtualFile>> myFilters = ContainerUtil.createLockFreeCopyOnWriteList();
private boolean myFiltersLoaded = false;
private final ProblemListener fireProblemListeners = new ProblemListener() {
@Override
public void problemsAppeared(@NotNull VirtualFile file) {
for (final ProblemListener problemListener : myProblemListeners) {
problemListener.problemsAppeared(file);
}
}
@Override
public void problemsChanged(@NotNull VirtualFile file) {
for (final ProblemListener problemListener : myProblemListeners) {
problemListener.problemsChanged(file);
}
}
@Override
public void problemsDisappeared(@NotNull VirtualFile file) {
for (final ProblemListener problemListener : myProblemListeners) {
problemListener.problemsDisappeared(file);
}
}
};
private void doRemove(@NotNull VirtualFile problemFile) {
ProblemFileInfo old;
synchronized (myProblems) {
old = myProblems.remove(problemFile);
}
synchronized (myCheckingQueue) {
myCheckingQueue.remove(problemFile);
}
if (old != null) {
// firing outside lock
fireProblemListeners.problemsDisappeared(problemFile);
}
}
private static class ProblemFileInfo {
private final Collection<Problem> problems = new THashSet<Problem>();
private boolean hasSyntaxErrors;
public boolean equals(@Nullable final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final ProblemFileInfo that = (ProblemFileInfo)o;
return hasSyntaxErrors == that.hasSyntaxErrors && problems.equals(that.problems);
}
public int hashCode() {
int result = problems.hashCode();
result = 31 * result + (hasSyntaxErrors ? 1 : 0);
return result;
}
}
public WolfTheProblemSolverImpl(@NotNull Project project,
@NotNull PsiManager psiManager,
@NotNull VirtualFileManager virtualFileManager) {
myProject = project;
PsiTreeChangeListener changeListener = new PsiTreeChangeAdapter() {
@Override
public void childAdded(@NotNull PsiTreeChangeEvent event) {
childrenChanged(event);
}
@Override
public void childRemoved(@NotNull PsiTreeChangeEvent event) {
childrenChanged(event);
}
@Override
public void childReplaced(@NotNull PsiTreeChangeEvent event) {
childrenChanged(event);
}
@Override
public void childMoved(@NotNull PsiTreeChangeEvent event) {
childrenChanged(event);
}
@Override
public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
childrenChanged(event);
}
@Override
public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
clearSyntaxErrorFlag(event);
}
};
psiManager.addPsiTreeChangeListener(changeListener);
VirtualFileListener virtualFileListener = new VirtualFileAdapter() {
@Override
public void fileDeleted(@NotNull final VirtualFileEvent event) {
onDeleted(event.getFile());
}
@Override
public void fileMoved(@NotNull final VirtualFileMoveEvent event) {
onDeleted(event.getFile());
}
private void onDeleted(@NotNull final VirtualFile file) {
if (file.isDirectory()) {
clearInvalidFiles();
}
else {
doRemove(file);
}
}
};
virtualFileManager.addVirtualFileListener(virtualFileListener, myProject);
FileStatusManager fileStatusManager = FileStatusManager.getInstance(myProject);
if (fileStatusManager != null) { //tests?
fileStatusManager.addFileStatusListener(new FileStatusListener() {
@Override
public void fileStatusesChanged() {
clearInvalidFiles();
}
@Override
public void fileStatusChanged(@NotNull VirtualFile virtualFile) {
fileStatusesChanged();
}
});
}
}
private void clearInvalidFiles() {
VirtualFile[] files;
synchronized (myProblems) {
files = VfsUtilCore.toVirtualFileArray(myProblems.keySet());
}
for (VirtualFile problemFile : files) {
if (!problemFile.isValid() || !isToBeHighlighted(problemFile)) {
doRemove(problemFile);
}
}
}
private void clearSyntaxErrorFlag(@NotNull final PsiTreeChangeEvent event) {
PsiFile file = event.getFile();
if (file == null) return;
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) return;
synchronized (myProblems) {
ProblemFileInfo info = myProblems.get(virtualFile);
if (info != null) {
info.hasSyntaxErrors = false;
}
}
}
public void startCheckingIfVincentSolvedProblemsYet(@NotNull ProgressIndicator progress,
@NotNull ProgressableTextEditorHighlightingPass pass)
throws ProcessCanceledException {
if (!myProject.isOpen()) return;
List<VirtualFile> files;
synchronized (myCheckingQueue) {
files = new ArrayList<VirtualFile>(myCheckingQueue);
}
long progressLimit = 0;
for (VirtualFile file : files) {
if (file.isValid()) {
progressLimit += file.getLength(); // (rough approx number of PSI elements = file length/2) * (visitor count = 2 usually)
}
}
pass.setProgressLimit(progressLimit);
for (final VirtualFile virtualFile : files) {
progress.checkCanceled();
if (virtualFile == null) break;
if (!virtualFile.isValid() || orderVincentToCleanTheCar(virtualFile, progress)) {
doRemove(virtualFile);
}
if (virtualFile.isValid()) pass.advanceProgress(virtualFile.getLength());
}
}
// returns true if car has been cleaned
private boolean orderVincentToCleanTheCar(@NotNull final VirtualFile file,
@NotNull final ProgressIndicator progressIndicator) throws ProcessCanceledException {
if (!isToBeHighlighted(file)) {
clearProblems(file);
return true; // file is going to be red waved no more
}
if (hasSyntaxErrors(file)) {
// optimization: it's no use anyway to try clean the file with syntax errors, only changing the file itself can help
return false;
}
if (myProject.isDisposed()) return false;
if (willBeHighlightedAnyway(file)) return false;
final PsiFile psiFile = PsiManager.getInstance(myProject).findFile(file);
if (psiFile == null) return false;
final Document document = FileDocumentManager.getInstance().getDocument(file);
if (document == null) return false;
final AtomicReference<HighlightInfo> error = new AtomicReference<HighlightInfo>();
final AtomicBoolean hasErrorElement = new AtomicBoolean();
try {
GeneralHighlightingPass pass = new GeneralHighlightingPass(myProject, psiFile, document, 0, document.getTextLength(),
false, new ProperTextRange(0, document.getTextLength()), null, HighlightInfoProcessor.getEmpty()) {
@Override
protected HighlightInfoHolder createInfoHolder(@NotNull final PsiFile file) {
return new HighlightInfoHolder(file) {
@Override
public boolean add(@Nullable HighlightInfo info) {
if (info != null && info.getSeverity() == HighlightSeverity.ERROR) {
error.set(info);
hasErrorElement.set(myHasErrorElement);
throw new ProcessCanceledException();
}
return super.add(info);
}
};
}
};
pass.collectInformation(progressIndicator);
}
catch (ProcessCanceledException e) {
if (error.get() != null) {
ProblemImpl problem = new ProblemImpl(file, error.get(), hasErrorElement.get());
reportProblems(file, Collections.<Problem>singleton(problem));
}
return false;
}
clearProblems(file);
return true;
}
@Override
public boolean hasSyntaxErrors(final VirtualFile file) {
synchronized (myProblems) {
ProblemFileInfo info = myProblems.get(file);
return info != null && info.hasSyntaxErrors;
}
}
private boolean willBeHighlightedAnyway(final VirtualFile file) {
// opened in some editor, and hence will be highlighted automatically sometime later
FileEditor[] selectedEditors = FileEditorManager.getInstance(myProject).getSelectedEditors();
for (FileEditor editor : selectedEditors) {
if (!(editor instanceof TextEditor)) continue;
Document document = ((TextEditor)editor).getEditor().getDocument();
PsiFile psiFile = PsiDocumentManager.getInstance(myProject).getCachedPsiFile(document);
if (psiFile == null) continue;
if (Comparing.equal(file, psiFile.getVirtualFile())) return true;
}
return false;
}
@Override
public boolean hasProblemFilesBeneath(@NotNull Condition<VirtualFile> condition) {
if (!myProject.isOpen()) return false;
synchronized (myProblems) {
if (!myProblems.isEmpty()) {
Set<VirtualFile> problemFiles = myProblems.keySet();
for (VirtualFile problemFile : problemFiles) {
if (problemFile.isValid() && condition.value(problemFile)) return true;
}
}
return false;
}
}
@Override
public boolean hasProblemFilesBeneath(@NotNull final Module scope) {
return hasProblemFilesBeneath(new Condition<VirtualFile>() {
@Override
public boolean value(final VirtualFile virtualFile) {
return ModuleUtilCore.moduleContainsFile(scope, virtualFile, false);
}
});
}
@Override
public void addProblemListener(@NotNull ProblemListener listener) {
myProblemListeners.add(listener);
}
@Override
public void addProblemListener(@NotNull final ProblemListener listener, @NotNull Disposable parentDisposable) {
addProblemListener(listener);
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
removeProblemListener(listener);
}
});
}
@Override
public void removeProblemListener(@NotNull ProblemListener listener) {
myProblemListeners.remove(listener);
}
@Override
public void registerFileHighlightFilter(@NotNull final Condition<VirtualFile> filter, @NotNull Disposable parentDisposable) {
myFilters.add(filter);
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
myFilters.remove(filter);
}
});
}
@Override
public void queue(VirtualFile suspiciousFile) {
if (!isToBeHighlighted(suspiciousFile)) return;
doQueue(suspiciousFile);
}
private void doQueue(@NotNull VirtualFile suspiciousFile) {
synchronized (myCheckingQueue) {
myCheckingQueue.add(suspiciousFile);
}
}
@Override
public boolean isProblemFile(VirtualFile virtualFile) {
synchronized (myProblems) {
return myProblems.containsKey(virtualFile);
}
}
private boolean isToBeHighlighted(@Nullable VirtualFile virtualFile) {
if (virtualFile == null) return false;
synchronized (myFilters) {
if (!myFiltersLoaded) {
myFiltersLoaded = true;
myFilters.addAll(Arrays.asList(Extensions.getExtensions(FILTER_EP_NAME, myProject)));
}
}
for (final Condition<VirtualFile> filter : myFilters) {
ProgressManager.checkCanceled();
if (filter.value(virtualFile)) {
return true;
}
}
return false;
}
@Override
public void weHaveGotProblems(@NotNull final VirtualFile virtualFile, @NotNull List<Problem> problems) {
if (problems.isEmpty()) return;
if (!isToBeHighlighted(virtualFile)) return;
weHaveGotNonIgnorableProblems(virtualFile, problems);
}
@Override
public void weHaveGotNonIgnorableProblems(@NotNull VirtualFile virtualFile, @NotNull List<Problem> problems) {
if (problems.isEmpty()) return;
boolean fireListener = false;
synchronized (myProblems) {
ProblemFileInfo storedProblems = myProblems.get(virtualFile);
if (storedProblems == null) {
storedProblems = new ProblemFileInfo();
myProblems.put(virtualFile, storedProblems);
fireListener = true;
}
storedProblems.problems.addAll(problems);
}
doQueue(virtualFile);
if (fireListener) {
fireProblemListeners.problemsAppeared(virtualFile);
}
}
@Override
public void clearProblems(@NotNull VirtualFile virtualFile) {
doRemove(virtualFile);
}
@Override
public Problem convertToProblem(@Nullable final VirtualFile virtualFile,
final int line,
final int column,
@NotNull final String[] message) {
if (virtualFile == null || virtualFile.isDirectory() || virtualFile.getFileType().isBinary()) return null;
HighlightInfo info = ApplicationManager.getApplication().runReadAction(new Computable<HighlightInfo>() {
@Override
public HighlightInfo compute() {
TextRange textRange = getTextRange(virtualFile, line, column);
String description = StringUtil.join(message, "\n");
return HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(textRange).descriptionAndTooltip(description).create();
}
});
if (info == null) return null;
return new ProblemImpl(virtualFile, info, false);
}
@Override
public void reportProblems(@NotNull final VirtualFile file, @NotNull Collection<Problem> problems) {
if (problems.isEmpty()) {
clearProblems(file);
return;
}
if (!isToBeHighlighted(file)) return;
boolean hasProblemsBefore;
boolean fireChanged;
synchronized (myProblems) {
final ProblemFileInfo oldInfo = myProblems.remove(file);
hasProblemsBefore = oldInfo != null;
ProblemFileInfo newInfo = new ProblemFileInfo();
myProblems.put(file, newInfo);
for (Problem problem : problems) {
newInfo.problems.add(problem);
newInfo.hasSyntaxErrors |= ((ProblemImpl)problem).isSyntaxOnly();
}
fireChanged = hasProblemsBefore && !oldInfo.equals(newInfo);
}
doQueue(file);
if (!hasProblemsBefore) {
fireProblemListeners.problemsAppeared(file);
}
else if (fireChanged) {
fireProblemListeners.problemsChanged(file);
}
}
@NotNull
private static TextRange getTextRange(@NotNull final VirtualFile virtualFile, int line, final int column) {
Document document = FileDocumentManager.getInstance().getDocument(virtualFile);
if (line > document.getLineCount()) line = document.getLineCount();
line = line <= 0 ? 0 : line - 1;
int offset = document.getLineStartOffset(line) + (column <= 0 ? 0 : column - 1);
return new TextRange(offset, offset);
}
}