/* * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.psi.impl; import com.google.common.annotations.VisibleForTesting; import com.intellij.injected.editor.DocumentWindow; import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.*; import com.intellij.openapi.application.impl.ApplicationInfoImpl; import com.intellij.openapi.components.ProjectComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.DocumentRunnable; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.event.DocumentListener; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.ex.PrioritizedInternalDocumentListener; import com.intellij.openapi.editor.impl.DocumentImpl; import com.intellij.openapi.editor.impl.EditorDocumentPriorities; import com.intellij.openapi.editor.impl.FrozenDocument; import com.intellij.openapi.editor.impl.event.RetargetRangeMarkers; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.FileIndexFacade; import com.intellij.openapi.util.*; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.impl.file.impl.FileManagerImpl; import com.intellij.psi.impl.smartPointers.SmartPointerManagerImpl; import com.intellij.psi.impl.source.PsiFileImpl; import com.intellij.psi.text.BlockSupport; import com.intellij.psi.util.PsiUtilCore; import com.intellij.util.*; import com.intellij.util.concurrency.Semaphore; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.messages.MessageBus; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import javax.swing.*; import java.util.*; import java.util.concurrent.ConcurrentMap; public abstract class PsiDocumentManagerBase extends PsiDocumentManager implements DocumentListener, ProjectComponent { static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.PsiDocumentManagerImpl"); private static final Key<Document> HARD_REF_TO_DOCUMENT = Key.create("HARD_REFERENCE_TO_DOCUMENT"); private final Key<PsiFile> HARD_REF_TO_PSI = Key.create("HARD_REF_TO_PSI"); // has to be different for each project to avoid mixups private static final Key<List<Runnable>> ACTION_AFTER_COMMIT = Key.create("ACTION_AFTER_COMMIT"); protected final Project myProject; private final PsiManager myPsiManager; private final DocumentCommitProcessor myDocumentCommitProcessor; protected final Set<Document> myUncommittedDocuments = ContainerUtil.newConcurrentSet(); private final Map<Document, UncommittedInfo> myUncommittedInfos = ContainerUtil.newConcurrentMap(); protected boolean myStopTrackingDocuments; private boolean myPerformBackgroundCommit = true; private volatile boolean myIsCommitInProgress; private final PsiToDocumentSynchronizer mySynchronizer; private final List<Listener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); protected PsiDocumentManagerBase(@NotNull final Project project, @NotNull PsiManager psiManager, @NotNull MessageBus bus, @NonNls @NotNull final DocumentCommitProcessor documentCommitProcessor) { myProject = project; myPsiManager = psiManager; myDocumentCommitProcessor = documentCommitProcessor; mySynchronizer = new PsiToDocumentSynchronizer(this, bus); myPsiManager.addPsiTreeChangeListener(mySynchronizer); bus.connect().subscribe(PsiDocumentTransactionListener.TOPIC, new PsiDocumentTransactionListener() { @Override public void transactionStarted(@NotNull Document document, @NotNull PsiFile file) { myUncommittedDocuments.remove(document); } @Override public void transactionCompleted(@NotNull Document document, @NotNull PsiFile file) { } }); } @Override @Nullable public PsiFile getPsiFile(@NotNull Document document) { if (document instanceof DocumentWindow && !((DocumentWindow)document).isValid()) { return null; } final PsiFile userData = document.getUserData(HARD_REF_TO_PSI); if (userData != null) { return ensureValidFile(userData, "From hard ref"); } PsiFile psiFile = getCachedPsiFile(document); if (psiFile != null) { return ensureValidFile(psiFile, "Cached PSI"); } final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; psiFile = getPsiFile(virtualFile); if (psiFile == null) return null; fireFileCreated(document, psiFile); return psiFile; } @NotNull private static PsiFile ensureValidFile(@NotNull PsiFile psiFile, @NotNull String debugInfo) { if (!psiFile.isValid()) throw new PsiInvalidElementAccessException(psiFile, debugInfo); return psiFile; } @Deprecated // todo remove when Database Navigator plugin doesn't need that anymore // todo to be removed in idea 17 public static void cachePsi(@NotNull Document document, @Nullable PsiFile file) { LOG.warn("Unsupported method"); } public void associatePsi(@NotNull Document document, @Nullable PsiFile file) { document.putUserData(HARD_REF_TO_PSI, file); } @Override public PsiFile getCachedPsiFile(@NotNull Document document) { final PsiFile userData = document.getUserData(HARD_REF_TO_PSI); if (userData != null) return userData; final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; return getCachedPsiFile(virtualFile); } @Nullable FileViewProvider getCachedViewProvider(@NotNull Document document) { final VirtualFile virtualFile = getVirtualFile(document); if (virtualFile == null) return null; return getCachedViewProvider(virtualFile); } private FileViewProvider getCachedViewProvider(@NotNull VirtualFile virtualFile) { return ((PsiManagerEx)myPsiManager).getFileManager().findCachedViewProvider(virtualFile); } @Nullable private static VirtualFile getVirtualFile(@NotNull Document document) { final VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !virtualFile.isValid()) return null; return virtualFile; } @Nullable PsiFile getCachedPsiFile(@NotNull VirtualFile virtualFile) { return ((PsiManagerEx)myPsiManager).getFileManager().getCachedPsiFile(virtualFile); } @Nullable private PsiFile getPsiFile(@NotNull VirtualFile virtualFile) { return ((PsiManagerEx)myPsiManager).getFileManager().findFile(virtualFile); } @Override public Document getDocument(@NotNull PsiFile file) { if (file instanceof PsiBinaryFile) return null; Document document = getCachedDocument(file); if (document != null) { if (!file.getViewProvider().isPhysical() && document.getUserData(HARD_REF_TO_PSI) == null) { PsiUtilCore.ensureValid(file); associatePsi(document, file); } return document; } FileViewProvider viewProvider = file.getViewProvider(); if (!viewProvider.isEventSystemEnabled()) return null; document = FileDocumentManager.getInstance().getDocument(viewProvider.getVirtualFile()); if (document != null) { if (document.getTextLength() != file.getTextLength()) { String message = "Document/PSI mismatch: " + file + " (" + file.getClass() + "); physical=" + viewProvider.isPhysical(); if (document.getTextLength() + file.getTextLength() < 8096) { message += "\n=== document ===\n" + document.getText() + "\n=== PSI ===\n" + file.getText(); } throw new AssertionError(message); } if (!viewProvider.isPhysical()) { PsiUtilCore.ensureValid(file); associatePsi(document, file); file.putUserData(HARD_REF_TO_DOCUMENT, document); } } return document; } @Override public Document getCachedDocument(@NotNull PsiFile file) { if (!file.isPhysical()) return null; VirtualFile vFile = file.getViewProvider().getVirtualFile(); return FileDocumentManager.getInstance().getCachedDocument(vFile); } @Override public void commitAllDocuments() { ApplicationManager.getApplication().assertIsDispatchThread(); ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed(); if (myUncommittedDocuments.isEmpty()) return; final Document[] documents = getUncommittedDocuments(); for (Document document : documents) { commitDocument(document); } LOG.assertTrue(!hasUncommitedDocuments(), myUncommittedDocuments); } @Override public void performForCommittedDocument(@NotNull final Document doc, @NotNull final Runnable action) { final Document document = doc instanceof DocumentWindow ? ((DocumentWindow)doc).getDelegate() : doc; if (isCommitted(document)) { action.run(); } else { addRunOnCommit(document, action); } } private final Map<Object, Runnable> actionsWhenAllDocumentsAreCommitted = new LinkedHashMap<>(); //accessed from EDT only private static final Object PERFORM_ALWAYS_KEY = new Object() { @Override @NonNls public String toString() { return "PERFORM_ALWAYS"; } }; /** * Cancel previously registered action and schedules (new) action to be executed when all documents are committed. * * @param key the (unique) id of the action. * @param action The action to be executed after automatic commit. * This action will overwrite any action which was registered under this key earlier. * The action will be executed in EDT. * @return true if action has been run immediately, or false if action was scheduled for execution later. */ public boolean cancelAndRunWhenAllCommitted(@NonNls @NotNull Object key, @NotNull final Runnable action) { ApplicationManager.getApplication().assertIsDispatchThread(); if (myProject.isDisposed()) { action.run(); return true; } if (myUncommittedDocuments.isEmpty()) { if (!isCommitInProgress()) { // in case of fireWriteActionFinished() we didn't execute 'actionsWhenAllDocumentsAreCommitted' yet assert actionsWhenAllDocumentsAreCommitted.isEmpty() : actionsWhenAllDocumentsAreCommitted; } action.run(); return true; } checkWeAreOutsideAfterCommitHandler(); actionsWhenAllDocumentsAreCommitted.put(key, action); return false; } public static void addRunOnCommit(@NotNull Document document, @NotNull Runnable action) { synchronized (ACTION_AFTER_COMMIT) { List<Runnable> list = document.getUserData(ACTION_AFTER_COMMIT); if (list == null) { document.putUserData(ACTION_AFTER_COMMIT, list = new SmartList<>()); } list.add(action); } } @Override public void commitDocument(@NotNull final Document doc) { final Document document = doc instanceof DocumentWindow ? ((DocumentWindow)doc).getDelegate() : doc; if (isEventSystemEnabled(document)) { ((TransactionGuardImpl)TransactionGuard.getInstance()).assertWriteActionAllowed(); } if (!isCommitted(document)) { doCommit(document); } } private boolean isEventSystemEnabled(Document document) { FileViewProvider viewProvider = getCachedViewProvider(document); return viewProvider != null && viewProvider.isEventSystemEnabled() && !SingleRootFileViewProvider.isFreeThreaded(viewProvider); } // public for Upsource public boolean finishCommit(@NotNull final Document document, @NotNull final List<Processor<Document>> finishProcessors, final boolean synchronously, @NotNull final Object reason) { assert !myProject.isDisposed() : "Already disposed"; ApplicationManager.getApplication().assertIsDispatchThread(); final boolean[] ok = {true}; Runnable runnable = new DocumentRunnable(document, myProject) { @Override public void run() { ok[0] = finishCommitInWriteAction(document, finishProcessors, synchronously); } }; if (synchronously) { runnable.run(); } else { ApplicationManager.getApplication().runWriteAction(runnable); } if (ok[0]) { // otherwise changes maybe not synced to the document yet, and injectors will crash if (!mySynchronizer.isDocumentAffectedByTransactions(document)) { InjectedLanguageManager.getInstance(myProject).startRunInjectors(document, synchronously); } // run after commit actions outside write action runAfterCommitActions(document); if (DebugUtil.DO_EXPENSIVE_CHECKS && !ApplicationInfoImpl.isInStressTest()) { checkAllElementsValid(document, reason); } } return ok[0]; } protected boolean finishCommitInWriteAction(@NotNull final Document document, @NotNull final List<Processor<Document>> finishProcessors, final boolean synchronously) { ApplicationManager.getApplication().assertIsDispatchThread(); if (myProject.isDisposed()) return false; assert !(document instanceof DocumentWindow); VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile != null) { getSmartPointerManager().fastenBelts(virtualFile); } FileViewProvider viewProvider = getCachedViewProvider(document); myIsCommitInProgress = true; boolean success = true; try { if (viewProvider != null) { success = commitToExistingPsi(document, finishProcessors, synchronously, virtualFile, viewProvider); } else { handleCommitWithoutPsi(document); } } catch (Throwable e) { forceReload(virtualFile, viewProvider); LOG.error(e); } finally { if (success) { myUncommittedDocuments.remove(document); } myIsCommitInProgress = false; } return success; } private boolean commitToExistingPsi(@NotNull Document document, @NotNull List<Processor<Document>> finishProcessors, boolean synchronously, @Nullable VirtualFile virtualFile, @NotNull FileViewProvider viewProvider) { for (Processor<Document> finishRunnable : finishProcessors) { boolean success = finishRunnable.process(document); if (synchronously) { assert success : finishRunnable + " in " + finishProcessors; } if (!success) { return false; } } clearUncommittedInfo(document); if (virtualFile != null) { getSmartPointerManager().updatePointerTargetsAfterReparse(virtualFile); } viewProvider.contentsSynchronized(); return true; } void forceReload(VirtualFile virtualFile, @Nullable FileViewProvider viewProvider) { if (viewProvider instanceof SingleRootFileViewProvider) { ((SingleRootFileViewProvider)viewProvider).markInvalidated(); } if (virtualFile != null) { ((FileManagerImpl)((PsiManagerEx)myPsiManager).getFileManager()).forceReload(virtualFile); } } private void checkAllElementsValid(@NotNull Document document, @NotNull final Object reason) { final PsiFile psiFile = getCachedPsiFile(document); if (psiFile != null) { psiFile.accept(new PsiRecursiveElementWalkingVisitor() { @Override public void visitElement(PsiElement element) { if (!element.isValid()) { throw new AssertionError("Commit to '" + psiFile.getVirtualFile() + "' has led to invalid element: " + element + "; Reason: '" + reason + "'"); } } }); } } private void doCommit(@NotNull final Document document) { assert !myIsCommitInProgress : "Do not call commitDocument() from inside PSI change listener"; // otherwise there are many clients calling commitAllDocs() on PSI childrenChanged() if (getSynchronizer().isDocumentAffectedByTransactions(document)) return; final PsiFile psiFile = getPsiFile(document); if (psiFile == null) { myUncommittedDocuments.remove(document); return; // the project must be closing or file deleted } Runnable runnable = () -> { myIsCommitInProgress = true; try { myDocumentCommitProcessor.commitSynchronously(document, myProject, psiFile); } finally { myIsCommitInProgress = false; } assert !isInUncommittedSet(document) : "Document :" + document; }; if (SingleRootFileViewProvider.isFreeThreaded(psiFile.getViewProvider())) { runnable.run(); } else { ApplicationManager.getApplication().runWriteAction(runnable); } } // true if the PSI is being modified and events being sent public boolean isCommitInProgress() { return myIsCommitInProgress; } @Override public <T> T commitAndRunReadAction(@NotNull final Computable<T> computation) { final Ref<T> ref = Ref.create(null); commitAndRunReadAction(() -> ref.set(computation.compute())); return ref.get(); } @Override public void reparseFiles(@NotNull Collection<VirtualFile> files, boolean includeOpenFiles) { FileContentUtilCore.reparseFiles(files); } @Override public void commitAndRunReadAction(@NotNull final Runnable runnable) { final Application application = ApplicationManager.getApplication(); if (SwingUtilities.isEventDispatchThread()) { commitAllDocuments(); runnable.run(); return; } if (ApplicationManager.getApplication().isReadAccessAllowed()) { LOG.error("Don't call commitAndRunReadAction inside ReadAction, it will cause a deadlock otherwise. "+Thread.currentThread()); } while (true) { boolean executed = application.runReadAction((Computable<Boolean>)() -> { if (myUncommittedDocuments.isEmpty()) { runnable.run(); return true; } return false; }); if (executed) break; final Semaphore semaphore = new Semaphore(); semaphore.down(); application.invokeLater(() -> { if (myProject.isDisposed()) { // committedness doesn't matter anymore; give clients a chance to do checkCanceled semaphore.up(); return; } performWhenAllCommitted(() -> semaphore.up()); }, ModalityState.any()); semaphore.waitFor(); } } /** * Schedules action to be executed when all documents are committed. * * @return true if action has been run immediately, or false if action was scheduled for execution later. */ @Override public boolean performWhenAllCommitted(@NotNull final Runnable action) { ApplicationManager.getApplication().assertIsDispatchThread(); checkWeAreOutsideAfterCommitHandler(); assert !myProject.isDisposed() : "Already disposed: " + myProject; if (myUncommittedDocuments.isEmpty()) { action.run(); return true; } CompositeRunnable actions = (CompositeRunnable)actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY); if (actions == null) { actions = new CompositeRunnable(); actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, actions); } actions.add(action); TransactionId current = TransactionGuard.getInstance().getContextTransaction(); if (current != ModalityState.NON_MODAL) { // re-add all uncommitted documents into the queue with this new modality // because this client obviously expects them to commit even inside modal dialog for (Document document : myUncommittedDocuments) { myDocumentCommitProcessor.commitAsynchronously(myProject, document, "re-added with modality "+current+" because performWhenAllCommitted("+current+") was called", current); } } return false; } @Override public void performLaterWhenAllCommitted(@NotNull final Runnable runnable) { performLaterWhenAllCommitted(runnable, ModalityState.defaultModalityState()); } @Override public void performLaterWhenAllCommitted(@NotNull final Runnable runnable, final ModalityState modalityState) { final Runnable whenAllCommitted = () -> ApplicationManager.getApplication().invokeLater(() -> { if (hasUncommitedDocuments()) { // no luck, will try later performLaterWhenAllCommitted(runnable); } else { runnable.run(); } }, modalityState, myProject.getDisposed()); if (ApplicationManager.getApplication().isDispatchThread() && isInsideCommitHandler()) { whenAllCommitted.run(); } else { UIUtil.invokeLaterIfNeeded(() -> { if (!myProject.isDisposed()) performWhenAllCommitted(whenAllCommitted);}); } } private static class CompositeRunnable extends ArrayList<Runnable> implements Runnable { @Override public void run() { for (Runnable runnable : this) { runnable.run(); } } } private void runAfterCommitActions(@NotNull Document document) { ApplicationManager.getApplication().assertIsDispatchThread(); List<Runnable> list; synchronized (ACTION_AFTER_COMMIT) { list = document.getUserData(ACTION_AFTER_COMMIT); if (list != null) { list = new ArrayList<>(list); document.putUserData(ACTION_AFTER_COMMIT, null); } } if (list != null) { for (final Runnable runnable : list) { runnable.run(); } } if (!hasUncommitedDocuments() && !actionsWhenAllDocumentsAreCommitted.isEmpty()) { List<Map.Entry<Object, Runnable>> entries = new ArrayList<>( new LinkedHashMap<>(actionsWhenAllDocumentsAreCommitted).entrySet()); beforeCommitHandler(); try { for (Map.Entry<Object, Runnable> entry : entries) { Runnable action = entry.getValue(); try { action.run(); } catch (ProcessCanceledException e) { // some actions are that crazy to use PCE for their own control flow. // swallow and ignore to not disrupt completely unrelated control flow. } catch (Throwable e) { LOG.error("During running " + action, e); } } } finally { actionsWhenAllDocumentsAreCommitted.clear(); } } } private void beforeCommitHandler() { actionsWhenAllDocumentsAreCommitted.put(PERFORM_ALWAYS_KEY, EmptyRunnable.getInstance()); // to prevent listeners from registering new actions during firing } private void checkWeAreOutsideAfterCommitHandler() { if (isInsideCommitHandler()) { throw new IncorrectOperationException("You must not call performWhenAllCommitted()/cancelAndRunWhenCommitted() from within after-commit handler"); } } private boolean isInsideCommitHandler() { return actionsWhenAllDocumentsAreCommitted.get(PERFORM_ALWAYS_KEY) == EmptyRunnable.getInstance(); } @Override public void addListener(@NotNull Listener listener) { myListeners.add(listener); } @Override public void removeListener(@NotNull Listener listener) { myListeners.remove(listener); } @Override public boolean isDocumentBlockedByPsi(@NotNull Document doc) { return false; } @Override public void doPostponedOperationsAndUnblockDocument(@NotNull Document doc) { } void fireDocumentCreated(@NotNull Document document, PsiFile file) { for (Listener listener : myListeners) { listener.documentCreated(document, file); } } private void fireFileCreated(@NotNull Document document, @NotNull PsiFile file) { for (Listener listener : myListeners) { listener.fileCreated(file, document); } } @Override @NotNull public CharSequence getLastCommittedText(@NotNull Document document) { return getLastCommittedDocument(document).getImmutableCharSequence(); } @Override public long getLastCommittedStamp(@NotNull Document document) { if (document instanceof DocumentWindow) document = ((DocumentWindow)document).getDelegate(); return getLastCommittedDocument(document).getModificationStamp(); } @Override @Nullable public Document getLastCommittedDocument(@NotNull PsiFile file) { Document document = getDocument(file); return document == null ? null : getLastCommittedDocument(document); } @NotNull public DocumentEx getLastCommittedDocument(@NotNull Document document) { if (document instanceof FrozenDocument) return (DocumentEx)document; if (document instanceof DocumentWindow) { DocumentWindow window = (DocumentWindow)document; Document delegate = window.getDelegate(); if (delegate instanceof FrozenDocument) return (DocumentEx)window; if (!window.isValid()) { throw new AssertionError("host committed: " + isCommitted(delegate) + ", window=" + window); } UncommittedInfo info = myUncommittedInfos.get(delegate); DocumentWindow answer = info == null ? null : info.myFrozenWindows.get(document); if (answer == null) answer = freezeWindow(window); if (info != null) answer = ConcurrencyUtil.cacheOrGet(info.myFrozenWindows, window, answer); return (DocumentEx)answer; } assert document instanceof DocumentImpl; UncommittedInfo info = myUncommittedInfos.get(document); return info != null ? info.myFrozen : ((DocumentImpl)document).freeze(); } @NotNull protected DocumentWindow freezeWindow(@NotNull DocumentWindow document) { throw new UnsupportedOperationException(); } @NotNull public List<DocumentEvent> getEventsSinceCommit(@NotNull Document document) { assert document instanceof DocumentImpl; UncommittedInfo info = myUncommittedInfos.get(document); if (info != null) { return info.myEvents; } return Collections.emptyList(); } @Override @NotNull public Document[] getUncommittedDocuments() { ApplicationManager.getApplication().assertReadAccessAllowed(); Document[] documents = myUncommittedDocuments.toArray(new Document[myUncommittedDocuments.size()]); return ArrayUtil.stripTrailingNulls(documents); } boolean isInUncommittedSet(@NotNull Document document) { if (document instanceof DocumentWindow) document = ((DocumentWindow)document).getDelegate(); return myUncommittedDocuments.contains(document); } @Override public boolean isUncommited(@NotNull Document document) { return !isCommitted(document); } @Override public boolean isCommitted(@NotNull Document document) { if (document instanceof DocumentWindow) document = ((DocumentWindow)document).getDelegate(); if (getSynchronizer().isInSynchronization(document)) return true; return (!(document instanceof DocumentEx) || !((DocumentEx)document).isInEventsHandling()) && !isInUncommittedSet(document); } @Override public boolean hasUncommitedDocuments() { return !myIsCommitInProgress && !myUncommittedDocuments.isEmpty(); } @Override public void beforeDocumentChange(@NotNull DocumentEvent event) { if (myStopTrackingDocuments || myProject.isDisposed()) return; final Document document = event.getDocument(); VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); boolean isRelevant = virtualFile != null && isRelevant(virtualFile); if (document instanceof DocumentImpl && !myUncommittedInfos.containsKey(document)) { myUncommittedInfos.put(document, new UncommittedInfo((DocumentImpl)document)); } final FileViewProvider viewProvider = getCachedViewProvider(document); boolean inMyProject = viewProvider != null && viewProvider.getManager() == myPsiManager; if (!isRelevant || !inMyProject) { return; } final List<PsiFile> files = viewProvider.getAllFiles(); PsiFile psiCause = null; for (PsiFile file : files) { if (file == null) { throw new AssertionError("View provider "+viewProvider+" ("+viewProvider.getClass()+") returned null in its files array: "+files+" for file "+viewProvider.getVirtualFile()); } if (PsiToDocumentSynchronizer.isInsideAtomicChange(file)) { psiCause = file; } } if (psiCause == null) { beforeDocumentChangeOnUnlockedDocument(viewProvider); } ((SingleRootFileViewProvider)viewProvider).beforeDocumentChanged(psiCause); } protected void beforeDocumentChangeOnUnlockedDocument(@NotNull final FileViewProvider viewProvider) { } @Override public void documentChanged(DocumentEvent event) { if (myStopTrackingDocuments || myProject.isDisposed()) return; final Document document = event.getDocument(); VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); boolean isRelevant = virtualFile != null && isRelevant(virtualFile); final FileViewProvider viewProvider = getCachedViewProvider(document); if (viewProvider == null) { handleCommitWithoutPsi(document); return; } boolean inMyProject = viewProvider.getManager() == myPsiManager; if (!isRelevant || !inMyProject) { clearUncommittedInfo(document); return; } List<PsiFile> files = viewProvider.getAllFiles(); boolean commitNecessary = files.stream().noneMatch(file -> PsiToDocumentSynchronizer.isInsideAtomicChange(file) || !(file instanceof PsiFileImpl)); boolean forceCommit = ApplicationManager.getApplication().hasWriteAction(ExternalChangeAction.class) && (SystemProperties.getBooleanProperty("idea.force.commit.on.external.change", false) || ApplicationManager.getApplication().isHeadlessEnvironment() && !ApplicationManager.getApplication().isUnitTestMode()); // Consider that it's worth to perform complete re-parse instead of merge if the whole document text is replaced and // current document lines number is roughly above 5000. This makes sense in situations when external change is performed // for the huge file (that causes the whole document to be reloaded and 'merge' way takes a while to complete). if (event.isWholeTextReplaced() && document.getTextLength() > 100000) { document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, Boolean.TRUE); } if (commitNecessary) { assert !(document instanceof DocumentWindow); myUncommittedDocuments.add(document); if (forceCommit) { commitDocument(document); } else if (!((DocumentEx)document).isInBulkUpdate() && myPerformBackgroundCommit) { myDocumentCommitProcessor.commitAsynchronously(myProject, document, event, TransactionGuard.getInstance().getContextTransaction()); } } else { clearUncommittedInfo(document); } } void handleCommitWithoutPsi(@NotNull Document document) { final UncommittedInfo prevInfo = clearUncommittedInfo(document); if (prevInfo == null) { return; } if (!myProject.isInitialized() || myProject.isDisposed()) { return; } myUncommittedDocuments.remove(document); VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); if (virtualFile == null || !FileIndexFacade.getInstance(myProject).isInContent(virtualFile)) { return; } final PsiFile psiFile = getPsiFile(document); if (psiFile == null) { return; } // we can end up outside write action here if the document has forUseInNonAWTThread=true ApplicationManager.getApplication().runWriteAction(new ExternalChangeAction() { @Override public void run() { FileViewProvider viewProvider = psiFile.getViewProvider(); if (viewProvider instanceof SingleRootFileViewProvider) { ((SingleRootFileViewProvider)viewProvider).onContentReload(); } else { LOG.error("Invalid view provider: " + viewProvider + " of " + viewProvider.getClass()); } } }); } @Nullable private UncommittedInfo clearUncommittedInfo(@NotNull Document document) { UncommittedInfo info = myUncommittedInfos.remove(document); if (info != null) { getSmartPointerManager().updatePointers(document, info.myFrozen, info.myEvents); info.removeListener(); } return info; } private SmartPointerManagerImpl getSmartPointerManager() { return (SmartPointerManagerImpl)SmartPointerManager.getInstance(myProject); } private boolean isRelevant(@NotNull VirtualFile virtualFile) { return !virtualFile.getFileType().isBinary() && !myProject.isDisposed(); } public static boolean checkConsistency(@NotNull PsiFile psiFile, @NotNull Document document) { //todo hack if (psiFile.getVirtualFile() == null) return true; CharSequence editorText = document.getCharsSequence(); int documentLength = document.getTextLength(); if (psiFile.textMatches(editorText)) { LOG.assertTrue(psiFile.getTextLength() == documentLength); return true; } char[] fileText = psiFile.textToCharArray(); @SuppressWarnings("NonConstantStringShouldBeStringBuffer") @NonNls String error = "File '" + psiFile.getName() + "' text mismatch after reparse. " + "File length=" + fileText.length + "; Doc length=" + documentLength + "\n"; int i = 0; for (; i < documentLength; i++) { if (i >= fileText.length) { error += "editorText.length > psiText.length i=" + i + "\n"; break; } if (i >= editorText.length()) { error += "editorText.length > psiText.length i=" + i + "\n"; break; } if (editorText.charAt(i) != fileText[i]) { error += "first unequal char i=" + i + "\n"; break; } } //error += "*********************************************" + "\n"; //if (i <= 500){ // error += "Equal part:" + editorText.subSequence(0, i) + "\n"; //} //else{ // error += "Equal part start:\n" + editorText.subSequence(0, 200) + "\n"; // error += "................................................" + "\n"; // error += "................................................" + "\n"; // error += "................................................" + "\n"; // error += "Equal part end:\n" + editorText.subSequence(i - 200, i) + "\n"; //} error += "*********************************************" + "\n"; error += "Editor Text tail:(" + (documentLength - i) + ")\n";// + editorText.subSequence(i, Math.min(i + 300, documentLength)) + "\n"; error += "*********************************************" + "\n"; error += "Psi Text tail:(" + (fileText.length - i) + ")\n"; error += "*********************************************" + "\n"; if (document instanceof DocumentWindow) { error += "doc: '" + document.getText() + "'\n"; error += "psi: '" + psiFile.getText() + "'\n"; error += "ast: '" + psiFile.getNode().getText() + "'\n"; error += psiFile.getLanguage() + "\n"; PsiElement context = InjectedLanguageManager.getInstance(psiFile.getProject()).getInjectionHost(psiFile); if (context != null) { error += "context: " + context + "; text: '" + context.getText() + "'\n"; error += "context file: " + context.getContainingFile() + "\n"; } error += "document window ranges: " + Arrays.asList(((DocumentWindow)document).getHostRanges()) + "\n"; } LOG.error(error); //document.replaceString(0, documentLength, psiFile.getText()); return false; } @VisibleForTesting public void clearUncommittedDocuments() { for (UncommittedInfo info : myUncommittedInfos.values()) { info.removeListener(); } myUncommittedInfos.clear(); myUncommittedDocuments.clear(); mySynchronizer.cleanupForNextTest(); } @TestOnly public void disableBackgroundCommit(@NotNull Disposable parentDisposable) { assert myPerformBackgroundCommit; myPerformBackgroundCommit = false; Disposer.register(parentDisposable, new Disposable() { @Override public void dispose() { myPerformBackgroundCommit = true; } }); } @Override public void projectOpened() { } @Override public void projectClosed() { } @Override public void initComponent() { } @Override public void disposeComponent() { clearUncommittedDocuments(); } @NotNull @Override public String getComponentName() { return getClass().getSimpleName(); } @NotNull public PsiToDocumentSynchronizer getSynchronizer() { return mySynchronizer; } private static class UncommittedInfo implements PrioritizedInternalDocumentListener, DocumentListener { private final DocumentImpl myOriginal; private final FrozenDocument myFrozen; private final List<DocumentEvent> myEvents = ContainerUtil.newArrayList(); private final ConcurrentMap<DocumentWindow, DocumentWindow> myFrozenWindows = ContainerUtil.newConcurrentMap(); private UncommittedInfo(DocumentImpl original) { myOriginal = original; myFrozen = original.freeze(); myOriginal.addDocumentListener(this); } @Override public int getPriority() { return EditorDocumentPriorities.RANGE_MARKER; } @Override public void documentChanged(DocumentEvent e) { myEvents.add(e); } @Override public void moveTextHappened(int start, int end, int base) { myEvents.add(new RetargetRangeMarkers(myOriginal, start, end, base)); } public void removeListener() { myOriginal.removeDocumentListener(this); } } }