d4d7c63da06dbbbfb00e8723cafb311c5b0a34e2
[idea/community.git] / platform / core-impl / src / com / intellij / psi / impl / DocumentCommitThread.java
1 // Copyright 2000-2018 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 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 final ApplicationEx myApplication;
56   private volatile boolean isDisposed;
57   private CommitTask currentTask; // guarded by lock
58   private boolean myEnabled; // true if we can do commits. set to false temporarily during the write action.  guarded by lock
59
60   static DocumentCommitThread getInstance() {
61     return (DocumentCommitThread)ServiceManager.getService(DocumentCommitProcessor.class);
62   }
63   DocumentCommitThread(final ApplicationEx application) {
64     myApplication = application;
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 !myApplication.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 !myApplication.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       myApplication.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     if (!myApplication.tryRunReadAction(runnable)) {
454       log(project, "Could not start read action", task, myApplication.isReadAccessAllowed(), Thread.currentThread());
455       return new Pair<>(null, "Could not start read action");
456     }
457
458     boolean canceled = task.isCanceled();
459     assert !synchronously || !canceled;
460     if (canceled) {
461       return new Pair<>(null, "Indicator was canceled");
462     }
463
464     Runnable result = createFinishCommitInEDTRunnable(task, synchronously, finishProcessors, reparseInjectedProcessors);
465     return Pair.create(result, null);
466   }
467
468   @NotNull
469   private Runnable createFinishCommitInEDTRunnable(@NotNull final CommitTask task,
470                                                    final boolean synchronously,
471                                                    @NotNull List<? extends BooleanRunnable> finishProcessors,
472                                                    @NotNull List<? extends BooleanRunnable> reparseInjectedProcessors) {
473     return () -> {
474       myApplication.assertIsDispatchThread();
475       Document document = task.getDocument();
476       Project project = task.project;
477       PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(project);
478       boolean committed = project.isDisposed() || documentManager.isCommitted(document);
479       synchronized (lock) {
480         documentsToApplyInEDT.remove(task);
481         if (committed) {
482           log(project, "Marked as already committed in EDT apply queue, return", task);
483           return;
484         }
485       }
486
487       boolean changeStillValid = task.isStillValid();
488       boolean success = changeStillValid && documentManager.finishCommit(document, finishProcessors, reparseInjectedProcessors,
489                                                                          synchronously, task.reason);
490       if (synchronously) {
491         assert success;
492       }
493       if (!changeStillValid) {
494         log(project, "document changed; ignore", task);
495         return;
496       }
497       if (synchronously || success) {
498         assert !documentManager.isInUncommittedSet(document);
499       }
500       if (success) {
501         log(project, "Commit finished", task);
502       }
503       else {
504         // add document back to the queue
505         commitAsynchronously(project, document, "Re-added back", task.myCreationContext);
506       }
507     };
508   }
509
510   @NotNull
511   private BooleanRunnable handleCommitWithoutPsi(@NotNull final PsiDocumentManagerBase documentManager,
512                                                      @NotNull final CommitTask task) {
513     return () -> {
514       log(task.project, "Finishing without PSI", task);
515       Document document = task.getDocument();
516       if (!task.isStillValid() || documentManager.getCachedViewProvider(document) != null) {
517         return false;
518       }
519
520       documentManager.handleCommitWithoutPsi(document);
521       return true;
522     };
523   }
524
525   boolean isEnabled() {
526     synchronized (lock) {
527       return myEnabled;
528     }
529   }
530
531   @Override
532   public String toString() {
533     return "Document commit thread; application: "+myApplication+"; isDisposed: "+isDisposed+"; myEnabled: "+isEnabled();
534   }
535
536   @TestOnly
537   @VisibleForTesting
538   // waits for all tasks in 'documentsToCommit' queue to be finished, i.e. wait
539   // - for 'commitUnderProgress' executed for all documents from there,
540   // - for (potentially) a number of documents added to 'documentsToApplyInEDT'
541   // - for these apply tasks (created in 'createFinishCommitInEDTRunnable') executed in EDT
542   // NB: failures applying EDT tasks are not handled - i.e. failed documents are added back to the queue and the method returns
543   public void waitForAllCommits(long timeout, @NotNull TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException {
544     ApplicationManager.getApplication().assertIsDispatchThread();
545     assert !ApplicationManager.getApplication().isWriteAccessAllowed();
546
547     ((BoundedTaskExecutor)executor).waitAllTasksExecuted(timeout, timeUnit);
548     UIUtil.dispatchAllInvocationEvents();
549     disable("waitForAllCommits() called in the tearDown()");
550   }
551
552   private static final Key<Object> CANCEL_REASON = Key.create("CANCEL_REASON");
553   private class CommitTask {
554     @NotNull private final Document document;
555     @NotNull final Project project;
556     private final int modificationSequence; // store initial document modification sequence here to check if it changed later before commit in EDT
557
558     // when queued it's not started
559     // when dequeued it's started
560     // when failed it's canceled
561     @NotNull final ProgressIndicator indicator; // progress to commit this doc under.
562     @NotNull final Object reason;
563     @Nullable final TransactionId myCreationContext;
564     private final CharSequence myLastCommittedText;
565     private volatile boolean dead; // the task was explicitly removed from the queue; no attempts to re-queue should be made
566
567     CommitTask(@NotNull final Project project,
568                @NotNull final Document document,
569                @NotNull ProgressIndicator indicator,
570                @NotNull Object reason,
571                @Nullable TransactionId context,
572                @NotNull CharSequence lastCommittedText) {
573       this.document = document;
574       this.project = project;
575       this.indicator = indicator;
576       this.reason = reason;
577       myCreationContext = context;
578       myLastCommittedText = lastCommittedText;
579       modificationSequence = ((DocumentEx)document).getModificationSequence();
580     }
581
582     @NonNls
583     @Override
584     public String toString() {
585       Document document = getDocument();
586       String indicatorInfo = isCanceled() ? " (Canceled: " + ((UserDataHolder)indicator).getUserData(CANCEL_REASON) + ")" : "";
587       String removedInfo = dead ? " (dead)" : "";
588       String reasonInfo = " task reason: " + StringUtil.first(String.valueOf(reason), 180, true) +
589                           (isStillValid() ? "" : "; changed: old seq=" + modificationSequence + ", new seq=" + ((DocumentEx)document).getModificationSequence());
590       String contextInfo = " Context: "+myCreationContext;
591       return System.identityHashCode(this)+"; " + indicatorInfo + removedInfo + contextInfo + reasonInfo;
592     }
593
594     @Override
595     public boolean equals(Object o) {
596       if (this == o) return true;
597       if (!(o instanceof CommitTask)) return false;
598
599       CommitTask task = (CommitTask)o;
600
601       return Comparing.equal(getDocument(),task.getDocument()) && project.equals(task.project);
602     }
603
604     @Override
605     public int hashCode() {
606       int result = getDocument().hashCode();
607       result = 31 * result + project.hashCode();
608       return result;
609     }
610
611     boolean isStillValid() {
612       Document document = getDocument();
613       return ((DocumentEx)document).getModificationSequence() == modificationSequence;
614     }
615
616     private void cancel(@NotNull Object reason, boolean canReQueue) {
617       dead |= !canReQueue; // set the flag before cancelling indicator
618       if (!isCanceled()) {
619         log(project, "cancel", this, reason);
620
621         indicator.cancel();
622         ((UserDataHolder)indicator).putUserData(CANCEL_REASON, reason);
623
624         synchronized (lock) {
625           documentsToCommit.remove(this);
626           documentsToApplyInEDT.remove(this);
627         }
628       }
629     }
630
631     @NotNull
632     Document getDocument() {
633       return document;
634     }
635
636     private boolean isCanceled() {
637       return indicator.isCanceled();
638     }
639   }
640
641   // returns runnable to execute under write action in AWT to finish the commit, updates "outChangedRange"
642   @NotNull
643   private static BooleanRunnable doCommit(@NotNull final CommitTask task,
644                                           @NotNull final PsiFile file,
645                                           @NotNull final FileASTNode oldFileNode,
646                                           @NotNull ProperTextRange changedPsiRange,
647                                           @NotNull List<? super BooleanRunnable> outReparseInjectedProcessors) {
648     Document document = task.getDocument();
649     final CharSequence newDocumentText = document.getImmutableCharSequence();
650
651     final Boolean data = document.getUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY);
652     if (data != null) {
653       document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
654       file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, data);
655     }
656
657     PsiDocumentManagerBase documentManager = (PsiDocumentManagerBase)PsiDocumentManager.getInstance(task.project);
658
659     DiffLog diffLog;
660     try (
661       BlockSupportImpl.ReparseResult result =
662         BlockSupportImpl.reparse(file, oldFileNode, changedPsiRange, newDocumentText, task.indicator, task.myLastCommittedText)) {
663       diffLog = result.log;
664
665
666       List<BooleanRunnable> injectedRunnables =
667         documentManager.reparseChangedInjectedFragments(document, file, changedPsiRange, task.indicator, result.oldRoot, result.newRoot);
668       outReparseInjectedProcessors.addAll(injectedRunnables);
669     }
670     catch (ProcessCanceledException e) {
671       throw e;
672     }
673     catch (Throwable e) {
674       LOG.error(e);
675       return () -> {
676         documentManager.forceReload(file.getViewProvider().getVirtualFile(), file.getViewProvider());
677         return true;
678       };
679     }
680
681     return () -> {
682       FileViewProvider viewProvider = file.getViewProvider();
683       Document document1 = task.getDocument();
684       if (!task.isStillValid() ||
685           ((PsiDocumentManagerBase)PsiDocumentManager.getInstance(file.getProject())).getCachedViewProvider(document1) != viewProvider) {
686         return false; // optimistic locking failed
687       }
688
689       if (!ApplicationManager.getApplication().isWriteAccessAllowed()) {
690         VirtualFile vFile = viewProvider.getVirtualFile();
691         LOG.error("Write action expected" +
692                   "; document=" + document1 +
693                   "; file=" + file + " of " + file.getClass() +
694                   "; file.valid=" + file.isValid() +
695                   "; file.eventSystemEnabled=" + viewProvider.isEventSystemEnabled() +
696                   "; viewProvider=" + viewProvider + " of " + viewProvider.getClass() +
697                   "; language=" + file.getLanguage() +
698                   "; vFile=" + vFile + " of " + vFile.getClass() +
699                   "; free-threaded=" + AbstractFileViewProvider.isFreeThreaded(viewProvider));
700       }
701
702       diffLog.doActualPsiChange(file);
703
704       assertAfterCommit(document1, file, (FileElement)oldFileNode);
705
706       return true;
707     };
708   }
709
710   private static void assertAfterCommit(@NotNull Document document, @NotNull final PsiFile file, @NotNull FileElement oldFileNode) {
711     if (oldFileNode.getTextLength() != document.getTextLength()) {
712       final String documentText = document.getText();
713       String fileText = file.getText();
714       boolean sameText = Comparing.equal(fileText, documentText);
715       String errorMessage = "commitDocument() left PSI inconsistent: " + DebugUtil.diagnosePsiDocumentInconsistency(file, document) +
716                             "; node.length=" + oldFileNode.getTextLength() +
717                             "; doc.text" + (sameText ? "==" : "!=") + "file.text" +
718                             "; file name:" + file.getName() +
719                             "; type:" + file.getFileType() +
720                             "; lang:" + file.getLanguage();
721       PluginException.logPluginError(LOG, errorMessage, null, file.getLanguage().getClass());
722
723       file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE);
724       try {
725         BlockSupport blockSupport = BlockSupport.getInstance(file.getProject());
726         final DiffLog diffLog = blockSupport.reparseRange(file, file.getNode(), new TextRange(0, documentText.length()), documentText, createProgressIndicator(),
727                                                           oldFileNode.getText());
728         diffLog.doActualPsiChange(file);
729
730         if (oldFileNode.getTextLength() != document.getTextLength()) {
731           PluginException.logPluginError(LOG, "PSI is broken beyond repair in: " + file, null, file.getLanguage().getClass());
732         }
733       }
734       finally {
735         file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null);
736       }
737     }
738   }
739
740   /**
741    * @return an internal lock object to prevent read & write phases of commit from running simultaneously for free-threaded PSI
742    */
743   private static Lock getDocumentLock(Document document) {
744     Lock lock = document.getUserData(DOCUMENT_LOCK);
745     return lock != null ? lock : ((UserDataHolderEx)document).putUserDataIfAbsent(DOCUMENT_LOCK, new ReentrantLock());
746   }
747   private static final Key<Lock> DOCUMENT_LOCK = Key.create("DOCUMENT_LOCK");
748
749   void cancelTasksOnProjectDispose(@NotNull final Project project) {
750     synchronized (lock) {
751       cancelTasksOnProjectDispose(project, documentsToCommit);
752       cancelTasksOnProjectDispose(project, documentsToApplyInEDT);
753     }
754   }
755
756   private void cancelTasksOnProjectDispose(@NotNull Project project, @NotNull HashSetQueue<CommitTask> queue) {
757     for (HashSetQueue.PositionalIterator<CommitTask> iterator = queue.iterator(); iterator.hasNext(); ) {
758       CommitTask commitTask = iterator.next();
759       if (commitTask.project == project) {
760         iterator.remove();
761         commitTask.cancel("project is disposed", false);
762       }
763     }
764   }
765 }