/* * 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.BackgroundEditorHighlighter; import com.intellij.codeHighlighting.HighlightingPass; import com.intellij.codeHighlighting.Pass; import com.intellij.codeHighlighting.TextEditorHighlightingPass; import com.intellij.codeInsight.AutoPopupController; import com.intellij.codeInsight.daemon.*; import com.intellij.codeInsight.hint.HintManager; import com.intellij.codeInsight.intention.impl.FileLevelIntentionComponent; import com.intellij.codeInsight.intention.impl.IntentionHintComponent; import com.intellij.diagnostic.ThreadDumper; import com.intellij.ide.PowerSaveMode; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.application.ex.ApplicationEx; import com.intellij.openapi.application.ex.ApplicationManagerEx; import com.intellij.openapi.application.impl.ApplicationInfoImpl; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.components.StoragePathMacros; 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.editor.ex.RangeHighlighterEx; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx; import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader; import com.intellij.openapi.fileEditor.impl.text.TextEditorImpl; import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.vfs.newvfs.RefreshQueueImpl; import com.intellij.packageDependencies.DependencyValidationManager; import com.intellij.psi.*; import com.intellij.psi.impl.PsiDocumentManagerBase; import com.intellij.psi.search.scope.packageSet.NamedScopeManager; import com.intellij.psi.util.PsiModificationTracker; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.*; import com.intellij.util.concurrency.EdtExecutorService; import com.intellij.util.io.storage.HeavyProcessLatch; import com.intellij.util.ui.UIUtil; import gnu.trove.THashMap; import gnu.trove.THashSet; import org.jdom.Element; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; /** * This class also controls the auto-reparse and auto-hints. */ @State( name = "DaemonCodeAnalyzer", storages = @Storage(StoragePathMacros.WORKSPACE_FILE) ) public class DaemonCodeAnalyzerImpl extends DaemonCodeAnalyzerEx implements PersistentStateComponent<Element>, Disposable { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl"); private static final Key<List<HighlightInfo>> FILE_LEVEL_HIGHLIGHTS = Key.create("FILE_LEVEL_HIGHLIGHTS"); private final Project myProject; private final DaemonCodeAnalyzerSettings mySettings; @NotNull private final EditorTracker myEditorTracker; @NotNull private final PsiDocumentManager myPsiDocumentManager; private DaemonProgressIndicator myUpdateProgress = new DaemonProgressIndicator(); //guarded by this private final UpdateRunnable myUpdateRunnable; // use scheduler instead of Alarm because the latter requires ModalityState.current() which is obtainable from EDT only which requires too many invokeLaters private final ScheduledExecutorService myAlarm = EdtExecutorService.getScheduledExecutorInstance(); @NotNull private volatile Future<?> myUpdateRunnableFuture = CompletableFuture.completedFuture(null); private boolean myUpdateByTimerEnabled = true; private final Collection<VirtualFile> myDisabledHintsFiles = new THashSet<>(); private final Collection<VirtualFile> myDisabledHighlightingFiles = new THashSet<>(); private final FileStatusMap myFileStatusMap; private DaemonCodeAnalyzerSettings myLastSettings; private volatile IntentionHintComponent myLastIntentionHint; private volatile boolean myDisposed; // the only possible transition: false -> true private volatile boolean myInitialized; // the only possible transition: false -> true @NonNls private static final String DISABLE_HINTS_TAG = "disable_hints"; @NonNls private static final String FILE_TAG = "file"; @NonNls private static final String URL_ATT = "url"; private final PassExecutorService myPassExecutorService; public DaemonCodeAnalyzerImpl(@NotNull Project project, @NotNull DaemonCodeAnalyzerSettings daemonCodeAnalyzerSettings, @NotNull EditorTracker editorTracker, @NotNull PsiDocumentManager psiDocumentManager, @SuppressWarnings("UnusedParameters") @NotNull final NamedScopeManager namedScopeManager, @SuppressWarnings("UnusedParameters") @NotNull final DependencyValidationManager dependencyValidationManager) { myProject = project; mySettings = daemonCodeAnalyzerSettings; myEditorTracker = editorTracker; myPsiDocumentManager = psiDocumentManager; myLastSettings = ((DaemonCodeAnalyzerSettingsImpl)daemonCodeAnalyzerSettings).clone(); myFileStatusMap = new FileStatusMap(project); myPassExecutorService = new PassExecutorService(project); Disposer.register(this, myPassExecutorService); Disposer.register(this, myFileStatusMap); DaemonProgressIndicator.setDebug(LOG.isDebugEnabled()); assert !myInitialized : "Double Initializing"; Disposer.register(this, new StatusBarUpdater(project)); myInitialized = true; myDisposed = false; myFileStatusMap.markAllFilesDirty("DCAI init"); myUpdateRunnable = new UpdateRunnable(myProject); Disposer.register(this, () -> { assert myInitialized : "Disposing not initialized component"; assert !myDisposed : "Double dispose"; myUpdateRunnable.clearFieldsOnDispose(); stopProcess(false, "Dispose"); myDisposed = true; myLastSettings = null; }); } @Override public synchronized void dispose() { myUpdateProgress = new DaemonProgressIndicator(); // leak of highlight session via user data myUpdateRunnableFuture.cancel(true); } @NotNull @TestOnly public static List<HighlightInfo> getHighlights(@NotNull Document document, HighlightSeverity minSeverity, @NotNull Project project) { List<HighlightInfo> infos = new ArrayList<>(); processHighlights(document, project, minSeverity, 0, document.getTextLength(), Processors.cancelableCollectProcessor(infos)); return infos; } @Override @NotNull @TestOnly public List<HighlightInfo> getFileLevelHighlights(@NotNull Project project, @NotNull PsiFile file) { VirtualFile vFile = file.getViewProvider().getVirtualFile(); final FileEditorManager manager = FileEditorManager.getInstance(project); return Arrays.stream(manager.getEditors(vFile)) .map(fileEditor -> fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS)) .filter(Objects::nonNull) .flatMap(Collection::stream) .collect(Collectors.toList()); } @Override public void cleanFileLevelHighlights(@NotNull Project project, final int group, PsiFile psiFile) { if (psiFile == null) return; FileViewProvider provider = psiFile.getViewProvider(); VirtualFile vFile = provider.getVirtualFile(); final FileEditorManager manager = FileEditorManager.getInstance(project); for (FileEditor fileEditor : manager.getEditors(vFile)) { final List<HighlightInfo> infos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS); if (infos == null) continue; List<HighlightInfo> infosToRemove = new ArrayList<>(); for (HighlightInfo info : infos) { if (info.getGroup() == group) { manager.removeTopComponent(fileEditor, info.fileLevelComponent); infosToRemove.add(info); } } infos.removeAll(infosToRemove); } } @Override public void addFileLevelHighlight(@NotNull final Project project, final int group, @NotNull final HighlightInfo info, @NotNull final PsiFile psiFile) { VirtualFile vFile = psiFile.getViewProvider().getVirtualFile(); final FileEditorManager manager = FileEditorManager.getInstance(project); for (FileEditor fileEditor : manager.getEditors(vFile)) { if (fileEditor instanceof TextEditor) { FileLevelIntentionComponent component = new FileLevelIntentionComponent(info.getDescription(), info.getSeverity(), info.getGutterIconRenderer(), info.quickFixActionRanges, project, psiFile, ((TextEditor)fileEditor).getEditor(), info.getToolTip()); manager.addTopComponent(fileEditor, component); List<HighlightInfo> fileLevelInfos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS); if (fileLevelInfos == null) { fileLevelInfos = new ArrayList<>(); fileEditor.putUserData(FILE_LEVEL_HIGHLIGHTS, fileLevelInfos); } info.fileLevelComponent = component; info.setGroup(group); fileLevelInfos.add(info); } } } @Override @NotNull public List<HighlightInfo> runMainPasses(@NotNull PsiFile psiFile, @NotNull Document document, @NotNull final ProgressIndicator progress) { if (ApplicationManager.getApplication().isDispatchThread()) { throw new IllegalStateException("Must not run highlighting from under EDT"); } if (!ApplicationManager.getApplication().isReadAccessAllowed()) { throw new IllegalStateException("Must run highlighting from under read action"); } ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator(); if (!(indicator instanceof DaemonProgressIndicator)) { throw new IllegalStateException("Must run highlighting under progress with DaemonProgressIndicator"); } // clear status maps to run passes from scratch so that refCountHolder won't conflict and try to restart itself on partially filled maps myFileStatusMap.markAllFilesDirty("prepare to run main passes"); stopProcess(false, "disable background daemon"); myPassExecutorService.cancelAll(true); final List<HighlightInfo> result; try { result = new ArrayList<>(); final VirtualFile virtualFile = psiFile.getVirtualFile(); if (virtualFile != null && !virtualFile.getFileType().isBinary()) { List<TextEditorHighlightingPass> passes = TextEditorHighlightingPassRegistrarEx.getInstanceEx(myProject).instantiateMainPasses(psiFile, document, HighlightInfoProcessor.getEmpty()); Collections.sort(passes, (o1, o2) -> { if (o1 instanceof GeneralHighlightingPass) return -1; if (o2 instanceof GeneralHighlightingPass) return 1; return 0; }); try { for (TextEditorHighlightingPass pass : passes) { pass.doCollectInformation(progress); result.addAll(pass.getInfos()); } } catch (ProcessCanceledException e) { LOG.debug("Canceled: " + progress); throw e; } } } finally { stopProcess(true, "re-enable background daemon after main passes run"); } return result; } @NotNull @TestOnly public List<HighlightInfo> runPasses(@NotNull PsiFile file, @NotNull Document document, @NotNull TextEditor textEditor, @NotNull int[] toIgnore, boolean canChangeDocument, @Nullable Runnable callbackWhileWaiting) throws ProcessCanceledException { return runPasses(file, document, Collections.singletonList(textEditor), toIgnore, canChangeDocument, callbackWhileWaiting); } private volatile boolean mustWaitForSmartMode = true; @TestOnly public void mustWaitForSmartMode(final boolean mustWait, @NotNull Disposable parent) { final boolean old = mustWaitForSmartMode; mustWaitForSmartMode = mustWait; Disposer.register(parent, () -> mustWaitForSmartMode = old); } @NotNull @TestOnly List<HighlightInfo> runPasses(@NotNull PsiFile file, @NotNull Document document, @NotNull List<TextEditor> textEditors, @NotNull int[] toIgnore, boolean canChangeDocument, @Nullable final Runnable callbackWhileWaiting) throws ProcessCanceledException { assert myInitialized; assert !myDisposed; ApplicationEx application = ApplicationManagerEx.getApplicationEx(); application.assertIsDispatchThread(); if (application.isWriteAccessAllowed()) { throw new AssertionError("Must not start highlighting from within write action, or deadlock is imminent"); } DaemonProgressIndicator.setDebug(!ApplicationInfoImpl.isInStressTest()); ((FileTypeManagerImpl)FileTypeManager.getInstance()).drainReDetectQueue(); // pump first so that queued event do not interfere UIUtil.dispatchAllInvocationEvents(); // refresh will fire write actions interfering with highlighting while (RefreshQueueImpl.isRefreshInProgress() || HeavyProcessLatch.INSTANCE.isRunning()) { UIUtil.dispatchAllInvocationEvents(); } long dstart = System.currentTimeMillis(); while (mustWaitForSmartMode && DumbService.getInstance(myProject).isDumb()) { if (System.currentTimeMillis() > dstart + 100000) { throw new IllegalStateException("Timeout waiting for smart mode. If you absolutely want to be dumb, please use DaemonCodeAnalyzerImpl.mustWaitForSmartMode(false)."); } UIUtil.dispatchAllInvocationEvents(); } UIUtil.dispatchAllInvocationEvents(); Project project = file.getProject(); FileStatusMap fileStatusMap = getFileStatusMap(); fileStatusMap.allowDirt(canChangeDocument); Map<FileEditor, HighlightingPass[]> map = new HashMap<>(); for (TextEditor textEditor : textEditors) { if (textEditor instanceof TextEditorImpl) { try { ((TextEditorImpl)textEditor).waitForLoaded(10, TimeUnit.SECONDS); } catch (TimeoutException e) { throw new RuntimeException(textEditor + " has not completed loading in 10 seconds"); } } TextEditorBackgroundHighlighter highlighter = (TextEditorBackgroundHighlighter)textEditor.getBackgroundHighlighter(); if (highlighter == null) { Editor editor = textEditor.getEditor(); throw new RuntimeException("Null highlighter from " + textEditor + "; loaded: " + AsyncEditorLoader.isEditorLoaded(editor)); } final List<TextEditorHighlightingPass> passes = highlighter.getPasses(toIgnore); HighlightingPass[] array = passes.toArray(new HighlightingPass[passes.size()]); assert array.length != 0 : "Highlighting is disabled for the file " + file; map.put(textEditor, array); } for (int ignoreId : toIgnore) { fileStatusMap.markFileUpToDate(document, ignoreId); } myUpdateRunnableFuture.cancel(false); final DaemonProgressIndicator progress = createUpdateProgress(); myPassExecutorService.submitPasses(map, progress); try { long start = System.currentTimeMillis(); while (progress.isRunning() && System.currentTimeMillis() < start + 5*60*1000) { wrap(() -> { progress.checkCanceled(); if (callbackWhileWaiting != null) { callbackWhileWaiting.run(); } waitInOtherThread(50, canChangeDocument); UIUtil.dispatchAllInvocationEvents(); Throwable savedException = PassExecutorService.getSavedException(progress); if (savedException != null) throw savedException; }); } if (progress.isRunning() && !progress.isCanceled()) { throw new RuntimeException("Highlighting still running after "+(System.currentTimeMillis()-start)/1000+" seconds.\n"+ ThreadDumper.dumpThreadsToString()); } final HighlightingSessionImpl session = (HighlightingSessionImpl)HighlightingSessionImpl.getOrCreateHighlightingSession(file, textEditors.get(0).getEditor(), progress, null); wrap(() -> { if (!waitInOtherThread(60000, canChangeDocument)) { throw new TimeoutException("Unable to complete in 60s"); } session.waitForHighlightInfosApplied(); }); UIUtil.dispatchAllInvocationEvents(); UIUtil.dispatchAllInvocationEvents(); assert progress.isCanceled() && progress.isDisposed(); return getHighlights(document, null, project); } finally { DaemonProgressIndicator.setDebug(false); fileStatusMap.allowDirt(true); waitForTermination(); } } @TestOnly private boolean waitInOtherThread(int millis, boolean canChangeDocument) throws Throwable { Disposable disposable = Disposer.newDisposable(); // last hope protection against PsiModificationTrackerImpl.incCounter() craziness (yes, Kotlin) myProject.getMessageBus().connect(disposable).subscribe(PsiModificationTracker.TOPIC, () -> { throw new IllegalStateException("You must not perform PSI modifications from inside highlighting"); }); if (!canChangeDocument) { myProject.getMessageBus().connect(disposable).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC, new DaemonListenerAdapter() { @Override public void daemonCancelEventOccurred(@NotNull String reason) { throw new IllegalStateException("You must not cancel daemon inside highlighting test: "+reason); } }); } try { Future<Boolean> future = ApplicationManager.getApplication().executeOnPooledThread(() -> { try { return myPassExecutorService.waitFor(millis); } catch (Throwable e) { throw new RuntimeException(e); } }); return future.get(); } finally { Disposer.dispose(disposable); } } @TestOnly public void prepareForTest() { setUpdateByTimerEnabled(false); waitForTermination(); } @TestOnly public void cleanupAfterTest() { if (myProject.isOpen()) { prepareForTest(); } } @TestOnly void waitForTermination() { myPassExecutorService.cancelAll(true); } @Override public void settingsChanged() { DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance(); if (settings.isCodeHighlightingChanged(myLastSettings)) { restart(); } myLastSettings = ((DaemonCodeAnalyzerSettingsImpl)settings).clone(); } @Override public void updateVisibleHighlighters(@NotNull Editor editor) { ApplicationManager.getApplication().assertIsDispatchThread(); // no need, will not work anyway } @Override public void setUpdateByTimerEnabled(boolean value) { myUpdateByTimerEnabled = value; stopProcess(value, "Update by timer change"); } private int myDisableCount; @Override public void disableUpdateByTimer(@NotNull Disposable parentDisposable) { setUpdateByTimerEnabled(false); myDisableCount++; ApplicationManager.getApplication().assertIsDispatchThread(); Disposer.register(parentDisposable, () -> { myDisableCount--; if (myDisableCount == 0) { setUpdateByTimerEnabled(true); } }); } boolean isUpdateByTimerEnabled() { return myUpdateByTimerEnabled; } @Override public void setImportHintsEnabled(@NotNull PsiFile file, boolean value) { VirtualFile vFile = file.getVirtualFile(); if (value) { myDisabledHintsFiles.remove(vFile); stopProcess(true, "Import hints change"); } else { myDisabledHintsFiles.add(vFile); HintManager.getInstance().hideAllHints(); } } @Override public void resetImportHintsEnabledForProject() { myDisabledHintsFiles.clear(); } @Override public void setHighlightingEnabled(@NotNull PsiFile file, boolean value) { VirtualFile virtualFile = PsiUtilCore.getVirtualFile(file); if (value) { myDisabledHighlightingFiles.remove(virtualFile); } else { myDisabledHighlightingFiles.add(virtualFile); } } @Override public boolean isHighlightingAvailable(@Nullable PsiFile file) { if (file == null || !file.isPhysical()) return false; if (myDisabledHighlightingFiles.contains(PsiUtilCore.getVirtualFile(file))) return false; if (file instanceof PsiCompiledElement) return false; final FileType fileType = file.getFileType(); // To enable T.O.D.O. highlighting return !fileType.isBinary(); } @Override public boolean isImportHintsEnabled(@NotNull PsiFile file) { return isAutohintsAvailable(file) && !myDisabledHintsFiles.contains(file.getVirtualFile()); } @Override public boolean isAutohintsAvailable(PsiFile file) { return isHighlightingAvailable(file) && !(file instanceof PsiCompiledElement); } @Override public void restart() { doRestart(); } // return true if the progress was really canceled boolean doRestart() { myFileStatusMap.markAllFilesDirty("Global restart"); return stopProcess(true, "Global restart"); } @Override public void restart(@NotNull PsiFile file) { Document document = myPsiDocumentManager.getCachedDocument(file); if (document == null) return; String reason = "Psi file restart: " + file.getName(); myFileStatusMap.markFileScopeDirty(document, new TextRange(0, document.getTextLength()), file.getTextLength(), reason); stopProcess(true, reason); } @NotNull List<TextEditorHighlightingPass> getPassesToShowProgressFor(Document document) { List<TextEditorHighlightingPass> allPasses = myPassExecutorService.getAllSubmittedPasses(); List<TextEditorHighlightingPass> result = new ArrayList<>(allPasses.size()); for (TextEditorHighlightingPass pass : allPasses) { if (pass.getDocument() == document || pass.getDocument() == null) { result.add(pass); } } return result; } boolean isAllAnalysisFinished(@NotNull PsiFile file) { if (myDisposed) return false; Document document = myPsiDocumentManager.getCachedDocument(file); return document != null && document.getModificationStamp() == file.getViewProvider().getModificationStamp() && myFileStatusMap.allDirtyScopesAreNull(document); } @Override public boolean isErrorAnalyzingFinished(@NotNull PsiFile file) { if (myDisposed) return false; Document document = myPsiDocumentManager.getCachedDocument(file); return document != null && document.getModificationStamp() == file.getViewProvider().getModificationStamp() && myFileStatusMap.getFileDirtyScope(document, Pass.UPDATE_ALL) == null; } @Override @NotNull public FileStatusMap getFileStatusMap() { return myFileStatusMap; } synchronized boolean isRunning() { return !myUpdateProgress.isCanceled(); } @TestOnly public boolean isRunningOrPending() { ApplicationManager.getApplication().assertIsDispatchThread(); return isRunning() || !myUpdateRunnableFuture.isDone(); } // return true if the progress really was canceled synchronized boolean stopProcess(boolean toRestartAlarm, @NotNull @NonNls String reason) { boolean canceled = cancelUpdateProgress(toRestartAlarm, reason); // optimisation: this check is to avoid too many re-schedules in case of thousands of events spikes boolean restart = toRestartAlarm && !myDisposed && myInitialized; if (restart && myUpdateRunnableFuture.isDone()) { myUpdateRunnableFuture.cancel(false); myUpdateRunnableFuture = myAlarm.schedule(myUpdateRunnable, mySettings.AUTOREPARSE_DELAY, TimeUnit.MILLISECONDS); } return canceled; } // return true if the progress really was canceled private synchronized boolean cancelUpdateProgress(boolean toRestartAlarm, @NonNls String reason) { DaemonProgressIndicator updateProgress = myUpdateProgress; if (myDisposed) return false; boolean wasCanceled = updateProgress.isCanceled(); myPassExecutorService.cancelAll(false); if (!wasCanceled) { PassExecutorService.log(updateProgress, null, "Cancel", reason, toRestartAlarm); updateProgress.cancel(); return true; } return false; } static boolean processHighlightsNearOffset(@NotNull Document document, @NotNull Project project, @NotNull final HighlightSeverity minSeverity, final int offset, final boolean includeFixRange, @NotNull final Processor<HighlightInfo> processor) { return processHighlights(document, project, null, 0, document.getTextLength(), info -> { if (!isOffsetInsideHighlightInfo(offset, info, includeFixRange)) return true; int compare = info.getSeverity().compareTo(minSeverity); return compare < 0 || processor.process(info); }); } @Nullable public HighlightInfo findHighlightByOffset(@NotNull Document document, final int offset, final boolean includeFixRange) { return findHighlightByOffset(document, offset, includeFixRange, HighlightSeverity.INFORMATION); } @Nullable HighlightInfo findHighlightByOffset(@NotNull Document document, final int offset, final boolean includeFixRange, @NotNull HighlightSeverity minSeverity) { final List<HighlightInfo> foundInfoList = new SmartList<>(); processHighlightsNearOffset(document, myProject, minSeverity, offset, includeFixRange, info -> { if (info.getSeverity() == HighlightInfoType.ELEMENT_UNDER_CARET_SEVERITY) { return true; } if (!foundInfoList.isEmpty()) { HighlightInfo foundInfo = foundInfoList.get(0); int compare = foundInfo.getSeverity().compareTo(info.getSeverity()); if (compare < 0) { foundInfoList.clear(); } else if (compare > 0) { return true; } } foundInfoList.add(info); return true; }); if (foundInfoList.isEmpty()) return null; if (foundInfoList.size() == 1) return foundInfoList.get(0); return new HighlightInfoComposite(foundInfoList); } private static boolean isOffsetInsideHighlightInfo(int offset, @NotNull HighlightInfo info, boolean includeFixRange) { RangeHighlighterEx highlighter = info.getHighlighter(); if (highlighter == null || !highlighter.isValid()) return false; int startOffset = highlighter.getStartOffset(); int endOffset = highlighter.getEndOffset(); if (startOffset <= offset && offset <= endOffset) { return true; } if (!includeFixRange) return false; RangeMarker fixMarker = info.fixMarker; if (fixMarker != null) { // null means its range is the same as highlighter if (!fixMarker.isValid()) return false; startOffset = fixMarker.getStartOffset(); endOffset = fixMarker.getEndOffset(); return startOffset <= offset && offset <= endOffset; } return false; } @NotNull public static List<LineMarkerInfo> getLineMarkers(@NotNull Document document, @NotNull Project project) { ApplicationManager.getApplication().assertIsDispatchThread(); List<LineMarkerInfo> result = new ArrayList<>(); LineMarkersUtil.processLineMarkers(project, document, new TextRange(0, document.getTextLength()), -1, new CommonProcessors.CollectProcessor<>(result)); return result; } void setLastIntentionHint(@NotNull Project project, @NotNull PsiFile file, @NotNull Editor editor, @NotNull ShowIntentionsPass.IntentionsInfo intentions, boolean hasToRecreate) { if (!editor.getSettings().isShowIntentionBulb()) { return; } ApplicationManager.getApplication().assertIsDispatchThread(); hideLastIntentionHint(); if (editor.getCaretModel().getCaretCount() > 1) return; IntentionHintComponent hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, intentions, false); if (hasToRecreate) { hintComponent.recreate(); } myLastIntentionHint = hintComponent; } void hideLastIntentionHint() { ApplicationManager.getApplication().assertIsDispatchThread(); IntentionHintComponent hint = myLastIntentionHint; if (hint != null && hint.isVisible()) { hint.hide(); myLastIntentionHint = null; } } @Nullable public IntentionHintComponent getLastIntentionHint() { return myLastIntentionHint; } @Nullable @Override public Element getState() { Element state = new Element("state"); if (myDisabledHintsFiles.isEmpty()) { return state; } List<String> array = new SmartList<>(); for (VirtualFile file : myDisabledHintsFiles) { if (file.isValid()) { array.add(file.getUrl()); } } if (!array.isEmpty()) { Collections.sort(array); Element disableHintsElement = new Element(DISABLE_HINTS_TAG); state.addContent(disableHintsElement); for (String url : array) { disableHintsElement.addContent(new Element(FILE_TAG).setAttribute(URL_ATT, url)); } } return state; } @Override public void loadState(Element state) { myDisabledHintsFiles.clear(); Element element = state.getChild(DISABLE_HINTS_TAG); if (element != null) { for (Element e : element.getChildren(FILE_TAG)) { String url = e.getAttributeValue(URL_ATT); if (url != null) { VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url); if (file != null) { myDisabledHintsFiles.add(file); } } } } } private final Runnable submitPassesRunnable = new Runnable() { @Override public void run() { PassExecutorService.log(getUpdateProgress(), null, "Update Runnable. myUpdateByTimerEnabled:", myUpdateByTimerEnabled, " something disposed:", PowerSaveMode.isEnabled() || myDisposed || !myProject.isInitialized(), " activeEditors:", myProject.isDisposed() ? null : getSelectedEditors()); if (!myUpdateByTimerEnabled) return; if (myDisposed) return; ApplicationManager.getApplication().assertIsDispatchThread(); final Collection<FileEditor> activeEditors = getSelectedEditors(); if (activeEditors.isEmpty()) return; if (ApplicationManager.getApplication().isWriteAccessAllowed()) { // makes no sense to start from within write action, will cancel anyway // we'll restart when the write action finish return; } final PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)myPsiDocumentManager; if (documentManager.hasUncommitedDocuments()) { documentManager.cancelAndRunWhenAllCommitted("restart daemon when all committed", this); return; } if (RefResolveService.ENABLED && !RefResolveService.getInstance(myProject).isUpToDate() && RefResolveService.getInstance(myProject).getQueueSize() == 1) { return; // if the user have just typed in something, wait until the file is re-resolved // (or else it will blink like crazy since unused symbols calculation depends on resolve service) } Map<FileEditor, HighlightingPass[]> passes = new THashMap<>(activeEditors.size()); for (FileEditor fileEditor : activeEditors) { BackgroundEditorHighlighter highlighter = fileEditor.getBackgroundHighlighter(); if (highlighter != null) { HighlightingPass[] highlightingPasses = highlighter.createPassesForEditor(); passes.put(fileEditor, highlightingPasses); } } // cancel all after calling createPasses() since there are perverts {@link com.intellij.util.xml.ui.DomUIFactoryImpl} who are changing PSI there cancelUpdateProgress(true, "Cancel by alarm"); myUpdateRunnableFuture.cancel(false); DaemonProgressIndicator progress = createUpdateProgress(); myPassExecutorService.submitPasses(passes, progress); } }; // made this class static and fields cleareable to avoid leaks when this object stuck in invokeLater queue private static class UpdateRunnable implements Runnable { private Project myProject; private UpdateRunnable(@NotNull Project project) { myProject = project; } @Override public void run() { ApplicationManager.getApplication().assertIsDispatchThread(); Project project = myProject; DaemonCodeAnalyzerImpl daemonCodeAnalyzer; if (project == null || !project.isInitialized() || project.isDisposed() || PowerSaveMode.isEnabled() || (daemonCodeAnalyzer = (DaemonCodeAnalyzerImpl)DaemonCodeAnalyzer.getInstance(project)).myDisposed) { return; } // wait for heavy processing to stop, re-schedule daemon but not too soon if (HeavyProcessLatch.INSTANCE.isRunning()) { HeavyProcessLatch.INSTANCE.executeOutOfHeavyProcess(() -> daemonCodeAnalyzer.stopProcess(true, "re-scheduled to execute after heavy processing finished")); return; } Editor activeEditor = FileEditorManager.getInstance(project).getSelectedTextEditor(); if (activeEditor == null) { AutoPopupController.runTransactionWithEverythingCommitted(project, daemonCodeAnalyzer.submitPassesRunnable); } else { ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(project)).cancelAndRunWhenAllCommitted("start daemon when all committed", daemonCodeAnalyzer.submitPassesRunnable); } } private void clearFieldsOnDispose() { myProject = null; } } @NotNull private synchronized DaemonProgressIndicator createUpdateProgress() { DaemonProgressIndicator old = myUpdateProgress; if (!old.isCanceled()) { old.cancel(); } DaemonProgressIndicator progress = new DaemonProgressIndicator() { @Override public void stopIfRunning() { super.stopIfRunning(); myProject.getMessageBus().syncPublisher(DAEMON_EVENT_TOPIC).daemonFinished(); } }; progress.setModalityProgress(null); progress.start(); myUpdateProgress = progress; return progress; } @Override public void autoImportReferenceAtCursor(@NotNull Editor editor, @NotNull PsiFile file) { for (ReferenceImporter importer : Extensions.getExtensions(ReferenceImporter.EP_NAME)) { if (importer.autoImportReferenceAtCursor(editor, file)) break; } } @TestOnly @NotNull synchronized DaemonProgressIndicator getUpdateProgress() { return myUpdateProgress; } @NotNull private Collection<FileEditor> getSelectedEditors() { ApplicationManager.getApplication().assertIsDispatchThread(); // Editors in modal context List<Editor> editors = getActiveEditors(); Collection<FileEditor> activeTextEditors = new THashSet<>(editors.size()); for (Editor editor : editors) { if (editor.isDisposed()) continue; TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(editor); activeTextEditors.add(textEditor); } if (ApplicationManager.getApplication().getCurrentModalityState() != ModalityState.NON_MODAL) { return activeTextEditors; } // Editors in tabs. Collection<FileEditor> result = new THashSet<>(); Collection<VirtualFile> files = new THashSet<>(activeTextEditors.size()); final FileEditor[] tabEditors = FileEditorManager.getInstance(myProject).getSelectedEditors(); for (FileEditor tabEditor : tabEditors) { if (!tabEditor.isValid()) continue; VirtualFile file = ((FileEditorManagerEx)FileEditorManager.getInstance(myProject)).getFile(tabEditor); if (file != null) { files.add(file); } result.add(tabEditor); } // do not duplicate documents for (FileEditor fileEditor : activeTextEditors) { VirtualFile file = ((FileEditorManagerEx)FileEditorManager.getInstance(myProject)).getFile(fileEditor); if (file != null && files.contains(file)) continue; result.add(fileEditor); } return result; } @NotNull private List<Editor> getActiveEditors() { return myEditorTracker.getActiveEditors(); } @TestOnly private static void wrap(@NotNull ThrowableRunnable runnable) { try { runnable.run(); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new RuntimeException(e); } } }