optimisation: in case of thousands of events do not call expensive Future.schedule...
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / daemon / impl / DaemonCodeAnalyzerImpl.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.codeInsight.daemon.impl;
18
19 import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
20 import com.intellij.codeHighlighting.HighlightingPass;
21 import com.intellij.codeHighlighting.Pass;
22 import com.intellij.codeHighlighting.TextEditorHighlightingPass;
23 import com.intellij.codeInsight.AutoPopupController;
24 import com.intellij.codeInsight.daemon.*;
25 import com.intellij.codeInsight.hint.HintManager;
26 import com.intellij.codeInsight.intention.impl.FileLevelIntentionComponent;
27 import com.intellij.codeInsight.intention.impl.IntentionHintComponent;
28 import com.intellij.diagnostic.ThreadDumper;
29 import com.intellij.ide.PowerSaveMode;
30 import com.intellij.lang.annotation.HighlightSeverity;
31 import com.intellij.openapi.Disposable;
32 import com.intellij.openapi.application.ApplicationManager;
33 import com.intellij.openapi.application.ModalityState;
34 import com.intellij.openapi.application.ex.ApplicationEx;
35 import com.intellij.openapi.application.ex.ApplicationManagerEx;
36 import com.intellij.openapi.application.impl.ApplicationInfoImpl;
37 import com.intellij.openapi.components.PersistentStateComponent;
38 import com.intellij.openapi.components.State;
39 import com.intellij.openapi.components.Storage;
40 import com.intellij.openapi.components.StoragePathMacros;
41 import com.intellij.openapi.diagnostic.Logger;
42 import com.intellij.openapi.editor.Document;
43 import com.intellij.openapi.editor.Editor;
44 import com.intellij.openapi.editor.RangeMarker;
45 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
46 import com.intellij.openapi.extensions.Extensions;
47 import com.intellij.openapi.fileEditor.FileEditor;
48 import com.intellij.openapi.fileEditor.FileEditorManager;
49 import com.intellij.openapi.fileEditor.TextEditor;
50 import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
51 import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader;
52 import com.intellij.openapi.fileEditor.impl.text.TextEditorImpl;
53 import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
54 import com.intellij.openapi.fileTypes.FileType;
55 import com.intellij.openapi.fileTypes.FileTypeManager;
56 import com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl;
57 import com.intellij.openapi.progress.ProcessCanceledException;
58 import com.intellij.openapi.progress.ProgressIndicator;
59 import com.intellij.openapi.progress.ProgressManager;
60 import com.intellij.openapi.project.DumbService;
61 import com.intellij.openapi.project.Project;
62 import com.intellij.openapi.util.Disposer;
63 import com.intellij.openapi.util.EmptyRunnable;
64 import com.intellij.openapi.util.Key;
65 import com.intellij.openapi.util.TextRange;
66 import com.intellij.openapi.vfs.VirtualFile;
67 import com.intellij.openapi.vfs.VirtualFileManager;
68 import com.intellij.openapi.vfs.newvfs.RefreshQueueImpl;
69 import com.intellij.packageDependencies.DependencyValidationManager;
70 import com.intellij.psi.*;
71 import com.intellij.psi.impl.PsiDocumentManagerBase;
72 import com.intellij.psi.search.scope.packageSet.NamedScopeManager;
73 import com.intellij.psi.util.PsiModificationTracker;
74 import com.intellij.psi.util.PsiUtilCore;
75 import com.intellij.util.*;
76 import com.intellij.util.concurrency.EdtExecutorService;
77 import com.intellij.util.io.storage.HeavyProcessLatch;
78 import com.intellij.util.ui.UIUtil;
79 import gnu.trove.THashMap;
80 import gnu.trove.THashSet;
81 import org.jdom.Element;
82 import org.jetbrains.annotations.NonNls;
83 import org.jetbrains.annotations.NotNull;
84 import org.jetbrains.annotations.Nullable;
85 import org.jetbrains.annotations.TestOnly;
86
87 import java.util.*;
88 import java.util.concurrent.*;
89
90 /**
91  * This class also controls the auto-reparse and auto-hints.
92  */
93 @State(
94   name = "DaemonCodeAnalyzer",
95   storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
96 )
97 public class DaemonCodeAnalyzerImpl extends DaemonCodeAnalyzerEx implements PersistentStateComponent<Element>, Disposable {
98   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl");
99
100   private static final Key<List<HighlightInfo>> FILE_LEVEL_HIGHLIGHTS = Key.create("FILE_LEVEL_HIGHLIGHTS");
101   private final Project myProject;
102   private final DaemonCodeAnalyzerSettings mySettings;
103   @NotNull private final EditorTracker myEditorTracker;
104   @NotNull private final PsiDocumentManager myPsiDocumentManager;
105   private DaemonProgressIndicator myUpdateProgress = new DaemonProgressIndicator(); //guarded by this
106
107   private final Runnable myUpdateRunnable = createUpdateRunnable();
108
109   // use scheduler instead of Alarm because the latter requires ModalityState.current() which is obtainable from EDT only which requires too many invokeLaters
110   private final ScheduledExecutorService myAlarm = EdtExecutorService.getScheduledExecutorInstance();
111   @NotNull
112   private volatile ScheduledFuture<?> myUpdateRunnableFuture = myAlarm.schedule(EmptyRunnable.getInstance(), 0, TimeUnit.NANOSECONDS);
113   private boolean myUpdateByTimerEnabled = true;
114   private final Collection<VirtualFile> myDisabledHintsFiles = new THashSet<>();
115   private final Collection<VirtualFile> myDisabledHighlightingFiles = new THashSet<>();
116
117   private final FileStatusMap myFileStatusMap;
118   private DaemonCodeAnalyzerSettings myLastSettings;
119
120   private volatile IntentionHintComponent myLastIntentionHint;
121   private volatile boolean myDisposed;     // the only possible transition: false -> true
122   private volatile boolean myInitialized;  // the only possible transition: false -> true
123
124   @NonNls private static final String DISABLE_HINTS_TAG = "disable_hints";
125   @NonNls private static final String FILE_TAG = "file";
126   @NonNls private static final String URL_ATT = "url";
127   private final PassExecutorService myPassExecutorService;
128
129   public DaemonCodeAnalyzerImpl(@NotNull Project project,
130                                 @NotNull DaemonCodeAnalyzerSettings daemonCodeAnalyzerSettings,
131                                 @NotNull EditorTracker editorTracker,
132                                 @NotNull PsiDocumentManager psiDocumentManager,
133                                 @SuppressWarnings("UnusedParameters") @NotNull final NamedScopeManager namedScopeManager,
134                                 @SuppressWarnings("UnusedParameters") @NotNull final DependencyValidationManager dependencyValidationManager) {
135     myProject = project;
136     mySettings = daemonCodeAnalyzerSettings;
137     myEditorTracker = editorTracker;
138     myPsiDocumentManager = psiDocumentManager;
139     myLastSettings = ((DaemonCodeAnalyzerSettingsImpl)daemonCodeAnalyzerSettings).clone();
140
141     myFileStatusMap = new FileStatusMap(project);
142     myPassExecutorService = new PassExecutorService(project);
143     Disposer.register(this, myPassExecutorService);
144     Disposer.register(this, myFileStatusMap);
145     DaemonProgressIndicator.setDebug(LOG.isDebugEnabled());
146
147     assert !myInitialized : "Double Initializing";
148     Disposer.register(this, new StatusBarUpdater(project));
149
150     myInitialized = true;
151     myDisposed = false;
152     myFileStatusMap.markAllFilesDirty("DCAI init");
153     Disposer.register(this, () -> {
154       assert myInitialized : "Disposing not initialized component";
155       assert !myDisposed : "Double dispose";
156
157       stopProcess(false, "Dispose");
158
159       myDisposed = true;
160       myLastSettings = null;
161     });
162   }
163
164   @Override
165   public synchronized void dispose() {
166     myUpdateProgress = null; // leak of highlight session via user data
167     myUpdateRunnableFuture.cancel(true);
168   }
169
170   @NotNull
171   @TestOnly
172   public static List<HighlightInfo> getHighlights(@NotNull Document document, HighlightSeverity minSeverity, @NotNull Project project) {
173     List<HighlightInfo> infos = new ArrayList<>();
174     processHighlights(document, project, minSeverity, 0, document.getTextLength(), Processors.cancelableCollectProcessor(infos));
175     return infos;
176   }
177
178   @Override
179   @NotNull
180   @TestOnly
181   public List<HighlightInfo> getFileLevelHighlights(@NotNull Project project, @NotNull PsiFile file) {
182     VirtualFile vFile = file.getViewProvider().getVirtualFile();
183     final FileEditorManager manager = FileEditorManager.getInstance(project);
184     List<HighlightInfo> result = new ArrayList<>();
185     for (FileEditor fileEditor : manager.getEditors(vFile)) {
186       final List<HighlightInfo> infos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS);
187       if (infos == null) continue;
188       for (HighlightInfo info : infos) {
189           result.add(info);
190       }
191     }
192     return result;
193   }
194
195   @Override
196   public void cleanFileLevelHighlights(@NotNull Project project, final int group, PsiFile psiFile) {
197     if (psiFile == null) return;
198     FileViewProvider provider = psiFile.getViewProvider();
199     VirtualFile vFile = provider.getVirtualFile();
200     final FileEditorManager manager = FileEditorManager.getInstance(project);
201     for (FileEditor fileEditor : manager.getEditors(vFile)) {
202       final List<HighlightInfo> infos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS);
203       if (infos == null) continue;
204       List<HighlightInfo> infosToRemove = new ArrayList<>();
205       for (HighlightInfo info : infos) {
206         if (info.getGroup() == group) {
207           manager.removeTopComponent(fileEditor, info.fileLevelComponent);
208           infosToRemove.add(info);
209         }
210       }
211       infos.removeAll(infosToRemove);
212     }
213   }
214
215   @Override
216   public void addFileLevelHighlight(@NotNull final Project project,
217                                     final int group,
218                                     @NotNull final HighlightInfo info,
219                                     @NotNull final PsiFile psiFile) {
220     VirtualFile vFile = psiFile.getViewProvider().getVirtualFile();
221     final FileEditorManager manager = FileEditorManager.getInstance(project);
222     for (FileEditor fileEditor : manager.getEditors(vFile)) {
223       if (fileEditor instanceof TextEditor) {
224         FileLevelIntentionComponent component = new FileLevelIntentionComponent(info.getDescription(), info.getSeverity(),
225                                                                                 info.getGutterIconRenderer(), info.quickFixActionRanges,
226                                                                                 project, psiFile, ((TextEditor)fileEditor).getEditor());
227         manager.addTopComponent(fileEditor, component);
228         List<HighlightInfo> fileLevelInfos = fileEditor.getUserData(FILE_LEVEL_HIGHLIGHTS);
229         if (fileLevelInfos == null) {
230           fileLevelInfos = new ArrayList<>();
231           fileEditor.putUserData(FILE_LEVEL_HIGHLIGHTS, fileLevelInfos);
232         }
233         info.fileLevelComponent = component;
234         info.setGroup(group);
235         fileLevelInfos.add(info);
236       }
237     }
238   }
239
240   @Override
241   @NotNull
242   public List<HighlightInfo> runMainPasses(@NotNull PsiFile psiFile,
243                                            @NotNull Document document,
244                                            @NotNull final ProgressIndicator progress) {
245     if (ApplicationManager.getApplication().isDispatchThread()) {
246       throw new IllegalStateException("Must not run highlighting from under EDT");
247     }
248     if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
249       throw new IllegalStateException("Must run highlighting from under read action");
250     }
251     ProgressIndicator indicator = ProgressManager.getInstance().getProgressIndicator();
252     if (!(indicator instanceof DaemonProgressIndicator)) {
253       throw new IllegalStateException("Must run highlighting under progress with DaemonProgressIndicator");
254     }
255     // clear status maps to run passes from scratch so that refCountHolder won't conflict and try to restart itself on partially filled maps
256     myFileStatusMap.markAllFilesDirty("prepare to run main passes");
257     stopProcess(false, "disable background daemon");
258     myPassExecutorService.cancelAll(true);
259
260     final List<HighlightInfo> result;
261     try {
262       result = new ArrayList<>();
263       final VirtualFile virtualFile = psiFile.getVirtualFile();
264       if (virtualFile != null && !virtualFile.getFileType().isBinary()) {
265         List<TextEditorHighlightingPass> passes =
266           TextEditorHighlightingPassRegistrarEx.getInstanceEx(myProject).instantiateMainPasses(psiFile, document,
267                                                                                                HighlightInfoProcessor.getEmpty());
268
269         Collections.sort(passes, (o1, o2) -> {
270           if (o1 instanceof GeneralHighlightingPass) return -1;
271           if (o2 instanceof GeneralHighlightingPass) return 1;
272           return 0;
273         });
274
275         try {
276           for (TextEditorHighlightingPass pass : passes) {
277             pass.doCollectInformation(progress);
278             result.addAll(pass.getInfos());
279           }
280         }
281         catch (ProcessCanceledException e) {
282           LOG.debug("Canceled: " + progress);
283           throw e;
284         }
285       }
286     }
287     finally {
288       stopProcess(true, "re-enable background daemon after main passes run");
289     }
290
291     return result;
292   }
293
294   @NotNull
295   @TestOnly
296   public List<HighlightInfo> runPasses(@NotNull PsiFile file,
297                                        @NotNull Document document,
298                                        @NotNull TextEditor textEditor,
299                                        @NotNull int[] toIgnore,
300                                        boolean canChangeDocument,
301                                        @Nullable Runnable callbackWhileWaiting) throws ProcessCanceledException {
302     return runPasses(file, document, Collections.singletonList(textEditor), toIgnore, canChangeDocument, callbackWhileWaiting);
303   }
304
305   private volatile boolean mustWaitForSmartMode = true;
306   @TestOnly
307   public void mustWaitForSmartMode(final boolean mustWait, @NotNull Disposable parent) {
308     final boolean old = mustWaitForSmartMode;
309     mustWaitForSmartMode = mustWait;
310     Disposer.register(parent, () -> mustWaitForSmartMode = old);
311   }
312
313   @NotNull
314   @TestOnly
315   List<HighlightInfo> runPasses(@NotNull PsiFile file,
316                                 @NotNull Document document,
317                                 @NotNull List<TextEditor> textEditors,
318                                 @NotNull int[] toIgnore,
319                                 boolean canChangeDocument,
320                                 @Nullable final Runnable callbackWhileWaiting) throws ProcessCanceledException {
321     assert myInitialized;
322     assert !myDisposed;
323     ApplicationEx application = ApplicationManagerEx.getApplicationEx();
324     application.assertIsDispatchThread();
325     if (application.isWriteAccessAllowed()) {
326       throw new AssertionError("Must not start highlighting from within write action, or deadlock is imminent");
327     }
328     DaemonProgressIndicator.setDebug(!ApplicationInfoImpl.isInPerformanceTest());
329     ((FileTypeManagerImpl)FileTypeManager.getInstance()).drainReDetectQueue();
330     // pump first so that queued event do not interfere
331     UIUtil.dispatchAllInvocationEvents();
332
333     // refresh will fire write actions interfering with highlighting
334     while (RefreshQueueImpl.isRefreshInProgress() || HeavyProcessLatch.INSTANCE.isRunning()) {
335       UIUtil.dispatchAllInvocationEvents();
336     }
337     long dstart = System.currentTimeMillis();
338     while (mustWaitForSmartMode && DumbService.getInstance(myProject).isDumb()) {
339       if (System.currentTimeMillis() > dstart + 100000) {
340         throw new IllegalStateException("Timeout waiting for smart mode. If you absolutely want to be dumb, please use DaemonCodeAnalyzerImpl.mustWaitForSmartMode(false).");
341       }
342       UIUtil.dispatchAllInvocationEvents();
343     }
344
345     UIUtil.dispatchAllInvocationEvents();
346
347     Project project = file.getProject();
348     FileStatusMap.getAndClearLog();
349     FileStatusMap fileStatusMap = getFileStatusMap();
350     fileStatusMap.allowDirt(canChangeDocument);
351
352     Map<FileEditor, HighlightingPass[]> map = new HashMap<>();
353     for (TextEditor textEditor : textEditors) {
354       if (textEditor instanceof TextEditorImpl) {
355         try {
356           ((TextEditorImpl)textEditor).waitForLoaded(10, TimeUnit.SECONDS);
357         }
358         catch (TimeoutException e) {
359           throw new RuntimeException(textEditor + " has not completed loading in 10 seconds");
360         }
361       }
362       TextEditorBackgroundHighlighter highlighter = (TextEditorBackgroundHighlighter)textEditor.getBackgroundHighlighter();
363       if (highlighter == null) {
364         Editor editor = textEditor.getEditor();
365         throw new RuntimeException("Null highlighter from " + textEditor + "; loaded: " + AsyncEditorLoader.isEditorLoaded(editor));
366       }
367       final List<TextEditorHighlightingPass> passes = highlighter.getPasses(toIgnore);
368       HighlightingPass[] array = passes.toArray(new HighlightingPass[passes.size()]);
369       assert array.length != 0 : "Highlighting is disabled for the file " + file;
370       map.put(textEditor, array);
371     }
372     for (int ignoreId : toIgnore) {
373       fileStatusMap.markFileUpToDate(document, ignoreId);
374     }
375
376     myUpdateRunnableFuture.cancel(false);
377
378     final DaemonProgressIndicator progress = createUpdateProgress();
379     myPassExecutorService.submitPasses(map, progress);
380     try {
381       long start = System.currentTimeMillis();
382       while (progress.isRunning() && System.currentTimeMillis() < start + 5*60*1000) {
383         wrap(() -> {
384           progress.checkCanceled();
385           if (callbackWhileWaiting != null) {
386             callbackWhileWaiting.run();
387           }
388           waitInOtherThread(50, canChangeDocument);
389           UIUtil.dispatchAllInvocationEvents();
390           Throwable savedException = PassExecutorService.getSavedException(progress);
391           if (savedException != null) throw savedException;
392         });
393       }
394       if (progress.isRunning() && !progress.isCanceled()) {
395         throw new RuntimeException("Highlighting still running after "+(System.currentTimeMillis()-start)/1000+" seconds.\n"+ ThreadDumper.dumpThreadsToString());
396       }
397
398       final HighlightingSessionImpl session =
399         (HighlightingSessionImpl)HighlightingSessionImpl.getOrCreateHighlightingSession(file, textEditors.get(0).getEditor(), progress, null);
400       wrap(() -> {
401         if (!waitInOtherThread(60000, canChangeDocument)) {
402           throw new TimeoutException("Unable to complete in 60s");
403         }
404         session.waitForHighlightInfosApplied();
405       });
406       UIUtil.dispatchAllInvocationEvents();
407       UIUtil.dispatchAllInvocationEvents();
408       assert progress.isCanceled() && progress.isDisposed();
409
410       return getHighlights(document, null, project);
411     }
412     finally {
413       DaemonProgressIndicator.setDebug(false);
414       String log = FileStatusMap.getAndClearLog();
415       fileStatusMap.allowDirt(true);
416       try {
417         waitForTermination();
418       }
419       catch (Throwable e) {
420         LOG.error(log, e);
421       }
422     }
423   }
424
425   @TestOnly
426   private boolean waitInOtherThread(int millis, boolean canChangeDocument) throws Throwable {
427     Disposable disposable = Disposer.newDisposable();
428     // last hope protection against PsiModificationTrackerImpl.incCounter() craziness (yes, Kotlin)
429     myProject.getMessageBus().connect(disposable).subscribe(PsiModificationTracker.TOPIC,
430       () -> {
431         throw new IllegalStateException("You must not perform PSI modifications from inside highlighting");
432       });
433     if (!canChangeDocument) {
434       myProject.getMessageBus().connect(disposable).subscribe(DaemonCodeAnalyzer.DAEMON_EVENT_TOPIC, new DaemonListenerAdapter() {
435         @Override
436         public void daemonCancelEventOccurred(@NotNull String reason) {
437           throw new IllegalStateException("You must not cancel daemon inside highlighting test: "+reason);
438         }
439       });
440     }
441
442     try {
443       Future<Boolean> future = ApplicationManager.getApplication().executeOnPooledThread(() -> {
444         try {
445           return myPassExecutorService.waitFor(millis);
446         }
447         catch (Throwable e) {
448           throw new RuntimeException(e);
449         }
450       });
451       return future.get();
452     }
453     finally {
454       Disposer.dispose(disposable);
455     }
456   }
457
458   @TestOnly
459   public void prepareForTest() {
460     setUpdateByTimerEnabled(false);
461     waitForTermination();
462   }
463
464   @TestOnly
465   public void cleanupAfterTest() {
466     if (myProject.isOpen()) {
467       prepareForTest();
468     }
469   }
470
471   @TestOnly
472   void waitForTermination() {
473     myPassExecutorService.cancelAll(true);
474   }
475
476   @Override
477   public void settingsChanged() {
478     DaemonCodeAnalyzerSettings settings = DaemonCodeAnalyzerSettings.getInstance();
479     if (settings.isCodeHighlightingChanged(myLastSettings)) {
480       restart();
481     }
482     myLastSettings = ((DaemonCodeAnalyzerSettingsImpl)settings).clone();
483   }
484
485   @Override
486   public void updateVisibleHighlighters(@NotNull Editor editor) {
487     ApplicationManager.getApplication().assertIsDispatchThread();
488     // no need, will not work anyway
489   }
490
491   @Override
492   public void setUpdateByTimerEnabled(boolean value) {
493     myUpdateByTimerEnabled = value;
494     stopProcess(value, "Update by timer change");
495   }
496
497   private int myDisableCount;
498
499   @Override
500   public void disableUpdateByTimer(@NotNull Disposable parentDisposable) {
501     setUpdateByTimerEnabled(false);
502     myDisableCount++;
503     ApplicationManager.getApplication().assertIsDispatchThread();
504
505     Disposer.register(parentDisposable, () -> {
506       myDisableCount--;
507       if (myDisableCount == 0) {
508         setUpdateByTimerEnabled(true);
509       }
510     });
511   }
512
513   boolean isUpdateByTimerEnabled() {
514     return myUpdateByTimerEnabled;
515   }
516
517   @Override
518   public void setImportHintsEnabled(@NotNull PsiFile file, boolean value) {
519     VirtualFile vFile = file.getVirtualFile();
520     if (value) {
521       myDisabledHintsFiles.remove(vFile);
522       stopProcess(true, "Import hints change");
523     }
524     else {
525       myDisabledHintsFiles.add(vFile);
526       HintManager.getInstance().hideAllHints();
527     }
528   }
529
530   @Override
531   public void resetImportHintsEnabledForProject() {
532     myDisabledHintsFiles.clear();
533   }
534
535   @Override
536   public void setHighlightingEnabled(@NotNull PsiFile file, boolean value) {
537     VirtualFile virtualFile = PsiUtilCore.getVirtualFile(file);
538     if (value) {
539       myDisabledHighlightingFiles.remove(virtualFile);
540     }
541     else {
542       myDisabledHighlightingFiles.add(virtualFile);
543     }
544   }
545
546   @Override
547   public boolean isHighlightingAvailable(@Nullable PsiFile file) {
548     if (file == null || !file.isPhysical()) return false;
549     if (myDisabledHighlightingFiles.contains(PsiUtilCore.getVirtualFile(file))) return false;
550
551     if (file instanceof PsiCompiledElement) return false;
552     final FileType fileType = file.getFileType();
553
554     // To enable T.O.D.O. highlighting
555     return !fileType.isBinary();
556   }
557
558   @Override
559   public boolean isImportHintsEnabled(@NotNull PsiFile file) {
560     return isAutohintsAvailable(file) && !myDisabledHintsFiles.contains(file.getVirtualFile());
561   }
562
563   @Override
564   public boolean isAutohintsAvailable(PsiFile file) {
565     return isHighlightingAvailable(file) && !(file instanceof PsiCompiledElement);
566   }
567
568   @Override
569   public void restart() {
570     doRestart();
571   }
572
573   // return true if the progress was really canceled
574   boolean doRestart() {
575     myFileStatusMap.markAllFilesDirty("Global restart");
576     return stopProcess(true, "Global restart");
577   }
578
579   @Override
580   public void restart(@NotNull PsiFile file) {
581     Document document = myPsiDocumentManager.getCachedDocument(file);
582     if (document == null) return;
583     String reason = "Psi file restart: " + file.getName();
584     myFileStatusMap.markFileScopeDirty(document, new TextRange(0, document.getTextLength()), file.getTextLength(), reason);
585     stopProcess(true, reason);
586   }
587
588   @NotNull
589   List<TextEditorHighlightingPass> getPassesToShowProgressFor(Document document) {
590     List<TextEditorHighlightingPass> allPasses = myPassExecutorService.getAllSubmittedPasses();
591     List<TextEditorHighlightingPass> result = new ArrayList<>(allPasses.size());
592     for (TextEditorHighlightingPass pass : allPasses) {
593       if (pass.getDocument() == document || pass.getDocument() == null) {
594         result.add(pass);
595       }
596     }
597     return result;
598   }
599
600   boolean isAllAnalysisFinished(@NotNull PsiFile file) {
601     if (myDisposed) return false;
602     Document document = myPsiDocumentManager.getCachedDocument(file);
603     return document != null &&
604            document.getModificationStamp() == file.getViewProvider().getModificationStamp() &&
605            myFileStatusMap.allDirtyScopesAreNull(document);
606   }
607
608   @Override
609   public boolean isErrorAnalyzingFinished(@NotNull PsiFile file) {
610     if (myDisposed) return false;
611     Document document = myPsiDocumentManager.getCachedDocument(file);
612     return document != null &&
613            document.getModificationStamp() == file.getViewProvider().getModificationStamp() &&
614            myFileStatusMap.getFileDirtyScope(document, Pass.UPDATE_ALL) == null;
615   }
616
617   @Override
618   @NotNull
619   public FileStatusMap getFileStatusMap() {
620     return myFileStatusMap;
621   }
622
623   synchronized boolean isRunning() {
624     return !myUpdateProgress.isCanceled();
625   }
626   
627   @TestOnly
628   public boolean isRunningOrPending() {
629     ApplicationManager.getApplication().assertIsDispatchThread();
630     return isRunning() || !myUpdateRunnableFuture.isDone();
631   }
632
633   // return true if the progress really was canceled
634   synchronized boolean stopProcess(boolean toRestartAlarm, @NotNull @NonNls String reason) {
635     boolean canceled = cancelUpdateProgress(toRestartAlarm, reason);
636     // optimisation: this check is to avoid too many re-schedules in case of thousands of events spikes
637     boolean restart = toRestartAlarm && !myDisposed && myInitialized;
638     if (restart && myUpdateRunnableFuture.isDone()) {
639       myUpdateRunnableFuture.cancel(false);
640       myUpdateRunnableFuture = myAlarm.schedule(myUpdateRunnable, mySettings.AUTOREPARSE_DELAY, TimeUnit.MILLISECONDS);
641     }
642
643     return canceled;
644   }
645
646   // return true if the progress really was canceled
647   private synchronized boolean cancelUpdateProgress(boolean toRestartAlarm, @NonNls String reason) {
648     boolean wasCanceled = myUpdateProgress.isCanceled();
649     myPassExecutorService.cancelAll(false);
650     if (!wasCanceled) {
651       PassExecutorService.log(myUpdateProgress, null, "Cancel", reason, toRestartAlarm);
652       myUpdateProgress.cancel();
653       return true;
654     }
655     return false;
656   }
657
658
659   static boolean processHighlightsNearOffset(@NotNull Document document,
660                                              @NotNull Project project,
661                                              @NotNull final HighlightSeverity minSeverity,
662                                              final int offset,
663                                              final boolean includeFixRange,
664                                              @NotNull final Processor<HighlightInfo> processor) {
665     return processHighlights(document, project, null, 0, document.getTextLength(), info -> {
666       if (!isOffsetInsideHighlightInfo(offset, info, includeFixRange)) return true;
667
668       int compare = info.getSeverity().compareTo(minSeverity);
669       return compare < 0 || processor.process(info);
670     });
671   }
672
673   @Nullable
674   public HighlightInfo findHighlightByOffset(@NotNull Document document, final int offset, final boolean includeFixRange) {
675     return findHighlightByOffset(document, offset, includeFixRange, HighlightSeverity.INFORMATION);
676   }
677
678   @Nullable
679   HighlightInfo findHighlightByOffset(@NotNull Document document,
680                                       final int offset,
681                                       final boolean includeFixRange,
682                                       @NotNull HighlightSeverity minSeverity) {
683     final List<HighlightInfo> foundInfoList = new SmartList<>();
684     processHighlightsNearOffset(document, myProject, minSeverity, offset, includeFixRange,
685         info -> {
686           if (info.getSeverity() == HighlightInfoType.ELEMENT_UNDER_CARET_SEVERITY) {
687             return true;
688           }
689           if (!foundInfoList.isEmpty()) {
690             HighlightInfo foundInfo = foundInfoList.get(0);
691             int compare = foundInfo.getSeverity().compareTo(info.getSeverity());
692             if (compare < 0) {
693               foundInfoList.clear();
694             }
695             else if (compare > 0) {
696               return true;
697             }
698           }
699           foundInfoList.add(info);
700           return true;
701         });
702
703     if (foundInfoList.isEmpty()) return null;
704     if (foundInfoList.size() == 1) return foundInfoList.get(0);
705     return new HighlightInfoComposite(foundInfoList);
706   }
707
708   private static boolean isOffsetInsideHighlightInfo(int offset, @NotNull HighlightInfo info, boolean includeFixRange) {
709     RangeHighlighterEx highlighter = info.highlighter;
710     if (highlighter == null || !highlighter.isValid()) return false;
711     int startOffset = highlighter.getStartOffset();
712     int endOffset = highlighter.getEndOffset();
713     if (startOffset <= offset && offset <= endOffset) {
714       return true;
715     }
716     if (!includeFixRange) return false;
717     RangeMarker fixMarker = info.fixMarker;
718     if (fixMarker != null) {  // null means its range is the same as highlighter
719       if (!fixMarker.isValid()) return false;
720       startOffset = fixMarker.getStartOffset();
721       endOffset = fixMarker.getEndOffset();
722       return startOffset <= offset && offset <= endOffset;
723     }
724     return false;
725   }
726
727   @NotNull
728   public static List<LineMarkerInfo> getLineMarkers(@NotNull Document document, @NotNull Project project) {
729     ApplicationManager.getApplication().assertIsDispatchThread();
730     List<LineMarkerInfo> result = new ArrayList<>();
731     LineMarkersUtil.processLineMarkers(project, document, new TextRange(0, document.getTextLength()), -1,
732                                        new CommonProcessors.CollectProcessor<>(result));
733     return result;
734   }
735
736   void setLastIntentionHint(@NotNull Project project,
737                             @NotNull PsiFile file,
738                             @NotNull Editor editor,
739                             @NotNull ShowIntentionsPass.IntentionsInfo intentions,
740                             boolean hasToRecreate) {
741     if (!editor.getSettings().isShowIntentionBulb()) {
742       return;
743     }
744     ApplicationManager.getApplication().assertIsDispatchThread();
745     hideLastIntentionHint();
746     
747     if (editor.getCaretModel().getCaretCount() > 1) return;
748     
749     IntentionHintComponent hintComponent = IntentionHintComponent.showIntentionHint(project, file, editor, intentions, false);
750     if (hasToRecreate) {
751       hintComponent.recreate();
752     }
753     myLastIntentionHint = hintComponent;
754   }
755
756   void hideLastIntentionHint() {
757     ApplicationManager.getApplication().assertIsDispatchThread();
758     IntentionHintComponent hint = myLastIntentionHint;
759     if (hint != null && hint.isVisible()) {
760       hint.hide();
761       myLastIntentionHint = null;
762     }
763   }
764
765   @Nullable
766   public IntentionHintComponent getLastIntentionHint() {
767     return myLastIntentionHint;
768   }
769
770   @Nullable
771   @Override
772   public Element getState() {
773     Element state = new Element("state");
774     if (myDisabledHintsFiles.isEmpty()) {
775       return state;
776     }
777
778     List<String> array = new SmartList<>();
779     for (VirtualFile file : myDisabledHintsFiles) {
780       if (file.isValid()) {
781         array.add(file.getUrl());
782       }
783     }
784
785     if (!array.isEmpty()) {
786       Collections.sort(array);
787
788       Element disableHintsElement = new Element(DISABLE_HINTS_TAG);
789       state.addContent(disableHintsElement);
790       for (String url : array) {
791         disableHintsElement.addContent(new Element(FILE_TAG).setAttribute(URL_ATT, url));
792       }
793     }
794     return state;
795   }
796
797   @Override
798   public void loadState(Element state) {
799     myDisabledHintsFiles.clear();
800
801     Element element = state.getChild(DISABLE_HINTS_TAG);
802     if (element != null) {
803       for (Element e : element.getChildren(FILE_TAG)) {
804         String url = e.getAttributeValue(URL_ATT);
805         if (url != null) {
806           VirtualFile file = VirtualFileManager.getInstance().findFileByUrl(url);
807           if (file != null) {
808             myDisabledHintsFiles.add(file);
809           }
810         }
811       }
812     }
813   }
814
815   private final Runnable submitPassesRunnable = new Runnable() {
816     @Override
817     public void run() {
818       PassExecutorService.log(getUpdateProgress(), null, "Update Runnable. myUpdateByTimerEnabled:",
819                               myUpdateByTimerEnabled, " something disposed:",
820                               PowerSaveMode.isEnabled() || myDisposed || !myProject.isInitialized(), " activeEditors:",
821                               myProject.isDisposed() ? null : getSelectedEditors());
822       if (!myUpdateByTimerEnabled) return;
823       if (myDisposed) return;
824       ApplicationManager.getApplication().assertIsDispatchThread();
825
826       final Collection<FileEditor> activeEditors = getSelectedEditors();
827       if (activeEditors.isEmpty()) return;
828
829       if (ApplicationManager.getApplication().isWriteAccessAllowed()) {
830         // makes no sense to start from within write action, will cancel anyway
831         // we'll restart when the write action finish
832         return;
833       }
834       final PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)myPsiDocumentManager;
835       if (documentManager.hasUncommitedDocuments()) {
836         documentManager.cancelAndRunWhenAllCommitted("restart daemon when all committed", this);
837         return;
838       }
839       if (RefResolveService.ENABLED &&
840           !RefResolveService.getInstance(myProject).isUpToDate() &&
841           RefResolveService.getInstance(myProject).getQueueSize() == 1) {
842         return; // if the user have just typed in something, wait until the file is re-resolved
843         // (or else it will blink like crazy since unused symbols calculation depends on resolve service)
844       }
845
846       Map<FileEditor, HighlightingPass[]> passes = new THashMap<>(activeEditors.size());
847       for (FileEditor fileEditor : activeEditors) {
848         BackgroundEditorHighlighter highlighter = fileEditor.getBackgroundHighlighter();
849         if (highlighter != null) {
850           HighlightingPass[] highlightingPasses = highlighter.createPassesForEditor();
851           passes.put(fileEditor, highlightingPasses);
852         }
853       }
854       // cancel all after calling createPasses() since there are perverts {@link com.intellij.util.xml.ui.DomUIFactoryImpl} who are changing PSI there
855       cancelUpdateProgress(true, "Cancel by alarm");
856       myUpdateRunnableFuture.cancel(false);
857       DaemonProgressIndicator progress = createUpdateProgress();
858       myPassExecutorService.submitPasses(passes, progress);
859     }
860   };
861
862   @NotNull
863   private Runnable createUpdateRunnable() {
864     return new Runnable() {
865       @Override
866       public void run() {
867         ApplicationManager.getApplication().assertIsDispatchThread();
868
869         if (myDisposed || !myProject.isInitialized() || PowerSaveMode.isEnabled()) {
870           return;
871         }
872         // wait for heavy processing to stop, re-schedule daemon but not too soon
873         if (HeavyProcessLatch.INSTANCE.isRunning()) {
874           HeavyProcessLatch.INSTANCE.executeOutOfHeavyProcess(() ->
875               myUpdateRunnableFuture = myAlarm.schedule(myUpdateRunnable, Math.max(mySettings.AUTOREPARSE_DELAY, 100), TimeUnit.MILLISECONDS));
876           return;
877         }
878         Editor activeEditor = FileEditorManager.getInstance(myProject).getSelectedTextEditor();
879
880         if (activeEditor == null) {
881           AutoPopupController.runTransactionWithEverythingCommitted(myProject, submitPassesRunnable);
882         }
883         else {
884           ((PsiDocumentManagerBase)myPsiDocumentManager).cancelAndRunWhenAllCommitted("start daemon when all committed", submitPassesRunnable);
885         }
886       }
887
888       @Override
889       public String toString() {
890         return "Daemon update runnable";
891       }
892     };
893   }
894
895   @NotNull
896   private synchronized DaemonProgressIndicator createUpdateProgress() {
897     DaemonProgressIndicator old = myUpdateProgress;
898     if (!old.isCanceled()) {
899       old.cancel();
900     }
901     DaemonProgressIndicator progress = new DaemonProgressIndicator() {
902       @Override
903       public void stopIfRunning() {
904         super.stopIfRunning();
905         myProject.getMessageBus().syncPublisher(DAEMON_EVENT_TOPIC).daemonFinished();
906       }
907     };
908     progress.setModalityProgress(null);
909     progress.start();
910     myUpdateProgress = progress;
911     return progress;
912   }
913
914   @Override
915   public void autoImportReferenceAtCursor(@NotNull Editor editor, @NotNull PsiFile file) {
916     for (ReferenceImporter importer : Extensions.getExtensions(ReferenceImporter.EP_NAME)) {
917       if (importer.autoImportReferenceAtCursor(editor, file)) break;
918     }
919   }
920
921   @TestOnly
922   @NotNull
923   synchronized DaemonProgressIndicator getUpdateProgress() {
924     return myUpdateProgress;
925   }
926
927   @NotNull
928   private Collection<FileEditor> getSelectedEditors() {
929     ApplicationManager.getApplication().assertIsDispatchThread();
930
931     // Editors in modal context
932     List<Editor> editors = getActiveEditors();
933
934     Collection<FileEditor> activeTextEditors = new THashSet<>(editors.size());
935     for (Editor editor : editors) {
936       if (editor.isDisposed()) continue;
937       TextEditor textEditor = TextEditorProvider.getInstance().getTextEditor(editor);
938       activeTextEditors.add(textEditor);
939     }
940     if (ApplicationManager.getApplication().getCurrentModalityState() != ModalityState.NON_MODAL) {
941       return activeTextEditors;
942     }
943
944     // Editors in tabs.
945     Collection<FileEditor> result = new THashSet<>();
946     Collection<VirtualFile> files = new THashSet<>(activeTextEditors.size());
947     final FileEditor[] tabEditors = FileEditorManager.getInstance(myProject).getSelectedEditors();
948     for (FileEditor tabEditor : tabEditors) {
949       if (!tabEditor.isValid()) continue;
950       VirtualFile file = ((FileEditorManagerEx)FileEditorManager.getInstance(myProject)).getFile(tabEditor);
951       if (file != null) {
952         files.add(file);
953       }
954       result.add(tabEditor);
955     }
956     // do not duplicate documents
957     for (FileEditor fileEditor : activeTextEditors) {
958       VirtualFile file = ((FileEditorManagerEx)FileEditorManager.getInstance(myProject)).getFile(fileEditor);
959       if (file != null && files.contains(file)) continue;
960       result.add(fileEditor);
961     }
962     return result;
963   }
964
965   @NotNull
966   private List<Editor> getActiveEditors() {
967     return myEditorTracker.getActiveEditors();
968   }
969
970   @TestOnly
971   private static void wrap(@NotNull ThrowableRunnable runnable) {
972     try {
973       runnable.run();
974     }
975     catch (RuntimeException | Error e) {
976       throw e;
977     }
978     catch (Throwable e) {
979       throw new RuntimeException(e);
980     }
981   }
982 }