2 * Copyright 2000-2016 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.intellij.psi.impl;
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;
62 import java.util.concurrent.ConcurrentMap;
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");
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;
78 private volatile boolean myIsCommitInProgress;
79 private final PsiToDocumentSynchronizer mySynchronizer;
81 private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
83 protected PsiDocumentManagerBase(@NotNull final Project project,
84 @NotNull PsiManager psiManager,
85 @NotNull MessageBus bus,
86 @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) {
88 myPsiManager = psiManager;
89 myDocumentCommitProcessor = documentCommitProcessor;
90 mySynchronizer = new PsiToDocumentSynchronizer(this, bus);
91 myPsiManager.addPsiTreeChangeListener(mySynchronizer);
92 bus.connect().subscribe(PsiDocumentTransactionListener.TOPIC, new PsiDocumentTransactionListener() {
94 public void transactionStarted(@NotNull Document document, @NotNull PsiFile file) {
95 myUncommittedDocuments.remove(document);
99 public void transactionCompleted(@NotNull Document document, @NotNull PsiFile file) {
106 public PsiFile getPsiFile(@NotNull Document document) {
107 final PsiFile userData = document.getUserData(HARD_REF_TO_PSI);
108 if (userData != null) return userData;
110 PsiFile psiFile = getCachedPsiFile(document);
111 if (psiFile != null) return psiFile;
113 final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
114 if (virtualFile == null || !virtualFile.isValid()) return null;
116 psiFile = getPsiFile(virtualFile);
117 if (psiFile == null) return null;
119 fireFileCreated(document, psiFile);
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");
131 public void associatePsi(@NotNull Document document, @Nullable PsiFile file) {
132 document.putUserData(HARD_REF_TO_PSI, file);
136 public PsiFile getCachedPsiFile(@NotNull Document document) {
137 final PsiFile userData = document.getUserData(HARD_REF_TO_PSI);
138 if (userData != null) return userData;
140 final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
141 if (virtualFile == null || !virtualFile.isValid()) return null;
142 return getCachedPsiFile(virtualFile);
146 FileViewProvider getCachedViewProvider(@NotNull Document document) {
147 final VirtualFile virtualFile = getVirtualFile(document);
148 if (virtualFile == null) return null;
149 return getCachedViewProvider(virtualFile);
152 private FileViewProvider getCachedViewProvider(@NotNull VirtualFile virtualFile) {
153 return ((PsiManagerEx)myPsiManager).getFileManager().findCachedViewProvider(virtualFile);
157 private static VirtualFile getVirtualFile(@NotNull Document document) {
158 final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
159 if (virtualFile == null || !virtualFile.isValid()) return null;
164 PsiFile getCachedPsiFile(@NotNull VirtualFile virtualFile) {
165 return ((PsiManagerEx)myPsiManager).getFileManager().getCachedPsiFile(virtualFile);
169 private PsiFile getPsiFile(@NotNull VirtualFile virtualFile) {
170 return ((PsiManagerEx)myPsiManager).getFileManager().findFile(virtualFile);
174 public Document getDocument(@NotNull PsiFile file) {
175 if (file instanceof PsiBinaryFile) return null;
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);
186 FileViewProvider viewProvider = file.getViewProvider();
187 if (!viewProvider.isEventSystemEnabled()) return null;
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();
196 throw new AssertionError(message);
199 if (!viewProvider.isPhysical()) {
200 PsiUtilCore.ensureValid(file);
201 associatePsi(document, file);
202 file.putUserData(HARD_REF_TO_DOCUMENT, document);
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);
217 public void commitAllDocuments() {
218 ApplicationManager.getApplication().assertIsDispatchThread();
219 ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed();
221 if (myUncommittedDocuments.isEmpty()) return;
223 final Document[] documents = getUncommittedDocuments();
224 for (Document document : documents) {
225 commitDocument(document);
228 LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments);
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)) {
238 addRunOnCommit(document, action);
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() {
246 public String toString() {
247 return "PERFORM_ALWAYS";
252 * Cancel previously registered action and schedules (new) action to be executed when all documents are committed.
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.
260 public boolean cancelAndRunWhenAllCommitted(@NonNls @NotNull Object key, @NotNull final Runnable action) {
261 ApplicationManager.getApplication().assertIsDispatchThread();
262 if (myProject.isDisposed()) {
266 if (myUncommittedDocuments.isEmpty()) {
267 if (!isCommitInProgress()) {
268 // in case of fireWriteActionFinished() we didn't execute 'actionsWhenAllDocumentsAreCommitted' yet
269 assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted;
275 checkWeAreOutsideAfterCommitHandler();
277 actionsWhenAllDocumentsAreCommitted.put(key, action);
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);
285 document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<Runnable>());
292 public void commitDocument(@NotNull final Document doc) {
293 final Document document = doc instanceof DocumentWindow ? ((DocumentWindow)doc).getDelegate() : doc;
295 if (isEventSystemEnabled(document)) {
296 ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed();
299 if (!isCommitted(document)) {
304 private boolean isEventSystemEnabled(Document document) {
305 VirtualFile vFile = getVirtualFile(document);
306 if (vFile == null || isFreeThreaded(vFile)) return false;
308 FileViewProvider viewProvider = getCachedViewProvider(document);
309 return viewProvider != null && viewProvider.isEventSystemEnabled();
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) {
323 ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously);
330 ApplicationManager.getApplication().runWriteAction(runnable);
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);
338 // run after commit actions outside write action
339 runAfterCommitActions(document);
340 if (DebugUtil.DO_EXPENSIVE_CHECKS && !ApplicationInfoImpl.isInPerformanceTest()) {
341 checkAllElementsValid(document, reason);
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);
354 VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
355 if (virtualFile != null) {
356 getSmartPointerManager().fastenBelts(virtualFile);
359 FileViewProvider viewProvider = getCachedViewProvider(document);
361 myIsCommitInProgress = true;
362 boolean success = true;
364 if (viewProvider != null) {
365 success = commitToExistingPsi(document, finishProcessors, synchronously, virtualFile, viewProvider);
368 handleCommitWithoutPsi(document);
371 catch (Throwable e) {
372 forceReload(virtualFile, viewProvider);
377 myUncommittedDocuments.remove(document);
379 myIsCommitInProgress = false;
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);
391 assert success : finishRunnable + " in " + finishProcessors;
397 clearUncommittedInfo(document);
398 if (virtualFile != null) {
399 getSmartPointerManager().updatePointerTargetsAfterReparse(virtualFile);
401 viewProvider.contentsSynchronized();
405 void forceReload(VirtualFile virtualFile, @Nullable FileViewProvider viewProvider) {
406 if (viewProvider instanceof SingleRootFileViewProvider) {
407 ((SingleRootFileViewProvider)viewProvider).markInvalidated();
409 if (virtualFile != null) {
410 ((FileManagerImpl)((PsiManagerEx)myPsiManager).getFileManager()).forceReload(virtualFile);
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() {
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 + "'");
428 private void doCommit(@NotNull final Document document) {
429 assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener";
431 // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged()
432 if (getSynchronizer().isDocumentAffectedByTransactions(document)) return;
434 final PsiFile psiFile = getPsiFile(document);
435 if (psiFile == null) {
436 myUncommittedDocuments.remove(document);
437 return; // the project must be closing or file deleted
440 Runnable runnable = new Runnable() {
443 myIsCommitInProgress = true;
445 myDocumentCommitProcessor.commitSynchronously(document, myProject, psiFile);
448 myIsCommitInProgress = false;
450 assert !isInUncommittedSet(document) : "Document :" + document;
454 if (isFreeThreaded(psiFile.getViewProvider().getVirtualFile())) {
458 ApplicationManager.getApplication().runWriteAction(runnable);
462 static boolean isFreeThreaded(@NotNull VirtualFile file) {
463 return Boolean.TRUE.equals(file.getUserData(SingleRootFileViewProvider.FREE_THREADED));
466 // true if the PSI is being modified and events being sent
467 public boolean isCommitInProgress() {
468 return myIsCommitInProgress;
472 public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) {
473 final Ref<T> ref = Ref.create(null);
474 commitAndRunReadAction(new Runnable() {
477 ref.set(computation.compute());
484 public void reparseFiles(@NotNull Collection<VirtualFile> files, boolean includeOpenFiles) {
485 FileContentUtilCore.reparseFiles(files);
489 public void commitAndRunReadAction(@NotNull final Runnable runnable) {
490 final Application application = ApplicationManager.getApplication();
491 if (SwingUtilities.isEventDispatchThread()) {
492 commitAllDocuments();
497 if (ApplicationManager.getApplication().isReadAccessAllowed()) {
498 LOG.error("Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock otherwise. "+Thread.currentThread());
502 boolean executed = application.runReadAction(new Computable<Boolean>() {
504 public Boolean compute() {
505 if (myUncommittedDocuments.isEmpty()) {
514 final Semaphore semaphore = new Semaphore();
516 application.invokeLater(new Runnable() {
519 if (myProject.isDisposed()) {
520 // committedness doesn't matter anymore; give clients a chance to do checkCanceled
525 performWhenAllCommitted(new Runnable() {
532 }, ModalityState.any());
538 * Schedules action to be executed when all documents are committed.
540 * @return true if action has been run immediately, or false if action was scheduled for execution later.
543 public boolean performWhenAllCommitted(@NotNull final Runnable action) {
544 ApplicationManager.getApplication().assertIsDispatchThread();
545 checkWeAreOutsideAfterCommitHandler();
547 assert !myProject.isDisposed() : "Already disposed: " + myProject;
548 if (myUncommittedDocuments.isEmpty()) {
552 CompositeRunnable actions = (CompositeRunnable)actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY);
553 if (actions == null) {
554 actions = new CompositeRunnable();
555 actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions);
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);
572 public void performLaterWhenAllCommitted(@NotNull final Runnable runnable) {
573 performLaterWhenAllCommitted(runnable, ModalityState.defaultModalityState());
577 public void performLaterWhenAllCommitted(@NotNull final Runnable runnable, final ModalityState modalityState) {
578 final Runnable whenAllCommitted = new Runnable() {
581 ApplicationManager.getApplication().invokeLater(new Runnable() {
584 if (hasUncommitedDocuments()) {
585 // no luck, will try later
586 performLaterWhenAllCommitted(runnable);
592 }, modalityState, myProject.getDisposed());
595 if (ApplicationManager.getApplication().isDispatchThread() && isInsideCommitHandler()) {
596 whenAllCommitted.run();
599 UIUtil.invokeLaterIfNeeded(new Runnable() {
602 performWhenAllCommitted(whenAllCommitted);
608 private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable {
611 for (Runnable runnable : this) {
617 private void runAfterCommitActions(@NotNull Document document) {
618 ApplicationManager.getApplication().assertIsDispatchThread();
620 synchronized (ACTION_AFTER_COMMIT) {
621 list = document.getUserData(ACTION_AFTER_COMMIT);
623 list = new ArrayList<Runnable>(list);
624 document.putUserData(ACTION_AFTER_COMMIT, null);
628 for (final Runnable runnable : list) {
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();
638 for (Map.Entry<Object, Runnable> entry : entries) {
639 Runnable action = entry.getValue();
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.
647 catch (Throwable e) {
648 LOG.error("During running " + action, e);
653 actionsWhenAllDocumentsAreCommitted.clear();
658 private void beforeCommitHandler() {
659 actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, EmptyRunnable.getInstance()); // to prevent listeners from registering new actions during firing
661 private void checkWeAreOutsideAfterCommitHandler() {
662 if (isInsideCommitHandler()) {
663 throw new IncorrectOperationException("You must not call performWhenAllCommitted()/cancelAndRunWhenCommitted() from within after-commit handler");
667 private boolean isInsideCommitHandler() {
668 return actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY) == EmptyRunnable.getInstance();
672 public void addListener(@NotNull Listener listener) {
673 myListeners.add(listener);
677 public void removeListener(@NotNull Listener listener) {
678 myListeners.remove(listener);
682 public boolean isDocumentBlockedByPsi(@NotNull Document doc) {
687 public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) {
690 void fireDocumentCreated(@NotNull Document document, PsiFile file) {
691 for (Listener listener : myListeners) {
692 listener.documentCreated(document, file);
696 private void fireFileCreated(@NotNull Document document, @NotNull PsiFile file) {
697 for (Listener listener : myListeners) {
698 listener.fileCreated(file, document);
704 public CharSequence getLastCommittedText(@NotNull Document document) {
705 return getLastCommittedDocument(document).getImmutableCharSequence();
709 public long getLastCommittedStamp(@NotNull Document document) {
710 if (document instanceof DocumentWindow) document = ((DocumentWindow)document).getDelegate();
711 return getLastCommittedDocument(document).getModificationStamp();
716 public Document getLastCommittedDocument(@NotNull PsiFile file) {
717 Document document = getDocument(file);
718 return document == null ? null : getLastCommittedDocument(document);
722 public DocumentEx getLastCommittedDocument(@NotNull Document document) {
723 if (document instanceof FrozenDocument) return (DocumentEx)document;
725 if (document instanceof DocumentWindow) {
726 DocumentWindow window = (DocumentWindow)document;
727 Document delegate = window.getDelegate();
728 if (delegate instanceof FrozenDocument) return (DocumentEx)window;
730 if (!window.isValid()) {
731 throw new AssertionError("host committed: " + isCommitted(delegate) + ", window=" + window);
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;
741 assert document instanceof DocumentImpl;
742 UncommittedInfo info = myUncommittedInfos.get(document);
743 return info != null ? info.myFrozen : ((DocumentImpl)document).freeze();
747 protected DocumentWindow freezeWindow(@NotNull DocumentWindow document) {
748 throw new UnsupportedOperationException();
752 public List<DocumentEvent> getEventsSinceCommit(@NotNull Document document) {
753 assert document instanceof DocumentImpl;
754 UncommittedInfo info = myUncommittedInfos.get(document);
756 return info.myEvents;
758 return Collections.emptyList();
764 public Document[] getUncommittedDocuments() {
765 ApplicationManager.getApplication().assertReadAccessAllowed();
766 Document[] documents = myUncommittedDocuments.toArray(new Document[myUncommittedDocuments.size()]);
767 return ArrayUtil.stripTrailingNulls(documents);
770 boolean isInUncommittedSet(@NotNull Document document) {
771 if (document instanceof DocumentWindow) document = ((DocumentWindow)document).getDelegate();
772 return myUncommittedDocuments.contains(document);
776 public boolean isUncommited(@NotNull Document document) {
777 return !isCommitted(document);
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);
788 public boolean hasUncommitedDocuments() {
789 return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty();
793 public void beforeDocumentChange(@NotNull DocumentEvent event) {
794 if (myStopTrackingDocuments || myProject.isDisposed()) return;
796 final Document document = event.getDocument();
797 VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
798 boolean isRelevant = virtualFile != null && isRelevant(virtualFile);
800 if (document instanceof DocumentImpl && !myUncommittedInfos.containsKey(document)) {
801 myUncommittedInfos.put(document, new UncommittedInfo((DocumentImpl)document));
804 final FileViewProvider viewProvider = getCachedViewProvider(document);
805 boolean inMyProject = viewProvider != null && viewProvider.getManager() == myPsiManager;
806 if (!isRelevant || !inMyProject) {
810 final List<PsiFile> files = viewProvider.getAllFiles();
811 PsiFile psiCause = null;
812 for (PsiFile file : files) {
814 throw new AssertionError("View provider "+viewProvider+" ("+viewProvider.getClass()+") returned null in its files array: "+files+" for file "+viewProvider.getVirtualFile());
817 if (PsiToDocumentSynchronizer.isInsideAtomicChange(file)) {
822 if (psiCause == null) {
823 beforeDocumentChangeOnUnlockedDocument(viewProvider);
826 ((SingleRootFileViewProvider)viewProvider).beforeDocumentChanged(psiCause);
829 protected void beforeDocumentChangeOnUnlockedDocument(@NotNull final FileViewProvider viewProvider) {
833 public void documentChanged(DocumentEvent event) {
834 if (myStopTrackingDocuments || myProject.isDisposed()) return;
836 final Document document = event.getDocument();
837 VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
838 boolean isRelevant = virtualFile != null && isRelevant(virtualFile);
840 final FileViewProvider viewProvider = getCachedViewProvider(document);
841 if (viewProvider == null) {
842 handleCommitWithoutPsi(document);
845 boolean inMyProject = viewProvider.getManager() == myPsiManager;
846 if (!isRelevant || !inMyProject) {
847 clearUncommittedInfo(document);
851 final List<PsiFile> files = viewProvider.getAllFiles();
852 boolean commitNecessary = true;
853 for (PsiFile file : files) {
855 if (PsiToDocumentSynchronizer.isInsideAtomicChange(file)) {
856 commitNecessary = false;
860 assert file instanceof PsiFileImpl || "mock.file".equals(file.getName()) && ApplicationManager.getApplication().isUnitTestMode() :
861 event + "; file=" + file + "; allFiles=" + files + "; viewProvider=" + viewProvider;
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());
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);
875 if (commitNecessary) {
876 assert !(document instanceof DocumentWindow);
877 myUncommittedDocuments.add(document);
879 commitDocument(document);
881 else if (!((DocumentEx)document).isInBulkUpdate() && myPerformBackgroundCommit) {
882 myDocumentCommitProcessor.commitAsynchronously(myProject, document, event, ApplicationManager.getApplication().getCurrentModalityState());
886 clearUncommittedInfo(document);
890 void handleCommitWithoutPsi(@NotNull Document document) {
891 final UncommittedInfo prevInfo = clearUncommittedInfo(document);
892 if (prevInfo == null) {
896 if (!myProject.isInitialized() || myProject.isDisposed()) {
900 myUncommittedDocuments.remove(document);
902 VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document);
903 if (virtualFile == null || !FileIndexFacade.getInstance(myProject).isInContent(virtualFile)) {
907 final PsiFile psiFile = getPsiFile(document);
908 if (psiFile == null) {
912 // we can end up outside write action here if the document has forUseInNonAWTThread=true
913 ApplicationManager.getApplication().runWriteAction(new ExternalChangeAction() {
916 FileViewProvider viewProvider = psiFile.getViewProvider();
917 if (viewProvider instanceof SingleRootFileViewProvider) {
918 ((SingleRootFileViewProvider)viewProvider).onContentReload();
920 LOG.error("Invalid view provider: " + viewProvider + " of " + viewProvider.getClass());
927 private UncommittedInfo clearUncommittedInfo(@NotNull Document document) {
928 UncommittedInfo info = myUncommittedInfos.remove(document);
930 getSmartPointerManager().updatePointers(document, info.myFrozen, info.myEvents);
931 info.removeListener();
936 private SmartPointerManagerImpl getSmartPointerManager() {
937 return (SmartPointerManagerImpl)SmartPointerManager.getInstance(myProject);
940 private boolean isRelevant(@NotNull VirtualFile virtualFile) {
941 return !virtualFile.getFileType().isBinary() && !myProject.isDisposed();
944 public static boolean checkConsistency(@NotNull PsiFile psiFile, @NotNull Document document) {
946 if (psiFile.getVirtualFile() == null) return true;
948 CharSequence editorText = document.getCharsSequence();
949 int documentLength = document.getTextLength();
950 if (psiFile.textMatches(editorText)) {
951 LOG.assertTrue(psiFile.getTextLength() == documentLength);
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";
960 for (; i < documentLength; i++) {
961 if (i >= fileText.length) {
962 error += "editorText.length > psiText.length i=" + i + "\n";
965 if (i >= editorText.length()) {
966 error += "editorText.length > psiText.length i=" + i + "\n";
969 if (editorText.charAt(i) != fileText[i]) {
970 error += "first unequal char i=" + i + "\n";
974 //error += "*********************************************" + "\n";
976 // error += "Equal part:" + editorText.subSequence(0, i) + "\n";
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";
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";
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";
1001 error += "document window ranges: " + Arrays.asList(((DocumentWindow)document).getHostRanges()) + "\n";
1004 //document.replaceString(0, documentLength, psiFile.getText());
1009 public void clearUncommittedDocuments() {
1010 for (UncommittedInfo info : myUncommittedInfos.values()) {
1011 info.removeListener();
1013 myUncommittedInfos.clear();
1014 myUncommittedDocuments.clear();
1015 mySynchronizer.cleanupForNextTest();
1019 public void disableBackgroundCommit(@NotNull Disposable parentDisposable) {
1020 assert myPerformBackgroundCommit;
1021 myPerformBackgroundCommit = false;
1022 Disposer.register(parentDisposable, new Disposable() {
1024 public void dispose() {
1025 myPerformBackgroundCommit = true;
1031 public void projectOpened() {
1035 public void projectClosed() {
1039 public void initComponent() {
1043 public void disposeComponent() {
1044 clearUncommittedDocuments();
1049 public String getComponentName() {
1050 return getClass().getSimpleName();
1054 public PsiToDocumentSynchronizer getSynchronizer() {
1055 return mySynchronizer;
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();
1064 private UncommittedInfo(DocumentImpl original) {
1065 myOriginal = original;
1066 myFrozen = original.freeze();
1067 myOriginal.addDocumentListener(this);
1071 public int getPriority() {
1072 return EditorDocumentPriorities.RANGE_MARKER;
1076 public void documentChanged(DocumentEvent e) {
1081 public void moveTextHappened(int start, int end, int base) {
1082 myEvents.add(new RetargetRangeMarkers(myOriginal, start, end, base));
1085 public void removeListener() {
1086 myOriginal.removeDocumentListener(this);