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