fix "IDEA-221944 Deadlock on opening second project" and support preloading for proje...
[idea/community.git] / platform / core-impl / src / com / intellij / psi / impl / DocumentCommitThread.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.diagnostic.PluginException;
6 import com.intellij.lang.FileASTNode;
7 import com.intellij.openapi.Disposable;
8 import com.intellij.openapi.application.*;
9 import com.intellij.openapi.application.ex.ApplicationEx;
10 import com.intellij.openapi.components.ServiceManager;
11 import com.intellij.openapi.diagnostic.Logger;
12 import com.intellij.openapi.editor.Document;
13 import com.intellij.openapi.editor.ex.DocumentEx;
14 import com.intellij.openapi.progress.ProcessCanceledException;
15 import com.intellij.openapi.progress.ProgressIndicator;
16 import com.intellij.openapi.progress.ProgressManager;
17 import com.intellij.openapi.progress.util.StandardProgressIndicatorBase;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.util.*;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.openapi.vfs.VirtualFile;
22 import com.intellij.psi.*;
23 import com.intellij.psi.impl.source.PsiFileImpl;
24 import com.intellij.psi.impl.source.tree.FileElement;
25 import com.intellij.psi.text.BlockSupport;
26 import com.intellij.util.ExceptionUtil;
27 import com.intellij.util.SmartList;
28 import com.intellij.util.concurrency.AppExecutorUtil;
29 import com.intellij.util.concurrency.BoundedTaskExecutor;
30 import com.intellij.util.containers.ContainerUtil;
31 import com.intellij.util.containers.HashSetQueue;
32 import com.intellij.util.ui.UIUtil;
33 import org.jetbrains.annotations.NonNls;
34 import org.jetbrains.annotations.NotNull;
35 import org.jetbrains.annotations.Nullable;
36 import org.jetbrains.annotations.TestOnly;
37 import org.jetbrains.ide.PooledThreadExecutor;
38
39 import java.util.List;
40 import java.util.concurrent.ExecutionException;
41 import java.util.concurrent.ExecutorService;
42 import java.util.concurrent.TimeUnit;
43 import java.util.concurrent.TimeoutException;
44 import java.util.concurrent.locks.Lock;
45 import java.util.concurrent.locks.ReentrantLock;
46
47 public final class DocumentCommitThread implements Runnable, Disposable, DocumentCommitProcessor {
48   private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.DocumentCommitThread");
49   private static final String SYNC_COMMIT_REASON = "Sync commit";
50
51   private final ExecutorService executor = AppExecutorUtil.createBoundedApplicationPoolExecutor("Document Committing Pool", PooledThreadExecutor.INSTANCE, 1, this);
52   private final Object lock = new Object();
53   private final HashSetQueue<CommitTask> documentsToCommit = new HashSetQueue<>();      // guarded by lock
54   private final HashSetQueue<CommitTask> documentsToApplyInEDT = new HashSetQueue<>();  // guarded by lock
55   private volatile boolean isDisposed;
56   private CommitTask currentTask; // guarded by lock
57   private boolean myEnabled; // true if we can do commits. set to false temporarily during the write action.  guarded by lock
58
59   static DocumentCommitThread getInstance() {
60     return (DocumentCommitThread)ServiceManager.getService(DocumentCommitProcessor.class);
61   }
62
63   DocumentCommitThread() {
64     ApplicationEx application = (ApplicationEx)ApplicationManager.getApplication();
65     // install listener in EDT to avoid missing events in case we are inside write action right now
66     application.invokeLater(() -> {
67       if (application.isDisposed()) return;
68       assert !application.isWriteAccessAllowed() || application.isUnitTestMode(); // crazy stuff happens in tests, e.g. UIUtil.dispatchInvocationEvents() inside write action
69       application.addApplicationListener(new ApplicationListener() {
70         @Override
71         public void beforeWriteActionStart(@NotNull Object action) {
72           disable("Write action started: " + action);
73         }
74
75         @Override
76         public void afterWriteActionFinished(@NotNull Object action) {
77           // crazy things happen when running tests, like starting write action in one thread but firing its end in the other
78           enable("Write action finished: " + action);
79         }
80       }, this);
81
82       enable("Listener installed, started");
83     });
84   }
85
86   @Override
87   public void dispose() {
88     isDisposed = true;
89     synchronized (lock) {
90       documentsToCommit.clear();
91     }
92     cancel("Stop thread", false);
93   }
94
95   private void disable(@NonNls @NotNull Object reason) {
96     // write action has just started, all commits are useless
97     synchronized (lock) {
98       cancel(reason, true);
99       myEnabled = false;
100     }
101     log(null, "disabled", null, reason);
102   }
103
104   private void enable(@NonNls @NotNull Object reason) {
105     synchronized (lock) {
106       myEnabled = true;
107       wakeUpQueue();
108     }
109     log(null, "enabled", null, reason);
110   }
111
112   // under lock
113   private void wakeUpQueue() {
114     if (!isDisposed && !documentsToCommit.isEmpty()) {
115       executor.execute(this);
116     }
117   }
118
119   private void cancel(@NonNls @NotNull Object reason, boolean canReQueue) {
120     startNewTask(null, reason, canReQueue);
121   }
122
123   @Override
124   public void commitAsynchronously(@NotNull final Project project,
125                                    @NotNull final Document document,
126                                    @NonNls @NotNull Object reason,
127                                    @Nullable TransactionId context) {
128     assert !isDisposed : "already disposed";
129
130     if (!project.isInitialized()) return;
131     PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
132     PsiFile psiFile = documentManager.getCachedPsiFile(document);
133     if (psiFile == null || psiFile instanceof PsiCompiledElement) return;
134     doQueue(project, document, reason, context, documentManager.getLastCommittedText(document));
135   }
136
137   private void doQueue(@NotNull Project project,
138                        @NotNull Document document,
139                        @NotNull Object reason,
140                        @Nullable TransactionId context,
141                        @NotNull CharSequence lastCommittedText) {
142     synchronized (lock) {
143       if (!project.isInitialized()) return;  // check the project is disposed under lock.
144       CommitTask newTask = createNewTaskAndCancelSimilar(project, document, reason, context, lastCommittedText, false);
145
146       documentsToCommit.offer(newTask);
147       log(project, "Queued", newTask, reason);
148
149       wakeUpQueue();
150     }
151   }
152
153   @NotNull
154   private CommitTask createNewTaskAndCancelSimilar(@NotNull Project project,
155                                                    @NotNull Document document,
156                                                    @NotNull Object reason,
157                                                    @Nullable TransactionId context,
158                                                    @NotNull CharSequence lastCommittedText, boolean canReQueue) {
159     synchronized (lock) {
160       CommitTask newTask = new CommitTask(project, document, createProgressIndicator(), reason, context, lastCommittedText);
161       cancelAndRemoveFromDocsToCommit(newTask, reason, canReQueue);
162       cancelAndRemoveCurrentTask(newTask, reason, canReQueue);
163       cancelAndRemoveFromDocsToApplyInEDT(newTask, reason, canReQueue);
164
165       return newTask;
166     }
167   }
168
169   @SuppressWarnings("unused")
170   private void log(Project project, @NonNls String msg, @Nullable CommitTask task, @NonNls Object... args) {
171     //System.out.println(msg + "; task: "+task + "; args: "+StringUtil.first(Arrays.toString(args), 80, true));
172   }
173
174
175   // cancels all pending commits
176   @TestOnly // under lock
177   private void cancelAll() {
178     String reason = "Cancel all in tests";
179     cancel(reason, false);
180     for (CommitTask commitTask : documentsToCommit) {
181       commitTask.cancel(reason, false);
182       log(commitTask.project, "Removed from background queue", commitTask);
183     }
184     documentsToCommit.clear();
185     for (CommitTask commitTask : documentsToApplyInEDT) {
186       commitTask.cancel(reason, false);
187       log(commitTask.project, "Removed from EDT apply queue (sync commit called)", commitTask);
188     }
189     documentsToApplyInEDT.clear();
190     CommitTask task = currentTask;
191     if (task != null) {
192       cancelAndRemoveFromDocsToCommit(task, reason, false);
193     }
194     cancel("Sync commit intervened", false);
195     ((BoundedTaskExecutor)executor).clearAndCancelAll();
196   }
197
198   @TestOnly
199   void clearQueue() {
200     synchronized (lock) {
201       cancelAll();
202       wakeUpQueue();
203     }
204   }
205
206   private void cancelAndRemoveCurrentTask(@NotNull CommitTask newTask, @NotNull Object reason, boolean canReQueue) {
207     CommitTask currentTask = this.currentTask;
208     if (newTask.equals(currentTask)) {
209       cancelAndRemoveFromDocsToCommit(currentTask, reason, canReQueue);
210       cancel(reason, canReQueue);
211     }
212   }
213
214   private void cancelAndRemoveFromDocsToApplyInEDT(@NotNull CommitTask newTask, @NotNull Object reason, boolean canReQueue) {
215     boolean removed = cancelAndRemoveFromQueue(newTask, documentsToApplyInEDT, reason, canReQueue);
216     if (removed) {
217       log(newTask.project, "Removed from EDT apply queue", newTask);
218     }
219   }
220
221   private void cancelAndRemoveFromDocsToCommit(@NotNull final CommitTask newTask, @NotNull Object reason, boolean canReQueue) {
222     boolean removed = cancelAndRemoveFromQueue(newTask, documentsToCommit, reason, canReQueue);
223     if (removed) {
224       log(newTask.project, "Removed from background queue", newTask);
225     }
226   }
227
228   private boolean cancelAndRemoveFromQueue(@NotNull CommitTask newTask, @NotNull HashSetQueue<CommitTask> queue, @NotNull Object reason, boolean canReQueue) {
229     CommitTask queuedTask = queue.find(newTask);
230     if (queuedTask != null) {
231       assert queuedTask != newTask;
232       queuedTask.cancel(reason, canReQueue);
233     }
234     return queue.remove(newTask);
235   }
236
237   @Override
238   public void run() {
239     while (!isDisposed) {
240       try {
241         if (!pollQueue()) break;
242       }
243       catch(Throwable e) {
244         LOG.error(e);
245       }
246     }
247   }
248
249   // returns true if queue changed
250   private boolean pollQueue() {
251     assert !ApplicationManager.getApplication().isDispatchThread() : Thread.currentThread();
252     CommitTask task;
253     synchronized (lock) {
254       if (!myEnabled || (task = documentsToCommit.poll()) == null) {
255         return false;
256       }
257
258       Document document = task.getDocument();
259       Project project = task.project;
260
261       if (project.isDisposed() || !((PsiDocumentManagerBase)PsiDocumentManager.getInstance(project)).isInUncommittedSet(document)) {
262         log(project, "Abandon and proceed to next", task);
263         return true;
264       }
265
266       if (task.isCanceled() || task.dead) {
267         return true; // document has been marked as removed, e.g. by synchronous commit
268       }
269
270       startNewTask(task, "Pulled new task", true);
271
272       documentsToApplyInEDT.add(task);
273     }
274
275     boolean success = false;
276     Object failureReason = null;
277     try {
278       if (!task.isCanceled()) {
279         final CommitTask commitTask = task;
280         final Ref<Pair<Runnable, Object>> result = new Ref<>();
281         ProgressManager.getInstance().executeProcessUnderProgress(() -> result.set(commitUnderProgress(commitTask, false)), task.indicator);
282         final Runnable finishRunnable = result.get().first;
283         success = finishRunnable != null;
284         failureReason = result.get().second;
285
286         if (success) {
287           assert !ApplicationManager.getApplication().isDispatchThread();
288           TransactionGuardImpl guard = (TransactionGuardImpl)TransactionGuard.getInstance();
289           guard.submitTransaction(task.project, task.myCreationContext, finishRunnable);
290         }
291       }
292     }
293     catch (ProcessCanceledException e) {
294       cancel(e + " (indicator cancel reason: "+((UserDataHolder)task.indicator).getUserData(CANCEL_REASON)+")", true); // leave queue unchanged
295       success = false;
296       failureReason = e;
297     }
298     catch (Throwable e) {
299       LOG.error(e); // unrecoverable
300       cancel(e, false);
301       failureReason = ExceptionUtil.getThrowableText(e);
302     }
303
304     if (!success) {
305       Project project = task.project;
306       Document document = task.document;
307       String reQueuedReason = "re-added on failure: " + failureReason;
308       ReadAction.run(() -> {
309         if (project.isDisposed()) return;
310         PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
311         if (documentManager.isCommitted(document)) return; // sync commit hasn't intervened
312         CharSequence lastCommittedText = documentManager.getLastCommittedText(document);
313         PsiFile file = documentManager.getPsiFile(document);
314         List<Pair<PsiFileImpl, FileASTNode>> oldFileNodes = file == null ? null : getAllFileNodes(file);
315         if (oldFileNodes != null) {
316           // somebody's told us explicitly not to beat the dead horse again,
317           // e.g. when submitted the same document with the different transaction context
318           if (task.dead) return;
319
320           doQueue(project, document, reQueuedReason, task.myCreationContext, lastCommittedText);
321         }
322       });
323     }
324     synchronized (lock) {
325       currentTask = null; // do not cancel, it's being invokeLatered
326     }
327
328     return true;
329   }
330
331   @Override
332   public void commitSynchronously(@NotNull Document document, @NotNull Project project, @NotNull PsiFile psiFile) {
333     assert !isDisposed;
334
335     if (!project.isInitialized() && !project.isDefault()) {
336       @NonNls String s = project + "; Disposed: "+project.isDisposed()+"; Open: "+project.isOpen();
337       try {
338         Disposer.dispose(project);
339       }
340       catch (Throwable ignored) {
341         // do not fill log with endless exceptions
342       }
343       throw new RuntimeException(s);
344     }
345
346     Lock documentLock = getDocumentLock(document);
347
348     CommitTask task;
349     synchronized (lock) {
350       // synchronized to ensure no new similar tasks can start before we hold the document's lock
351       task = createNewTaskAndCancelSimilar(project, document, SYNC_COMMIT_REASON, TransactionGuard.getInstance().getContextTransaction(),
352                                            PsiDocumentManager.getInstance(project).getLastCommittedText(document), true);
353     }
354
355     documentLock.lock();
356     try {
357       assert !task.isCanceled();
358       Pair<Runnable, Object> result = commitUnderProgress(task, true);
359       Runnable finish = result.first;
360       log(project, "Committed sync", task, finish, task.indicator);
361       assert finish != null;
362
363       finish.run();
364     }
365     finally {
366       documentLock.unlock();
367     }
368
369     // will wake itself up on write action end
370   }
371
372   @NotNull
373   private static List<Pair<PsiFileImpl, FileASTNode>> getAllFileNodes(@NotNull PsiFile file) {
374     if (!file.isValid()) {
375       throw new PsiInvalidElementAccessException(file, "File " + file + " is invalid, can't commit");
376     }
377     if (file instanceof PsiCompiledFile) {
378       throw new IllegalArgumentException("Can't commit ClsFile: "+file);
379     }
380
381     return ContainerUtil.map(file.getViewProvider().getAllFiles(), root -> Pair.create((PsiFileImpl)root, root.getNode()));
382   }
383
384   @NotNull
385   private static ProgressIndicator createProgressIndicator() {
386     return new StandardProgressIndicatorBase();
387   }
388
389   private void startNewTask(@Nullable CommitTask task, @NotNull Object reason, boolean canReQueue) {
390     synchronized (lock) { // sync to prevent overwriting
391       CommitTask cur = currentTask;
392       if (cur != null) {
393         cur.cancel(reason, canReQueue);
394       }
395       currentTask = task;
396     }
397   }
398
399   // returns (finish commit Runnable (to be invoked later in EDT), null) on success or (null, failure reason) on failure
400   @NotNull
401   private Pair<Runnable, Object> commitUnderProgress(@NotNull final CommitTask task, final boolean synchronously) {
402     if (synchronously) {
403       assert !task.isCanceled();
404     }
405
406     final Document document = task.getDocument();
407     final Project project = task.project;
408     final PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project);
409     final List<BooleanRunnable> finishProcessors = new SmartList<>();
410     List<BooleanRunnable> reparseInjectedProcessors = new SmartList<>();
411     Runnable runnable = () -> {
412       ApplicationManager.getApplication().assertReadAccessAllowed();
413       if (project.isDisposed()) return;
414
415       Lock lock = getDocumentLock(document);
416       if (!lock.tryLock()) {
417         task.cancel("Can't obtain document lock", true);
418         return;
419       }
420
421       boolean canceled = false;
422       try {
423         if (documentManager.isCommitted(document)) return;
424
425         if (!task.isStillValid()) {
426           canceled = true;
427           return;
428         }
429
430         FileViewProvider viewProvider = documentManager.getCachedViewProvider(document);
431         if (viewProvider == null) {
432           finishProcessors.add(handleCommitWithoutPsi(documentManager, task));
433           return;
434         }
435
436         for (PsiFile file : viewProvider.getAllFiles()) {
437           FileASTNode oldFileNode = file.getNode();
438           ProperTextRange changedPsiRange = ChangedPsiRangeUtil
439             .getChangedPsiRange(file, task.document, task.myLastCommittedText, document.getImmutableCharSequence());
440           if (changedPsiRange != null) {
441             BooleanRunnable finishProcessor = doCommit(task, file, oldFileNode, changedPsiRange, reparseInjectedProcessors);
442             finishProcessors.add(finishProcessor);
443           }
444         }
445       }
446       finally {
447         lock.unlock();
448         if (canceled) {
449           task.cancel("Task invalidated", false);
450         }
451       }
452     };
453
454     ApplicationEx app = (ApplicationEx)ApplicationManager.getApplication();
455     if (!app.tryRunReadAction(runnable)) {
456       log(project, "Could not start read action", task, app.isReadAccessAllowed(), Thread.currentThread());
457       return new Pair<>(null, "Could not start read action");
458     }
459
460     boolean canceled = task.isCanceled();
461     assert !synchronously || !canceled;
462     if (canceled) {
463       return new Pair<>(null, "Indicator was canceled");
464     }
465
466     Runnable result = createFinishCommitInEDTRunnable(task, synchronously, finishProcessors, reparseInjectedProcessors);
467     return Pair.create(result, null);
468   }
469
470   @NotNull
471   private Runnable createFinishCommitInEDTRunnable(@NotNull final CommitTask task,
472                                                    final boolean synchronously,
473                                                    @NotNull List<? extends BooleanRunnable> finishProcessors,
474                                                    @NotNull List<? extends BooleanRunnable> reparseInjectedProcessors) {
475     return () -> {
476       ApplicationManager.getApplication().assertIsDispatchThread();
477       Document document = task.getDocument();
478       Project project = task.project;
479       PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project);
480       boolean committed = project.isDisposed() || documentManager.isCommitted(document);
481       synchronized (lock) {
482         documentsToApplyInEDT.remove(task);
483         if (committed) {
484           log(project, "Marked as already committed in EDT apply queue, return", task);
485           return;
486         }
487       }
488
489       boolean changeStillValid = task.isStillValid();
490       boolean success = changeStillValid && documentManager.finishCommit(document, finishProcessors, reparseInjectedProcessors,
491                                                                          synchronously, task.reason);
492       if (synchronously) {
493         assert success;
494       }
495       if (!changeStillValid) {
496         log(project, "document changed; ignore", task);
497         return;
498       }
499       if (synchronously || success) {
500         assert !documentManager.isInUncommittedSet(document);
501       }
502       if (success) {
503         log(project, "Commit finished", task);
504       }
505       else {
506         // add document back to the queue
507         commitAsynchronously(project, document, "Re-added back", task.myCreationContext);
508       }
509     };
510   }
511
512   @NotNull
513   private BooleanRunnable handleCommitWithoutPsi(@NotNull final PsiDocumentManagerBase documentManager,
514                                                      @NotNull final CommitTask task) {
515     return () -> {
516       log(task.project, "Finishing without PSI", task);
517       Document document = task.getDocument();
518       if (!task.isStillValid() || documentManager.getCachedViewProvider(document) != null) {
519         return false;
520       }
521
522       documentManager.handleCommitWithoutPsi(document);
523       return true;
524     };
525   }
526
527   boolean isEnabled() {
528     synchronized (lock) {
529       return myEnabled;
530     }
531   }
532
533   @Override
534   public String toString() {
535     return "Document commit thread; application: "+ApplicationManager.getApplication()+"; isDisposed: "+isDisposed+"; myEnabled: "+isEnabled();
536   }
537
538   @TestOnly
539   @VisibleForTesting
540   // waits for all tasks in 'documentsToCommit' queue to be finished, i.e. wait
541   // - for 'commitUnderProgress' executed for all documents from there,
542   // - for (potentially) a number of documents added to 'documentsToApplyInEDT'
543   // - for these apply tasks (created in 'createFinishCommitInEDTRunnable') executed in EDT
544   // NB: failures applying EDT tasks are not handled - i.e. failed documents are added back to the queue and the method returns
545   public void waitForAllCommits(long timeout, @NotNull TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException {
546     ApplicationManager.getApplication().assertIsDispatchThread();
547     assert !ApplicationManager.getApplication().isWriteAccessAllowed();
548
549     ((BoundedTaskExecutor)executor).waitAllTasksExecuted(timeout, timeUnit);
550     UIUtil.dispatchAllInvocationEvents();
551     disable("waitForAllCommits() called in the tearDown()");
552   }
553
554   private static final Key<Object> CANCEL_REASON = Key.create("CANCEL_REASON");
555   private class CommitTask {
556     @NotNull private final Document document;
557     @NotNull final Project project;
558     private final int modificationSequence; // store initial document modification sequence here to check if it changed later before commit in EDT
559
560     // when queued it's not started
561     // when dequeued it's started
562     // when failed it's canceled
563     @NotNull final ProgressIndicator indicator; // progress to commit this doc under.
564     @NotNull final Object reason;
565     @Nullable final TransactionId myCreationContext;
566     private final CharSequence myLastCommittedText;
567     private volatile boolean dead; // the task was explicitly removed from the queue; no attempts to re-queue should be made
568
569     CommitTask(@NotNull final Project project,
570                @NotNull final Document document,
571                @NotNull ProgressIndicator indicator,
572                @NotNull Object reason,
573                @Nullable TransactionId context,
574                @NotNull CharSequence lastCommittedText) {
575       this.document = document;
576       this.project = project;
577       this.indicator = indicator;
578       this.reason = reason;
579       myCreationContext = context;
580       myLastCommittedText = lastCommittedText;
581       modificationSequence = ((DocumentEx)document).getModificationSequence();
582     }
583
584     @NonNls
585     @Override
586     public String toString() {
587       Document document = getDocument();
588       String indicatorInfo = isCanceled() ? " (Canceled: " + ((UserDataHolder)indicator).getUserData(CANCEL_REASON) + ")" : "";
589       String removedInfo = dead ? " (dead)" : "";
590       String reasonInfo = " task reason: " + StringUtil.first(String.valueOf(reason), 180, true) +
591                           (isStillValid() ? "" : "; changed: old seq=" + modificationSequence + ", new seq=" + ((DocumentEx)document).getModificationSequence());
592       String contextInfo = " Context: "+myCreationContext;
593       return System.identityHashCode(this)+"; " + indicatorInfo + removedInfo + contextInfo + reasonInfo;
594     }
595
596     @Override
597     public boolean equals(Object o) {
598       if (this == o) return true;
599       if (!(o instanceof CommitTask)) return false;
600
601       CommitTask task = (CommitTask)o;
602
603       return Comparing.equal(getDocument(),task.getDocument()) && project.equals(task.project);
604     }
605
606     @Override
607     public int hashCode() {
608       int result = getDocument().hashCode();
609       result = 31 * result + project.hashCode();
610       return result;
611     }
612
613     boolean isStillValid() {
614       Document document = getDocument();
615       return ((DocumentEx)document).getModificationSequence() == modificationSequence;
616     }
617
618     private void cancel(@NotNull Object reason, boolean canReQueue) {
619       dead |= !canReQueue; // set the flag before cancelling indicator
620       if (!isCanceled()) {
621         log(project, "cancel", this, reason);
622
623         indicator.cancel();
624         ((UserDataHolder)indicator).putUserData(CANCEL_REASON, reason);
625
626         synchronized (lock) {
627           documentsToCommit.remove(this);
628           documentsToApplyInEDT.remove(this);
629         }
630       }
631     }
632
633     @NotNull
634     Document getDocument() {
635       return document;
636     }
637
638     private boolean isCanceled() {
639       return indicator.isCanceled();
640     }
641   }
642
643   // returns runnable to execute under write action in AWT to finish the commit, updates "outChangedRange"
644   @NotNull
645   private static BooleanRunnable doCommit(@NotNull final CommitTask task,
646                                           @NotNull final PsiFile file,
647                                           @NotNull final FileASTNode oldFileNode,
648                                           @NotNull ProperTextRange changedPsiRange,
649                                           @NotNull List<? super BooleanRunnable> outReparseInjectedProcessors) {
650     Document document = task.getDocument();
651     final CharSequence newDocumentText = document.getImmutableCharSequence();
652
653     final Boolean data = document.getUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY);
654     if (data != null) {
655       document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
656       file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, data);
657     }
658
659     PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(task.project);
660
661     DiffLog diffLog;
662     try (
663       BlockSupportImpl.ReparseResult result =
664         BlockSupportImpl.reparse(file, oldFileNode, changedPsiRange, newDocumentText, task.indicator, task.myLastCommittedText)) {
665       diffLog = result.log;
666
667
668       List<BooleanRunnable> injectedRunnables =
669         documentManager.reparseChangedInjectedFragments(document, file, changedPsiRange, task.indicator, result.oldRoot, result.newRoot);
670       outReparseInjectedProcessors.addAll(injectedRunnables);
671     }
672     catch (ProcessCanceledException e) {
673       throw e;
674     }
675     catch (Throwable e) {
676       LOG.error(e);
677       return () -> {
678         documentManager.forceReload(file.getViewProvider().getVirtualFile(), file.getViewProvider());
679         return true;
680       };
681     }
682
683     return () -> {
684       FileViewProvider viewProvider = file.getViewProvider();
685       Document document1 = task.getDocument();
686       if (!task.isStillValid() ||
687           ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(file.getProject())).getCachedViewProvider(document1) != viewProvider) {
688         return false; // optimistic locking failed
689       }
690
691       if (!ApplicationManager.getApplication().isWriteAccessAllowed()) {
692         VirtualFile vFile = viewProvider.getVirtualFile();
693         LOG.error("Write action expected" +
694                   "; document=" + document1 +
695                   "; file=" + file + " of " + file.getClass() +
696                   "; file.valid=" + file.isValid() +
697                   "; file.eventSystemEnabled=" + viewProvider.isEventSystemEnabled() +
698                   "; viewProvider=" + viewProvider + " of " + viewProvider.getClass() +
699                   "; language=" + file.getLanguage() +
700                   "; vFile=" + vFile + " of " + vFile.getClass() +
701                   "; free-threaded=" + AbstractFileViewProvider.isFreeThreaded(viewProvider));
702       }
703
704       diffLog.doActualPsiChange(file);
705
706       assertAfterCommit(document1, file, (FileElement)oldFileNode);
707
708       return true;
709     };
710   }
711
712   private static void assertAfterCommit(@NotNull Document document, @NotNull final PsiFile file, @NotNull FileElement oldFileNode) {
713     if (oldFileNode.getTextLength() != document.getTextLength()) {
714       final String documentText = document.getText();
715       String fileText = file.getText();
716       boolean sameText = Comparing.equal(fileText, documentText);
717       String errorMessage = "commitDocument() left PSI inconsistent: " + DebugUtil.diagnosePsiDocumentInconsistency(file, document) +
718                             "; node.length=" + oldFileNode.getTextLength() +
719                             "; doc.text" + (sameText ? "==" : "!=") + "file.text" +
720                             "; file name:" + file.getName() +
721                             "; type:" + file.getFileType() +
722                             "; lang:" + file.getLanguage();
723       PluginException.logPluginError(LOG, errorMessage, null, file.getLanguage().getClass());
724
725       file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
726       try {
727         BlockSupport blockSupport = BlockSupport.getInstance(file.getProject());
728         final DiffLog diffLog = blockSupport.reparseRange(file, file.getNode(), new TextRange(0, documentText.length()), documentText, createProgressIndicator(),
729                                                           oldFileNode.getText());
730         diffLog.doActualPsiChange(file);
731
732         if (oldFileNode.getTextLength() != document.getTextLength()) {
733           PluginException.logPluginError(LOG, "PSI is broken beyond repair in: " + file, null, file.getLanguage().getClass());
734         }
735       }
736       finally {
737         file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
738       }
739     }
740   }
741
742   /**
743    * @return an internal lock object to prevent read & write phases of commit from running simultaneously for free-threaded PSI
744    */
745   private static Lock getDocumentLock(Document document) {
746     Lock lock = document.getUserData(DOCUMENT_LOCK);
747     return lock != null ? lock : ((UserDataHolderEx)document).putUserDataIfAbsent(DOCUMENT_LOCK, new ReentrantLock());
748   }
749   private static final Key<Lock> DOCUMENT_LOCK = Key.create("DOCUMENT_LOCK");
750
751   void cancelTasksOnProjectDispose(@NotNull final Project project) {
752     synchronized (lock) {
753       cancelTasksOnProjectDispose(project, documentsToCommit);
754       cancelTasksOnProjectDispose(project, documentsToApplyInEDT);
755     }
756   }
757
758   private void cancelTasksOnProjectDispose(@NotNull Project project, @NotNull HashSetQueue<CommitTask> queue) {
759     for (HashSetQueue.PositionalIterator<CommitTask> iterator = queue.iterator(); iterator.hasNext(); ) {
760       CommitTask commitTask = iterator.next();
761       if (commitTask.project == project) {
762         iterator.remove();
763         commitTask.cancel("project is disposed", false);
764       }
765     }
766   }
767 }