fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / core-impl / src / com / intellij / psi / impl / PsiDocumentManagerBase.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.psi.impl;
3
4 import com.google.common.annotations.VisibleForTesting;
5 import com.intellij.injected.editor.DocumentWindow;
6 import com.intellij.lang.ASTNode;
7 import com.intellij.lang.injection.InjectedLanguageManager;
8 import com.intellij.openapi.Disposable;
9 import com.intellij.openapi.application.*;
10 import com.intellij.openapi.application.impl.ApplicationInfoImpl;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.editor.Document;
13 import com.intellij.openapi.editor.DocumentRunnable;
14 import com.intellij.openapi.editor.event.DocumentEvent;
15 import com.intellij.openapi.editor.event.DocumentListener;
16 import com.intellij.openapi.editor.ex.DocumentEx;
17 import com.intellij.openapi.editor.ex.PrioritizedInternalDocumentListener;
18 import com.intellij.openapi.editor.impl.DocumentImpl;
19 import com.intellij.openapi.editor.impl.EditorDocumentPriorities;
20 import com.intellij.openapi.editor.impl.FrozenDocument;
21 import com.intellij.openapi.editor.impl.event.RetargetRangeMarkers;
22 import com.intellij.openapi.fileEditor.FileDocumentManager;
23 import com.intellij.openapi.progress.*;
24 import com.intellij.openapi.project.Project;
25 import com.intellij.openapi.roots.FileIndexFacade;
26 import com.intellij.openapi.util.*;
27 import com.intellij.openapi.vfs.VirtualFile;
28 import com.intellij.psi.*;
29 import com.intellij.psi.impl.file.impl.FileManager;
30 import com.intellij.psi.impl.file.impl.FileManagerImpl;
31 import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl;
32 import com.intellij.psi.impl.source.PsiFileImpl;
33 import com.intellij.psi.impl.source.tree.FileElement;
34 import com.intellij.psi.text.BlockSupport;
35 import com.intellij.psi.util.PsiUtilCore;
36 import com.intellij.util.*;
37 import com.intellij.util.concurrency.Semaphore;
38 import com.intellij.util.containers.ContainerUtil;
39 import com.intellij.util.ui.UIUtil;
40 import org.jetbrains.annotations.*;
41
42 import javax.swing.*;
43 import java.util.*;
44 import java.util.concurrent.ConcurrentMap;
45
46 public abstract class PsiDocumentManagerBase extends PsiDocumentManager implements DocumentListener, Disposable {
47   static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl");
48   private static final Key<Document> HARD_REF_TO_DOCUMENT = Key.create("HARD_REFERENCE_TO_DOCUMENT");
49   private static final Key<List<Runnable>> ACTION_AFTER_COMMIT = Key.create("ACTION_AFTER_COMMIT");
50
51   protected final Project myProject;
52   private final PsiManager myPsiManager;
53   protected final DocumentCommitProcessor myDocumentCommitProcessor;
54   final Set<Document> myUncommittedDocuments = ContainerUtil.newConcurrentSet();
55   private final Map<Document, UncommittedInfo> myUncommittedInfos = ContainerUtil.newConcurrentMap();
56   boolean myStopTrackingDocuments;
57   private boolean myPerformBackgroundCommit = true;
58
59   private volatile boolean myIsCommitInProgress;
60   private static volatile boolean ourIsFullReparseInProgress;
61   private final PsiToDocumentSynchronizer mySynchronizer;
62
63   private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
64
65   protected PsiDocumentManagerBase(@NotNull Project project) {
66     myProject = project;
67     myPsiManager = PsiManager.getInstance(project);
68     myDocumentCommitProcessor = ApplicationManager.getApplication().getService(DocumentCommitProcessor.class);
69     mySynchronizer = new PsiToDocumentSynchronizer(this, project.getMessageBus());
70     myPsiManager.addPsiTreeChangeListener(mySynchronizer);
71
72     project.getMessageBus().connect(this).subscribe(PsiDocumentTransactionListener.TOPIC, (document, file) -> {
73       myUncommittedDocuments.remove(document);
74     });
75   }
76
77   @Override
78   @Nullable
79   public PsiFile getPsiFile(@NotNull Document document) {
80     if (document instanceof DocumentWindow && !((DocumentWindow)document).isValid()) {
81       return null;
82     }
83
84     PsiFile psiFile = getCachedPsiFile(document);
85     if (psiFile != null) {
86       return ensureValidFile(psiFile, "Cached PSI");
87     }
88
89     final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
90     if (virtualFile == null || !virtualFile.isValid()) return null;
91
92     psiFile = getPsiFile(virtualFile);
93     if (psiFile == null) return null;
94
95     fireFileCreated(document, psiFile);
96
97     return psiFile;
98   }
99
100   @NotNull
101   private static PsiFile ensureValidFile(@NotNull PsiFile psiFile, @NotNull String debugInfo) {
102     if (!psiFile.isValid()) throw new PsiInvalidElementAccessException(psiFile, debugInfo);
103     return psiFile;
104   }
105
106   @Deprecated
107   @ApiStatus.ScheduledForRemoval(inVersion = "2017")
108   // todo remove when plugins come to their senses and stopped using it
109   // todo to be removed in idea 17
110   public static void cachePsi(@NotNull Document document, @Nullable PsiFile file) {
111     DeprecatedMethodException.report("Unsupported method");
112   }
113
114   public void associatePsi(@NotNull Document document, @Nullable PsiFile file) {
115     throw new UnsupportedOperationException();
116   }
117
118   @Override
119   public PsiFile getCachedPsiFile(@NotNull Document document) {
120     final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
121     if (virtualFile == null || !virtualFile.isValid()) return null;
122     return getCachedPsiFile(virtualFile);
123   }
124
125   @Nullable
126   FileViewProvider getCachedViewProvider(@NotNull Document document) {
127     final VirtualFile virtualFile = getVirtualFile(document);
128     if (virtualFile == null) return null;
129     return getFileManager().findCachedViewProvider(virtualFile);
130   }
131
132   @Nullable
133   private static VirtualFile getVirtualFile(@NotNull Document document) {
134     final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
135     if (virtualFile == null || !virtualFile.isValid()) return null;
136     return virtualFile;
137   }
138
139   @Nullable
140   PsiFile getCachedPsiFile(@NotNull VirtualFile virtualFile) {
141     return getFileManager().getCachedPsiFile(virtualFile);
142   }
143
144   @Nullable
145   private PsiFile getPsiFile(@NotNull VirtualFile virtualFile) {
146     return getFileManager().findFile(virtualFile);
147   }
148
149   @NotNull
150   private FileManager getFileManager() {
151     return ((PsiManagerEx)myPsiManager).getFileManager();
152   }
153
154   @Override
155   public Document getDocument(@NotNull PsiFile file) {
156     Document document = getCachedDocument(file);
157     if (document != null) {
158       if (!file.getViewProvider().isPhysical()) {
159         PsiUtilCore.ensureValid(file);
160         associatePsi(document, file);
161       }
162       return document;
163     }
164
165     FileViewProvider viewProvider = file.getViewProvider();
166     if (!viewProvider.isEventSystemEnabled()) return null;
167
168     document = FileDocumentManager.getInstance().getDocument(viewProvider.getVirtualFile());
169     if (document != null) {
170       if (document.getTextLength() != file.getTextLength()) {
171         String message = "Document/PSI mismatch: " + file + " (" + file.getClass() + "); physical=" + viewProvider.isPhysical();
172         if (document.getTextLength() + file.getTextLength() < 8096) {
173           message += "\n=== document ===\n" + document.getText() + "\n=== PSI ===\n" + file.getText();
174         }
175         throw new AssertionError(message);
176       }
177
178       if (!viewProvider.isPhysical()) {
179         PsiUtilCore.ensureValid(file);
180         associatePsi(document, file);
181         file.putUserData(HARD_REF_TO_DOCUMENT, document);
182       }
183     }
184
185     return document;
186   }
187
188   @Override
189   public Document getCachedDocument(@NotNull PsiFile file) {
190     if (!file.isPhysical()) return null;
191     VirtualFile vFile = file.getViewProvider().getVirtualFile();
192     return FileDocumentManager.getInstance().getCachedDocument(vFile);
193   }
194
195   @Override
196   public void commitAllDocuments() {
197     ApplicationManager.getApplication().assertIsDispatchThread();
198     ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed();
199
200     if (myUncommittedDocuments.isEmpty()) return;
201
202     final Document[] documents = getUncommittedDocuments();
203     for (Document document : documents) {
204       if (isCommitted(document)) {
205         boolean success = doCommitWithoutReparse(document);
206         LOG.error("Committed document in uncommitted set: " + document + ", force-committed=" + success);
207       }
208       else if (!doCommit(document)) {
209         LOG.error("Couldn't commit " + document);
210       }
211     }
212
213     assertEverythingCommitted();
214   }
215
216   private void assertEverythingCommitted() {
217     LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments);
218   }
219
220   @VisibleForTesting
221   public boolean doCommitWithoutReparse(@NotNull Document document) {
222     return finishCommitInWriteAction(document, Collections.emptyList(), Collections.emptyList(), true, true);
223   }
224
225   @Override
226   public void performForCommittedDocument(@NotNull final Document doc, @NotNull final Runnable action) {
227     Document document = getTopLevelDocument(doc);
228     if (isCommitted(document)) {
229       action.run();
230     }
231     else {
232       addRunOnCommit(document, action);
233     }
234   }
235
236   private final Map<Object, Runnable> actionsWhenAllDocumentsAreCommitted = new LinkedHashMap<>(); //accessed from EDT only
237   private static final Object PERFORM_ALWAYS_KEY = ObjectUtils.sentinel("PERFORM_ALWAYS");
238
239   /**
240    * Cancel previously registered action and schedules (new) action to be executed when all documents are committed.
241    *
242    * @param key    the (unique) id of the action.
243    * @param action The action to be executed after automatic commit.
244    *               This action will overwrite any action which was registered under this key earlier.
245    *               The action will be executed in EDT.
246    * @return true if action has been run immediately, or false if action was scheduled for execution later.
247    */
248   public boolean cancelAndRunWhenAllCommitted(@NonNls @NotNull Object key, @NotNull final Runnable action) {
249     ApplicationManager.getApplication().assertIsDispatchThread();
250     if (myProject.isDisposed()) {
251       action.run();
252       return true;
253     }
254     if (myUncommittedDocuments.isEmpty()) {
255       if (!isCommitInProgress()) {
256         // in case of fireWriteActionFinished() we didn't execute 'actionsWhenAllDocumentsAreCommitted' yet
257         assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted;
258       }
259       action.run();
260       return true;
261     }
262
263     checkWeAreOutsideAfterCommitHandler();
264
265     actionsWhenAllDocumentsAreCommitted.put(key, action);
266     return false;
267   }
268
269   public static void addRunOnCommit(@NotNull Document document, @NotNull Runnable action) {
270     synchronized (ACTION_AFTER_COMMIT) {
271       List<Runnable> list = document.getUserData(ACTION_AFTER_COMMIT);
272       if (list == null) {
273         document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<>());
274       }
275       list.add(action);
276     }
277   }
278
279   private static List<Runnable> getAndClearActionsAfterCommit(@NotNull Document document) {
280     List<Runnable> list;
281     synchronized (ACTION_AFTER_COMMIT) {
282       list = document.getUserData(ACTION_AFTER_COMMIT);
283       if (list != null) {
284         list = new ArrayList<>(list);
285         document.putUserData(ACTION_AFTER_COMMIT, null);
286       }
287     }
288     return list;
289   }
290
291   @Override
292   public void commitDocument(@NotNull final Document doc) {
293     final Document document = getTopLevelDocument(doc);
294
295     if (isEventSystemEnabled(document)) {
296       ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed();
297     }
298
299     if (!isCommitted(document)) {
300       doCommit(document);
301     }
302   }
303
304   private boolean isEventSystemEnabled(Document document) {
305     FileViewProvider viewProvider = getCachedViewProvider(document);
306     return viewProvider != null && viewProvider.isEventSystemEnabled() && !AbstractFileViewProvider.isFreeThreaded(viewProvider);
307   }
308
309   boolean finishCommit(@NotNull final Document document,
310                        @NotNull List<? extends BooleanRunnable> finishProcessors,
311                        @NotNull List<? extends BooleanRunnable> reparseInjectedProcessors,
312                        final boolean synchronously,
313                        @NotNull final Object reason) {
314     assert !myProject.isDisposed() : "Already disposed";
315     ApplicationManager.getApplication().assertIsDispatchThread();
316     final boolean[] ok = {true};
317     Runnable runnable = new DocumentRunnable(document, myProject) {
318       @Override
319       public void run() {
320         ok[0] = finishCommitInWriteAction(document, finishProcessors, reparseInjectedProcessors, synchronously, false);
321       }
322     };
323     if (synchronously) {
324       runnable.run();
325     }
326     else {
327       ApplicationManager.getApplication().runWriteAction(runnable);
328     }
329
330     if (ok[0]) {
331       // run after commit actions outside write action
332       runAfterCommitActions(document);
333       if (DebugUtil.DO_EXPENSIVE_CHECKS && !ApplicationInfoImpl.isInStressTest()) {
334         checkAllElementsValid(document, reason);
335       }
336     }
337     return ok[0];
338   }
339
340   protected boolean finishCommitInWriteAction(@NotNull final Document document,
341                                               @NotNull List<? extends BooleanRunnable> finishProcessors,
342                                               @NotNull List<? extends BooleanRunnable> reparseInjectedProcessors,
343                                               final boolean synchronously,
344                                               boolean forceNoPsiCommit) {
345     ApplicationManager.getApplication().assertIsDispatchThread();
346     if (myProject.isDisposed()) return false;
347     assert !(document instanceof DocumentWindow);
348
349     VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
350     if (virtualFile != null) {
351       getSmartPointerManager().fastenBelts(virtualFile);
352     }
353
354     FileViewProvider viewProvider = forceNoPsiCommit ? null : getCachedViewProvider(document);
355
356     myIsCommitInProgress = true;
357     Ref<Boolean> success = new Ref<>(true);
358     try {
359       ProgressManager.getInstance().executeNonCancelableSection(() -> {
360         if (viewProvider == null) {
361           handleCommitWithoutPsi(document);
362         }
363         else {
364           success.set(commitToExistingPsi(document, finishProcessors, reparseInjectedProcessors, synchronously, virtualFile));
365         }
366       });
367     }
368     catch (Throwable e) {
369       try {
370         forceReload(virtualFile, viewProvider);
371       }
372       finally {
373         LOG.error(e);
374       }
375     }
376     finally {
377       if (success.get()) {
378         myUncommittedDocuments.remove(document);
379       }
380       myIsCommitInProgress = false;
381     }
382
383     return success.get();
384   }
385
386   private boolean commitToExistingPsi(@NotNull Document document,
387                                       @NotNull List<? extends BooleanRunnable> finishProcessors,
388                                       @NotNull List<? extends BooleanRunnable> reparseInjectedProcessors,
389                                       boolean synchronously,
390                                       @Nullable VirtualFile virtualFile) {
391     for (BooleanRunnable finishRunnable : finishProcessors) {
392       boolean success = finishRunnable.run();
393       if (synchronously) {
394         assert success : finishRunnable + " in " + finishProcessors;
395       }
396       if (!success) {
397         return false;
398       }
399     }
400     clearUncommittedInfo(document);
401     if (virtualFile != null) {
402       getSmartPointerManager().updatePointerTargetsAfterReparse(virtualFile);
403     }
404     FileViewProvider viewProvider = getCachedViewProvider(document);
405     if (viewProvider != null) {
406       viewProvider.contentsSynchronized();
407     }
408     for (BooleanRunnable runnable : reparseInjectedProcessors) {
409       if (!runnable.run()) return false;
410     }
411     return true;
412   }
413
414   void forceReload(VirtualFile virtualFile, @Nullable FileViewProvider viewProvider) {
415     if (viewProvider != null) {
416       ((AbstractFileViewProvider)viewProvider).markInvalidated();
417     }
418     if (virtualFile != null) {
419       ((FileManagerImpl)getFileManager()).forceReload(virtualFile);
420     }
421   }
422
423   private void checkAllElementsValid(@NotNull Document document, @NotNull final Object reason) {
424     final PsiFile psiFile = getCachedPsiFile(document);
425     if (psiFile != null) {
426       psiFile.accept(new PsiRecursiveElementWalkingVisitor() {
427         @Override
428         public void visitElement(PsiElement element) {
429           if (!element.isValid()) {
430             throw new AssertionError("Commit to '" + psiFile.getVirtualFile() + "' has led to invalid element: " + element + "; Reason: '" + reason + "'");
431           }
432         }
433       });
434     }
435   }
436
437   private boolean doCommit(@NotNull final Document document) {
438     assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener";
439
440     // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged()
441     if (getSynchronizer().isDocumentAffectedByTransactions(document)) return false;
442
443     final PsiFile psiFile = getPsiFile(document);
444     if (psiFile == null) {
445       myUncommittedDocuments.remove(document);
446       runAfterCommitActions(document);
447       return true; // the project must be closing or file deleted
448     }
449
450     Runnable runnable = () -> {
451       myIsCommitInProgress = true;
452       try {
453         myDocumentCommitProcessor.commitSynchronously(document, myProject, psiFile);
454       }
455       finally {
456         myIsCommitInProgress = false;
457       }
458       assert !isInUncommittedSet(document) : "Document :" + document;
459     };
460
461     ApplicationManager.getApplication().runWriteAction(runnable);
462     return true;
463   }
464
465   // true if the PSI is being modified and events being sent
466   public boolean isCommitInProgress() {
467     return myIsCommitInProgress || isFullReparseInProgress();
468   }
469
470   public static boolean isFullReparseInProgress() {
471     return ourIsFullReparseInProgress;
472   }
473
474   @Override
475   public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) {
476     final Ref<T> ref = Ref.create(null);
477     commitAndRunReadAction(() -> ref.set(computation.compute()));
478     return ref.get();
479   }
480
481   @Override
482   public void reparseFiles(@NotNull Collection<? extends VirtualFile> files, boolean includeOpenFiles) {
483     FileContentUtilCore.reparseFiles(files);
484   }
485
486   @Override
487   public void commitAndRunReadAction(@NotNull final Runnable runnable) {
488     final Application application = ApplicationManager.getApplication();
489     if (SwingUtilities.isEventDispatchThread()) {
490       commitAllDocuments();
491       runnable.run();
492       return;
493     }
494
495     if (application.isReadAccessAllowed()) {
496       LOG.error("Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock. "+Thread.currentThread());
497     }
498
499     while (true) {
500       boolean executed = ReadAction.compute(() -> {
501         if (myUncommittedDocuments.isEmpty()) {
502           runnable.run();
503           return true;
504         }
505         return false;
506       });
507       if (executed) break;
508
509       TransactionId contextTransaction = TransactionGuard.getInstance().getContextTransaction();
510       Semaphore semaphore = new Semaphore(1);
511       application.invokeLater(() -> {
512         if (myProject.isDisposed()) {
513           // committedness doesn't matter anymore; give clients a chance to do checkCanceled
514           semaphore.up();
515           return;
516         }
517
518         performWhenAllCommitted(() -> semaphore.up(), contextTransaction);
519       }, ModalityState.any());
520
521       while (!semaphore.waitFor(10)) {
522         ProgressManager.checkCanceled();
523       }
524     }
525   }
526
527   /**
528    * Schedules action to be executed when all documents are committed.
529    *
530    * @return true if action has been run immediately, or false if action was scheduled for execution later.
531    */
532   @Override
533   public boolean performWhenAllCommitted(@NotNull final Runnable action) {
534     return performWhenAllCommitted(action, TransactionGuard.getInstance().getContextTransaction());
535   }
536
537   private boolean performWhenAllCommitted(@NotNull Runnable action, @Nullable TransactionId context) {
538     ApplicationManager.getApplication().assertIsDispatchThread();
539     checkWeAreOutsideAfterCommitHandler();
540
541     assert !myProject.isDisposed() : "Already disposed: " + myProject;
542     if (myUncommittedDocuments.isEmpty()) {
543       action.run();
544       return true;
545     }
546     CompositeRunnable actions = (CompositeRunnable)actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY);
547     if (actions == null) {
548       actions = new CompositeRunnable();
549       actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions);
550     }
551     actions.add(action);
552
553     if (context != null) {
554       // re-add all uncommitted documents into the queue with this new modality
555       // because this client obviously expects them to commit even inside modal dialog
556       for (Document document : myUncommittedDocuments) {
557         myDocumentCommitProcessor.commitAsynchronously(myProject, document,
558                                                        "re-added with context "+context+" because performWhenAllCommitted("+context+") was called", context);
559       }
560     }
561     return false;
562   }
563
564   @Override
565   public void performLaterWhenAllCommitted(@NotNull final Runnable runnable) {
566     performLaterWhenAllCommitted(runnable, ModalityState.defaultModalityState());
567   }
568
569   @Override
570   public void performLaterWhenAllCommitted(@NotNull final Runnable runnable, final ModalityState modalityState) {
571     final Runnable whenAllCommitted = () -> ApplicationManager.getApplication().invokeLater(() -> {
572       if (hasUncommitedDocuments()) {
573         // no luck, will try later
574         performLaterWhenAllCommitted(runnable);
575       }
576       else {
577         runnable.run();
578       }
579     }, modalityState, myProject.getDisposed());
580     if (ApplicationManager.getApplication().isDispatchThread() && isInsideCommitHandler()) {
581       whenAllCommitted.run();
582     }
583     else {
584       UIUtil.invokeLaterIfNeeded(() -> { if (!myProject.isDisposed()) performWhenAllCommitted(whenAllCommitted);});
585     }
586   }
587
588   private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable {
589     @Override
590     public void run() {
591       for (Runnable runnable : this) {
592         runnable.run();
593       }
594     }
595   }
596
597   private void runAfterCommitActions(@NotNull Document document) {
598     if (!ApplicationManager.getApplication().isDispatchThread()) {
599       // have to run in EDT to guarantee data structure safe access and "execute in EDT" callbacks contract
600       ApplicationManager.getApplication().invokeLater(()-> {
601         if (!myProject.isDisposed() && isCommitted(document)) runAfterCommitActions(document);
602       });
603       return;
604     }
605     ApplicationManager.getApplication().assertIsDispatchThread();
606     List<Runnable> list = getAndClearActionsAfterCommit(document);
607     if (list != null) {
608       for (final Runnable runnable : list) {
609         runnable.run();
610       }
611     }
612
613     if (!hasUncommitedDocuments() && !actionsWhenAllDocumentsAreCommitted.isEmpty()) {
614       List<Runnable> actions = new ArrayList<>(actionsWhenAllDocumentsAreCommitted.values());
615       beforeCommitHandler();
616       List<Pair<Runnable, Throwable>> exceptions = new ArrayList<>();
617       try {
618         for (Runnable action : actions) {
619           try {
620             action.run();
621           }
622           catch (ProcessCanceledException e) {
623             // some actions are crazy enough to use PCE for their own control flow.
624             // swallow and ignore to not disrupt completely unrelated control flow.
625           }
626           catch (Throwable e) {
627             exceptions.add(Pair.create(action, e));
628           }
629         }
630       }
631       finally {
632         // unblock adding listeners
633         actionsWhenAllDocumentsAreCommitted.clear();
634       }
635       for (Pair<Runnable, Throwable> pair : exceptions) {
636         Runnable action = pair.getFirst();
637         Throwable e = pair.getSecond();
638         LOG.error("During running " + action, e);
639       }
640     }
641   }
642
643   private void beforeCommitHandler() {
644     actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, EmptyRunnable.getInstance()); // to prevent listeners from registering new actions during firing
645   }
646   private void checkWeAreOutsideAfterCommitHandler() {
647     if (isInsideCommitHandler()) {
648       throw new IncorrectOperationException("You must not call performWhenAllCommitted()/cancelAndRunWhenCommitted() from within after-commit handler");
649     }
650   }
651
652   private boolean isInsideCommitHandler() {
653     return actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY) == EmptyRunnable.getInstance();
654   }
655
656   @Override
657   public void addListener(@NotNull Listener listener) {
658     myListeners.add(listener);
659   }
660
661   @Override
662   public void removeListener(@NotNull Listener listener) {
663     myListeners.remove(listener);
664   }
665
666   @Override
667   public boolean isDocumentBlockedByPsi(@NotNull Document doc) {
668     return false;
669   }
670
671   @Override
672   public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) {
673   }
674
675   void fireDocumentCreated(@NotNull Document document, PsiFile file) {
676     myProject.getMessageBus().syncPublisher(PsiDocumentListener.TOPIC).documentCreated(document, file, myProject);
677     for (Listener listener : myListeners) {
678       listener.documentCreated(document, file);
679     }
680   }
681
682   private void fireFileCreated(@NotNull Document document, @NotNull PsiFile file) {
683     myProject.getMessageBus().syncPublisher(PsiDocumentListener.TOPIC).fileCreated(file, document);
684     for (Listener listener : myListeners) {
685       listener.fileCreated(file, document);
686     }
687   }
688
689   @Override
690   @NotNull
691   public CharSequence getLastCommittedText(@NotNull Document document) {
692     return getLastCommittedDocument(document).getImmutableCharSequence();
693   }
694
695   @Override
696   public long getLastCommittedStamp(@NotNull Document document) {
697     return getLastCommittedDocument(getTopLevelDocument(document)).getModificationStamp();
698   }
699
700   @Override
701   @Nullable
702   public Document getLastCommittedDocument(@NotNull PsiFile file) {
703     Document document = getDocument(file);
704     return document == null ? null : getLastCommittedDocument(document);
705   }
706
707   @NotNull
708   public DocumentEx getLastCommittedDocument(@NotNull Document document) {
709     if (document instanceof FrozenDocument) return (DocumentEx)document;
710
711     if (document instanceof DocumentWindow) {
712       DocumentWindow window = (DocumentWindow)document;
713       Document delegate = window.getDelegate();
714       if (delegate instanceof FrozenDocument) return (DocumentEx)window;
715
716       if (!window.isValid()) {
717         throw new AssertionError("host committed: " + isCommitted(delegate) + ", window=" + window);
718       }
719
720       UncommittedInfo info = myUncommittedInfos.get(delegate);
721       DocumentWindow answer = info == null ? null : info.myFrozenWindows.get(document);
722       if (answer == null) answer = freezeWindow(window);
723       if (info != null) answer = ConcurrencyUtil.cacheOrGet(info.myFrozenWindows, window, answer);
724       return (DocumentEx)answer;
725     }
726
727     assert document instanceof DocumentImpl;
728     UncommittedInfo info = myUncommittedInfos.get(document);
729     return info != null ? info.myFrozen : ((DocumentImpl)document).freeze();
730   }
731
732   @NotNull
733   protected DocumentWindow freezeWindow(@NotNull DocumentWindow document) {
734     throw new UnsupportedOperationException();
735   }
736
737   @NotNull
738   public List<DocumentEvent> getEventsSinceCommit(@NotNull Document document) {
739     assert document instanceof DocumentImpl : document;
740     UncommittedInfo info = myUncommittedInfos.get(document);
741     if (info != null) {
742       //noinspection unchecked
743       return (List<DocumentEvent>)info.myEvents.clone();
744     }
745     return Collections.emptyList();
746
747   }
748
749   @Override
750   @NotNull
751   public Document[] getUncommittedDocuments() {
752     ApplicationManager.getApplication().assertReadAccessAllowed();
753     //noinspection UnnecessaryLocalVariable
754     Document[] documents = myUncommittedDocuments.toArray(Document.EMPTY_ARRAY);
755     return documents; // java.util.ConcurrentHashMap.keySet().toArray() guaranteed to return array with no nulls
756   }
757
758   boolean isInUncommittedSet(@NotNull Document document) {
759     return myUncommittedDocuments.contains(getTopLevelDocument(document));
760   }
761
762   @Override
763   public boolean isUncommited(@NotNull Document document) {
764     return !isCommitted(document);
765   }
766
767   @Override
768   public boolean isCommitted(@NotNull Document document) {
769     document = getTopLevelDocument(document);
770     if (getSynchronizer().isInSynchronization(document)) return true;
771     return (!(document instanceof DocumentEx) || !((DocumentEx)document).isInEventsHandling())
772            && !isInUncommittedSet(document);
773   }
774
775   @NotNull
776   private static Document getTopLevelDocument(@NotNull Document document) {
777     return document instanceof DocumentWindow ? ((DocumentWindow)document).getDelegate() : document;
778   }
779
780   @Override
781   public boolean hasUncommitedDocuments() {
782     return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty();
783   }
784
785   @Override
786   public void beforeDocumentChange(@NotNull DocumentEvent event) {
787     if (myStopTrackingDocuments || myProject.isDisposed()) return;
788
789     final Document document = event.getDocument();
790     VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
791     boolean isRelevant = virtualFile != null && isRelevant(virtualFile);
792
793     if (document instanceof DocumentImpl && !myUncommittedInfos.containsKey(document)) {
794       myUncommittedInfos.put(document, new UncommittedInfo((DocumentImpl)document));
795     }
796
797     final FileViewProvider viewProvider = getCachedViewProvider(document);
798     boolean inMyProject = viewProvider != null && viewProvider.getManager() == myPsiManager;
799     if (!isRelevant || !inMyProject) {
800       return;
801     }
802
803     final List<PsiFile> files = viewProvider.getAllFiles();
804     PsiFile psiCause = null;
805     for (PsiFile file : files) {
806       if (file == null) {
807         throw new AssertionError("View provider "+viewProvider+" ("+viewProvider.getClass()+") returned null in its files array: "+files+" for file "+viewProvider.getVirtualFile());
808       }
809
810       if (PsiToDocumentSynchronizer.isInsideAtomicChange(file)) {
811         psiCause = file;
812       }
813     }
814
815     if (psiCause == null) {
816       beforeDocumentChangeOnUnlockedDocument(viewProvider);
817     }
818   }
819
820   protected void beforeDocumentChangeOnUnlockedDocument(@NotNull final FileViewProvider viewProvider) {
821   }
822
823   @Override
824   public void documentChanged(@NotNull DocumentEvent event) {
825     if (myStopTrackingDocuments || myProject.isDisposed()) return;
826
827     final Document document = event.getDocument();
828
829     VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
830     boolean isRelevant = virtualFile != null && isRelevant(virtualFile);
831
832     final FileViewProvider viewProvider = getCachedViewProvider(document);
833     if (viewProvider == null) {
834       handleCommitWithoutPsi(document);
835       return;
836     }
837     boolean inMyProject = viewProvider.getManager() == myPsiManager;
838     if (!isRelevant || !inMyProject) {
839       clearUncommittedInfo(document);
840       return;
841     }
842
843     List<PsiFile> files = viewProvider.getAllFiles();
844     if (files.isEmpty()) {
845       handleCommitWithoutPsi(document);
846       return;
847     }
848
849     boolean commitNecessary = files.stream().noneMatch(file -> PsiToDocumentSynchronizer.isInsideAtomicChange(file) || !(file instanceof PsiFileImpl));
850
851     boolean forceCommit = ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class) &&
852                           (SystemProperties.getBooleanProperty("idea.force.commit.on.external.change", false) ||
853                            ApplicationManager.getApplication().isHeadlessEnvironment() && !ApplicationManager.getApplication().isUnitTestMode());
854
855     // Consider that it's worth to perform complete re-parse instead of merge if the whole document text is replaced and
856     // current document lines number is roughly above 5000. This makes sense in situations when external change is performed
857     // for the huge file (that causes the whole document to be reloaded and 'merge' way takes a while to complete).
858     if (event.isWholeTextReplaced() && document.getTextLength() > 100000) {
859       document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
860     }
861
862     if (commitNecessary) {
863       assert !(document instanceof DocumentWindow);
864       myUncommittedDocuments.add(document);
865       if (forceCommit) {
866         commitDocument(document);
867       }
868       else if (!document.isInBulkUpdate() && myPerformBackgroundCommit) {
869         myDocumentCommitProcessor.commitAsynchronously(myProject, document, event, TransactionGuard.getInstance().getContextTransaction());
870       }
871     }
872     else {
873       clearUncommittedInfo(document);
874     }
875   }
876
877   @Override
878   public void bulkUpdateStarting(@NotNull Document document) {
879     document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
880   }
881
882   @Override
883   public void bulkUpdateFinished(@NotNull Document document) {
884     myDocumentCommitProcessor.commitAsynchronously(myProject, document, "Bulk update finished",
885                                                    TransactionGuard.getInstance().getContextTransaction());
886   }
887
888   class PriorityEventCollector implements PrioritizedInternalDocumentListener {
889     @Override
890     public int getPriority() {
891       return EditorDocumentPriorities.RANGE_MARKER;
892     }
893
894     @Override
895     public void moveTextHappened(@NotNull Document document, int start, int end, int base) {
896       UncommittedInfo info = myUncommittedInfos.get(document);
897       if (info != null) {
898         info.myEvents.add(new RetargetRangeMarkers(document, start, end, base));
899       }
900     }
901
902     @Override
903     public void documentChanged(@NotNull DocumentEvent event) {
904       UncommittedInfo info = myUncommittedInfos.get(event.getDocument());
905       if (info != null) {
906         info.myEvents.add(event);
907       }
908     }
909   }
910
911   void handleCommitWithoutPsi(@NotNull Document document) {
912     final UncommittedInfo prevInfo = clearUncommittedInfo(document);
913     if (prevInfo == null) {
914       return;
915     }
916
917     myUncommittedDocuments.remove(document);
918
919     if (!myProject.isInitialized() || myProject.isDisposed() || myProject.isDefault()) {
920       return;
921     }
922
923     VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
924     if (virtualFile != null) {
925       FileManager fileManager = getFileManager();
926       FileViewProvider viewProvider = fileManager.findCachedViewProvider(virtualFile);
927       if (viewProvider != null) {
928         // we can end up outside write action here if the document has forUseInNonAWTThread=true
929         ApplicationManager.getApplication().runWriteAction((ExternalChangeAction)() ->
930           ((AbstractFileViewProvider)viewProvider).onContentReload());
931       } else if (FileIndexFacade.getInstance(myProject).isInContent(virtualFile)) {
932         ApplicationManager.getApplication().runWriteAction((ExternalChangeAction)() ->
933           ((FileManagerImpl)fileManager).firePropertyChangedForUnloadedPsi());
934       }
935     }
936
937     runAfterCommitActions(document);
938   }
939
940   @Nullable
941   private UncommittedInfo clearUncommittedInfo(@NotNull Document document) {
942     UncommittedInfo info = myUncommittedInfos.remove(document);
943     if (info != null) {
944       getSmartPointerManager().updatePointers(document, info.myFrozen, info.myEvents);
945     }
946     return info;
947   }
948
949   private SmartPointerManagerImpl getSmartPointerManager() {
950     return (SmartPointerManagerImpl)SmartPointerManager.getInstance(myProject);
951   }
952
953   private boolean isRelevant(@NotNull VirtualFile virtualFile) {
954     return !myProject.isDisposed() && !virtualFile.getFileType().isBinary();
955   }
956
957   public static boolean checkConsistency(@NotNull PsiFile psiFile, @NotNull Document document) {
958     //todo hack
959     if (psiFile.getVirtualFile() == null) return true;
960
961     CharSequence editorText = document.getCharsSequence();
962     int documentLength = document.getTextLength();
963     if (psiFile.textMatches(editorText)) {
964       LOG.assertTrue(psiFile.getTextLength() == documentLength);
965       return true;
966     }
967
968     char[] fileText = psiFile.textToCharArray();
969     @SuppressWarnings("NonConstantStringShouldBeStringBuffer")
970     @NonNls String error = "File '" + psiFile.getName() + "' text mismatch after reparse. " +
971                            "File length=" + fileText.length + "; Doc length=" + documentLength + "\n";
972     int i = 0;
973     for (; i < documentLength; i++) {
974       if (i >= fileText.length) {
975         error += "editorText.length > psiText.length i=" + i + "\n";
976         break;
977       }
978       if (i >= editorText.length()) {
979         error += "editorText.length > psiText.length i=" + i + "\n";
980         break;
981       }
982       if (editorText.charAt(i) != fileText[i]) {
983         error += "first unequal char i=" + i + "\n";
984         break;
985       }
986     }
987     //error += "*********************************************" + "\n";
988     //if (i <= 500){
989     //  error += "Equal part:" + editorText.subSequence(0, i) + "\n";
990     //}
991     //else{
992     //  error += "Equal part start:\n" + editorText.subSequence(0, 200) + "\n";
993     //  error += "................................................" + "\n";
994     //  error += "................................................" + "\n";
995     //  error += "................................................" + "\n";
996     //  error += "Equal part end:\n" + editorText.subSequence(i - 200, i) + "\n";
997     //}
998     error += "*********************************************" + "\n";
999     error += "Editor Text tail:(" + (documentLength - i) + ")\n";// + editorText.subSequence(i, Math.min(i + 300, documentLength)) + "\n";
1000     error += "*********************************************" + "\n";
1001     error += "Psi Text tail:(" + (fileText.length - i) + ")\n";
1002     error += "*********************************************" + "\n";
1003
1004     if (document instanceof DocumentWindow) {
1005       error += "doc: '" + document.getText() + "'\n";
1006       error += "psi: '" + psiFile.getText() + "'\n";
1007       error += "ast: '" + psiFile.getNode().getText() + "'\n";
1008       error += psiFile.getLanguage() + "\n";
1009       PsiElement context = InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile);
1010       if (context != null) {
1011         error += "context: " + context + "; text: '" + context.getText() + "'\n";
1012         error += "context file: " + context.getContainingFile() + "\n";
1013       }
1014       error += "document window ranges: " + Arrays.asList(((DocumentWindow)document).getHostRanges()) + "\n";
1015     }
1016     LOG.error(error);
1017     //document.replaceString(0, documentLength, psiFile.getText());
1018     return false;
1019   }
1020
1021   @VisibleForTesting
1022   @TestOnly
1023   public void clearUncommittedDocuments() {
1024     myUncommittedInfos.clear();
1025     myUncommittedDocuments.clear();
1026     mySynchronizer.cleanupForNextTest();
1027   }
1028
1029   @TestOnly
1030   public void disableBackgroundCommit(@NotNull Disposable parentDisposable) {
1031     assert myPerformBackgroundCommit;
1032     myPerformBackgroundCommit = false;
1033     Disposer.register(parentDisposable, () -> myPerformBackgroundCommit = true);
1034   }
1035
1036   @Override
1037   public void dispose() {
1038     clearUncommittedDocuments();
1039   }
1040
1041   @NotNull
1042   public PsiToDocumentSynchronizer getSynchronizer() {
1043     return mySynchronizer;
1044   }
1045
1046   @SuppressWarnings("AssignmentToStaticFieldFromInstanceMethod")
1047   public void reparseFileFromText(@NotNull PsiFileImpl file) {
1048     ApplicationManager.getApplication().assertIsDispatchThread();
1049     if (isCommitInProgress()) throw new IllegalStateException("Re-entrant commit is not allowed");
1050
1051     FileElement node = file.calcTreeElement();
1052     CharSequence text = node.getChars();
1053     ourIsFullReparseInProgress = true;
1054     try {
1055       WriteAction.run(() -> {
1056         ProgressIndicator indicator = ProgressIndicatorProvider.getGlobalProgressIndicator();
1057         if (indicator == null) indicator = new EmptyProgressIndicator();
1058         DiffLog log = BlockSupportImpl.makeFullParse(file, node, text, indicator, text).log;
1059         log.doActualPsiChange(file);
1060         file.getViewProvider().contentsSynchronized();
1061       });
1062     }
1063     finally {
1064       ourIsFullReparseInProgress = false;
1065     }
1066   }
1067
1068   private static class UncommittedInfo {
1069     private final FrozenDocument myFrozen;
1070     private final ArrayList<DocumentEvent> myEvents = new ArrayList<>();
1071     private final ConcurrentMap<DocumentWindow, DocumentWindow> myFrozenWindows = ContainerUtil.newConcurrentMap();
1072
1073     private UncommittedInfo(@NotNull DocumentImpl original) {
1074       myFrozen = original.freeze();
1075     }
1076   }
1077
1078   @NotNull
1079   List<BooleanRunnable> reparseChangedInjectedFragments(@NotNull Document hostDocument,
1080                                                         @NotNull PsiFile hostPsiFile,
1081                                                         @NotNull TextRange range,
1082                                                         @NotNull ProgressIndicator indicator,
1083                                                         @NotNull ASTNode oldRoot,
1084                                                         @NotNull ASTNode newRoot) {
1085     return Collections.emptyList();
1086   }
1087
1088   @TestOnly
1089   public boolean isDefaultProject() {
1090     return myProject.isDefault();
1091   }
1092 }