blob: 0bb406f235454f0263ef7f897aa3fc276d989e6b [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.codeHighlighting.DirtyScopeTrackingHighlightingPassFactory;
import com.intellij.codeHighlighting.Pass;
import com.intellij.codeHighlighting.TextEditorHighlightingPassRegistrar;
import com.intellij.codeInsight.daemon.ProblemHighlightFilter;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.WeakHashMap;
import gnu.trove.TIntObjectHashMap;
import gnu.trove.TIntObjectProcedure;
import gnu.trove.TObjectFunction;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import java.util.Map;
public class FileStatusMap implements Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.FileStatusMap");
private final Project myProject;
private final Map<Document,FileStatus> myDocumentToStatusMap = new WeakHashMap<Document, FileStatus>(); // all dirty if absent
private volatile boolean myAllowDirt = true;
public FileStatusMap(@NotNull Project project) {
myProject = project;
}
@Override
public void dispose() {
// clear dangling references to PsiFiles/Documents. SCR#10358
markAllFilesDirty();
}
@Nullable("null means the file is clean")
public static TextRange getDirtyTextRange(@NotNull Editor editor, int passId) {
Document document = editor.getDocument();
FileStatusMap me = DaemonCodeAnalyzerEx.getInstanceEx(editor.getProject()).getFileStatusMap();
TextRange dirtyScope = me.getFileDirtyScope(document, passId);
if (dirtyScope == null) return null;
TextRange documentRange = TextRange.from(0, document.getTextLength());
return documentRange.intersection(dirtyScope);
}
public void setErrorFoundFlag(@NotNull Document document, boolean errorFound) {
//GHP has found error. Flag is used by ExternalToolPass to decide whether to run or not
synchronized(myDocumentToStatusMap) {
FileStatus status = myDocumentToStatusMap.get(document);
if (status == null){
if (!errorFound) return;
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
assert file != null : document;
status = new FileStatus(file.getProject());
myDocumentToStatusMap.put(document, status);
}
status.errorFound = errorFound;
}
}
public boolean wasErrorFound(@NotNull Document document) {
synchronized(myDocumentToStatusMap) {
FileStatus status = myDocumentToStatusMap.get(document);
return status != null && status.errorFound;
}
}
public boolean isMarkedDirtyDefensively(@NotNull Document document) {
synchronized(myDocumentToStatusMap) {
FileStatus status = myDocumentToStatusMap.get(document);
return status != null && status.defensivelyMarked;
}
}
private static class FileStatus {
public boolean defensivelyMarked; // file marked dirty without knowledge of specific dirty region. Subsequent markScopeDirty can refine dirty scope, not extend it
private boolean wolfPassFinished;
// if contains the special value "WHOLE_FILE_MARKER" then the corresponding range is (0, document length)
private final TIntObjectHashMap<RangeMarker> dirtyScopes = new TIntObjectHashMap<RangeMarker>();
private boolean errorFound;
private FileStatus(@NotNull Project project) {
markWholeFileDirty(project);
}
private void markWholeFileDirty(@NotNull Project project) {
dirtyScopes.put(Pass.UPDATE_ALL, WHOLE_FILE_DIRTY_MARKER);
dirtyScopes.put(Pass.EXTERNAL_TOOLS, WHOLE_FILE_DIRTY_MARKER);
dirtyScopes.put(Pass.LOCAL_INSPECTIONS, WHOLE_FILE_DIRTY_MARKER);
TextEditorHighlightingPassRegistrarEx registrar = (TextEditorHighlightingPassRegistrarEx) TextEditorHighlightingPassRegistrar.getInstance(project);
for(DirtyScopeTrackingHighlightingPassFactory factory: registrar.getDirtyScopeTrackingFactories()) {
dirtyScopes.put(factory.getPassId(), WHOLE_FILE_DIRTY_MARKER);
}
}
public boolean allDirtyScopesAreNull() {
for (Object o : dirtyScopes.getValues()) {
if (o != null) return false;
}
return true;
}
public void combineScopesWith(@NotNull final TextRange scope, final int fileLength, @NotNull final Document document) {
dirtyScopes.transformValues(new TObjectFunction<RangeMarker, RangeMarker>() {
@Override
public RangeMarker execute(RangeMarker oldScope) {
RangeMarker newScope = combineScopes(oldScope, scope, fileLength, document);
if (newScope != oldScope && oldScope != null) oldScope.dispose();
return newScope;
}
});
}
@Override
public String toString() {
@NonNls final StringBuilder s = new StringBuilder();
s.append("defensivelyMarked = ").append(defensivelyMarked);
s.append("; wolfPassFinfished = ").append(wolfPassFinished);
s.append("; errorFound = ").append(errorFound);
s.append("; dirtyScopes: (");
dirtyScopes.forEachEntry(new TIntObjectProcedure<RangeMarker>() {
@Override
public boolean execute(int passId, RangeMarker rangeMarker) {
s.append(" pass: ").append(passId).append(" -> ").append(rangeMarker == WHOLE_FILE_DIRTY_MARKER ? "Whole file" : rangeMarker).append(";");
return true;
}
});
s.append(")");
return s.toString();
}
}
public void markAllFilesDirty() {
assertAllowModifications();
LOG.debug("********************************* Mark all dirty");
synchronized (myDocumentToStatusMap) {
myDocumentToStatusMap.clear();
}
}
private void assertAllowModifications() {
try {
assert myAllowDirt;
}
finally {
myAllowDirt = true; //give next test a chance
}
}
public void markFileUpToDate(@NotNull Document document, int passId) {
synchronized(myDocumentToStatusMap){
FileStatus status = myDocumentToStatusMap.get(document);
if (status == null){
status = new FileStatus(myProject);
myDocumentToStatusMap.put(document, status);
}
status.defensivelyMarked=false;
if (passId == Pass.WOLF) {
status.wolfPassFinished = true;
}
else if (status.dirtyScopes.containsKey(passId)) {
RangeMarker marker = status.dirtyScopes.get(passId);
if (marker != null) {
marker.dispose();
status.dirtyScopes.put(passId, null);
}
}
}
}
/**
* @return null for processed file, whole file for untouched or entirely dirty file, range(usually code block) for dirty region (optimization)
*/
@Nullable
public TextRange getFileDirtyScope(@NotNull Document document, int passId) {
synchronized(myDocumentToStatusMap){
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
if (!ProblemHighlightFilter.shouldHighlightFile(file)) return null;
FileStatus status = myDocumentToStatusMap.get(document);
if (status == null){
return file == null ? null : file.getTextRange();
}
if (status.defensivelyMarked) {
status.markWholeFileDirty(myProject);
status.defensivelyMarked = false;
}
LOG.assertTrue(status.dirtyScopes.containsKey(passId), "Unknown pass " + passId);
RangeMarker marker = status.dirtyScopes.get(passId);
return marker == null ? null : marker.isValid() ? TextRange.create(marker) : new TextRange(0, document.getTextLength());
}
}
public void markFileScopeDirty(@NotNull Document document, int passId) {
assertAllowModifications();
synchronized(myDocumentToStatusMap) {
FileStatus status = myDocumentToStatusMap.get(document);
if (status == null){
return;
}
if (passId == Pass.WOLF) {
status.wolfPassFinished = false;
}
else {
LOG.assertTrue(status.dirtyScopes.containsKey(passId));
RangeMarker marker = status.dirtyScopes.get(passId);
if (marker != null) {
marker.dispose();
}
status.dirtyScopes.put(passId, WHOLE_FILE_DIRTY_MARKER);
}
}
}
public void markFileScopeDirtyDefensively(@NotNull PsiFile file) {
assertAllowModifications();
if (LOG.isDebugEnabled()) {
LOG.debug("********************************* Mark dirty file defensively: "+file.getName());
}
// mark whole file dirty in case no subsequent PSI events will come, but file requires rehighlighting nevertheless
// e.g. in the case of quick typing/backspacing char
synchronized(myDocumentToStatusMap){
Document document = PsiDocumentManager.getInstance(myProject).getCachedDocument(file);
if (document == null) return;
FileStatus status = myDocumentToStatusMap.get(document);
if (status == null) return; // all dirty already
status.defensivelyMarked = true;
}
}
public void markFileScopeDirty(@NotNull Document document, @NotNull TextRange scope, int fileLength) {
assertAllowModifications();
if (LOG.isDebugEnabled()) {
LOG.debug("********************************* Mark dirty: "+scope);
}
synchronized(myDocumentToStatusMap) {
FileStatus status = myDocumentToStatusMap.get(document);
if (status == null) return; // all dirty already
if (status.defensivelyMarked) {
status.defensivelyMarked = false;
}
status.combineScopesWith(scope, fileLength, document);
}
}
@NotNull
private static RangeMarker combineScopes(RangeMarker old, @NotNull TextRange scope, int textLength, @NotNull Document document) {
if (old == null) {
if (scope.equalsToRange(0, textLength)) return WHOLE_FILE_DIRTY_MARKER;
return document.createRangeMarker(scope);
}
if (old == WHOLE_FILE_DIRTY_MARKER) return old;
TextRange oldRange = TextRange.create(old);
TextRange union = scope.union(oldRange);
if (old.isValid() && union.equals(oldRange)) {
return old;
}
if (union.getEndOffset() > textLength) {
union = union.intersection(new TextRange(0, textLength));
}
assert union != null;
return document.createRangeMarker(union);
}
public boolean allDirtyScopesAreNull(@NotNull Document document) {
synchronized (myDocumentToStatusMap) {
PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document);
if (!ProblemHighlightFilter.shouldHighlightFile(file)) return true;
FileStatus status = myDocumentToStatusMap.get(document);
return status != null && !status.defensivelyMarked && status.wolfPassFinished && status.allDirtyScopesAreNull();
}
}
@TestOnly
public void assertAllDirtyScopesAreNull(@NotNull Document document) {
synchronized (myDocumentToStatusMap) {
FileStatus status = myDocumentToStatusMap.get(document);
assert status != null && !status.defensivelyMarked && status.wolfPassFinished && status.allDirtyScopesAreNull() : status;
}
}
@TestOnly
public void allowDirt(boolean allow) {
myAllowDirt = allow;
}
private static final RangeMarker WHOLE_FILE_DIRTY_MARKER = new RangeMarker(){
@NotNull
@Override
public Document getDocument() {
throw new UnsupportedOperationException();
}
@Override
public int getStartOffset() {
throw new UnsupportedOperationException();
}
@Override
public int getEndOffset() {
throw new UnsupportedOperationException();
}
@Override
public boolean isValid() {
return false;
}
@Override
public void setGreedyToLeft(boolean greedy) {
throw new UnsupportedOperationException();
}
@Override
public void setGreedyToRight(boolean greedy) {
throw new UnsupportedOperationException();
}
@Override
public boolean isGreedyToRight() {
throw new UnsupportedOperationException();
}
@Override
public boolean isGreedyToLeft() {
throw new UnsupportedOperationException();
}
@Override
public void dispose() {
// ignore
}
@Override
public <T> T getUserData(@NotNull Key<T> key) {
throw new UnsupportedOperationException();
}
@Override
public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) {
throw new UnsupportedOperationException();
}
};
}