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