/* * Copyright 2000-2016 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.openapi.util.text.StringUtil; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.util.ConcurrencyUtil; import com.intellij.util.containers.WeakHashMap; import gnu.trove.TIntObjectHashMap; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.util.Arrays; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class FileStatusMap implements Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.FileStatusMap"); public static final String CHANGES_NOT_ALLOWED_DURING_HIGHLIGHTING = "PSI/document/model changes are not allowed during highlighting"; private final Project myProject; private final Map<Document,FileStatus> myDocumentToStatusMap = new WeakHashMap<>(); // all dirty if absent private volatile boolean myAllowDirt = true; // Don't reduce visibility rules here because this class is used in Upsource as well. public FileStatusMap(@NotNull Project project) { myProject = project; } @Override public void dispose() { // clear dangling references to PsiFiles/Documents. SCR#10358 markAllFilesDirty("FileStatusMap dispose"); } @Nullable("null means the file is clean") // used in scala 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 Project project, @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; status = new FileStatus(project); myDocumentToStatusMap.put(document, status); } status.errorFound = errorFound; } } boolean wasErrorFound(@NotNull Document document) { synchronized(myDocumentToStatusMap) { FileStatus status = myDocumentToStatusMap.get(document); return status != null && status.errorFound; } } private static class FileStatus { private 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<>(); private boolean errorFound; private FileStatus(@NotNull Project project) { markWholeFileDirty(project); } private void markWholeFileDirty(@NotNull Project project) { setDirtyScope(Pass.UPDATE_ALL, WHOLE_FILE_DIRTY_MARKER); setDirtyScope(Pass.EXTERNAL_TOOLS, WHOLE_FILE_DIRTY_MARKER); setDirtyScope(Pass.LOCAL_INSPECTIONS, WHOLE_FILE_DIRTY_MARKER); setDirtyScope(Pass.LINE_MARKERS, WHOLE_FILE_DIRTY_MARKER); TextEditorHighlightingPassRegistrarEx registrar = (TextEditorHighlightingPassRegistrarEx) TextEditorHighlightingPassRegistrar.getInstance(project); for(DirtyScopeTrackingHighlightingPassFactory factory: registrar.getDirtyScopeTrackingFactories()) { setDirtyScope(factory.getPassId(), WHOLE_FILE_DIRTY_MARKER); } } private boolean allDirtyScopesAreNull() { for (Object o : dirtyScopes.getValues()) { if (o != null) return false; } return true; } private void combineScopesWith(@NotNull final TextRange scope, final int fileLength, @NotNull final Document document) { dirtyScopes.transformValues(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((passId, rangeMarker) -> { s.append(" pass: ").append(passId).append(" -> ").append(rangeMarker == WHOLE_FILE_DIRTY_MARKER ? "Whole file" : rangeMarker).append(";"); return true; }); s.append(")"); return s.toString(); } private void setDirtyScope(int passId, RangeMarker scope) { RangeMarker marker = dirtyScopes.get(passId); if (marker != scope) { if (marker != null) { marker.dispose(); } dirtyScopes.put(passId, scope); } } } void markAllFilesDirty(@NotNull @NonNls Object reason) { assertAllowModifications(); synchronized (myDocumentToStatusMap) { if (!myDocumentToStatusMap.isEmpty()) { log("Mark all dirty: ", reason); } myDocumentToStatusMap.clear(); } } private void assertAllowModifications() { try { assert myAllowDirt : CHANGES_NOT_ALLOWED_DURING_HIGHLIGHTING; } finally { myAllowDirt = true; //give next test a chance } } public void markFileUpToDate(@NotNull Document document, int passId) { synchronized(myDocumentToStatusMap){ FileStatus status = myDocumentToStatusMap.computeIfAbsent(document, __ -> new FileStatus(myProject)); status.defensivelyMarked=false; if (passId == Pass.WOLF) { status.wolfPassFinished = true; } else if (status.dirtyScopes.containsKey(passId)) { status.setDirtyScope(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; } if (!status.dirtyScopes.containsKey(passId)) throw new IllegalStateException("Unknown pass " + passId); RangeMarker marker = status.dirtyScopes.get(passId); return marker == null ? null : marker.isValid() ? TextRange.create(marker) : new TextRange(0, document.getTextLength()); } } void markFileScopeDirtyDefensively(@NotNull PsiFile file, @NotNull @NonNls Object reason) { assertAllowModifications(); log("Mark dirty file defensively: ",file.getName(),reason); // 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; } } void markFileScopeDirty(@NotNull Document document, @NotNull TextRange scope, int fileLength, @NotNull @NonNls Object reason) { assertAllowModifications(); log("Mark scope dirty: ",scope,reason); 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); } 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 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) { return null; } @Override public <T> void putUserData(@NotNull Key<T> key, @Nullable T value) { throw new UnsupportedOperationException(); } @Override public String toString() { return "WHOLE_FILE"; } }; // logging private static final ConcurrentMap<Thread, Integer> threads = new ConcurrentHashMap<>(); private static int getThreadNum() { return ConcurrencyUtil.cacheOrGet(threads, Thread.currentThread(), threads.size()); } public static void log(@NonNls @NotNull Object... info) { if (LOG.isDebugEnabled()) { String s = StringUtil.repeatSymbol(' ', getThreadNum() * 4) + Arrays.asList(info) + "\n"; LOG.debug(s); } } }