diff --git a/platform/core-impl/src/com/intellij/core/MockDocumentCommitProcessor.java b/platform/core-impl/src/com/intellij/core/MockDocumentCommitProcessor.java index 89979c7d8b3f..33adf6bce388 100644 --- a/platform/core-impl/src/com/intellij/core/MockDocumentCommitProcessor.java +++ b/platform/core-impl/src/com/intellij/core/MockDocumentCommitProcessor.java @@ -4,14 +4,11 @@ package com.intellij.core; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; -import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiFile; import com.intellij.psi.impl.DocumentCommitProcessor; import com.intellij.psi.impl.PsiDocumentManagerBase; import org.jetbrains.annotations.NotNull; -import java.util.List; - final class MockDocumentCommitProcessor implements DocumentCommitProcessor { @Override public void commitSynchronously(@NotNull Document document, @NotNull Project project, @NotNull PsiFile psiFile) { @@ -22,8 +19,7 @@ final class MockDocumentCommitProcessor implements DocumentCommitProcessor { @NotNull PsiDocumentManagerBase documentManager, @NotNull Document document, @NotNull Object reason, - @NotNull ModalityState modality, - @NotNull List cachedViewProviders) { + @NotNull ModalityState modality) { } } diff --git a/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitProcessor.java b/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitProcessor.java index dcdb0842f270..d89a306751f8 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitProcessor.java +++ b/platform/core-impl/src/com/intellij/psi/impl/DocumentCommitProcessor.java @@ -4,14 +4,11 @@ package com.intellij.psi.impl; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.editor.Document; import com.intellij.openapi.project.Project; -import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; -import java.util.List; - @ApiStatus.Internal public interface DocumentCommitProcessor { void commitSynchronously(@NotNull Document document, @NotNull Project project, @NotNull PsiFile psiFile); @@ -20,6 +17,5 @@ public interface DocumentCommitProcessor { @NotNull PsiDocumentManagerBase documentManager, @NotNull Document document, @NonNls @NotNull Object reason, - @NotNull ModalityState modality, - @NotNull List cachedViewProviders); + @NotNull ModalityState modality); } diff --git a/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java b/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java index 34a3ae7e7846..d6a6db78c2f2 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java +++ b/platform/core-impl/src/com/intellij/psi/impl/PsiDocumentManagerBase.java @@ -693,7 +693,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen // this client obviously expects all documents to be committed ASAP even inside modal dialog for (Document document : myUncommittedDocuments) { try (AccessToken ignore = SlowOperations.knownIssue("IJPL-162971")) { - retainProviderAndCommitAsync(document, "re-added because performWhenAllCommitted(" + modality + ") was called", modality); + commitAsync(document, "re-added because performWhenAllCommitted(" + modality + ") was called", modality); } } } @@ -1077,7 +1077,7 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen commitDocument(document); } else if (!document.isInBulkUpdate() && myPerformBackgroundCommit) { - retainProviderAndCommitAsync(document, event, ModalityState.defaultModalityState()); + commitAsync(document, event, ModalityState.defaultModalityState()); } } else { @@ -1092,17 +1092,17 @@ public abstract class PsiDocumentManagerBase extends PsiDocumentManager implemen @Override public void bulkUpdateFinished(@NotNull Document document) { - retainProviderAndCommitAsync(document, "Bulk update finished", ModalityState.defaultModalityState()); + commitAsync(document, "Bulk update finished", ModalityState.defaultModalityState()); } - private void retainProviderAndCommitAsync(@NotNull Document document, - @NotNull Object reason, - @NotNull ModalityState modality) { + private void commitAsync(@NotNull Document document, + @NotNull Object reason, + @NotNull ModalityState modality) { List viewProviders = getCachedViewProviders(document); if (FileViewProviderUtil.isEventSystemEnabled(viewProviders)) { ThreadingAssertions.assertEventDispatchThread(); // make cached provider non-gcable temporarily (until commit end) to avoid surprising getCachedProvider()==null - myDocumentCommitProcessor.commitAsynchronously(myProject, this, document, reason, modality, viewProviders); + myDocumentCommitProcessor.commitAsynchronously(myProject, this, document, reason, modality); } } diff --git a/platform/ide-core-impl/src/com/intellij/psi/impl/DocumentCommitThread.kt b/platform/ide-core-impl/src/com/intellij/psi/impl/DocumentCommitThread.kt index 41f05d26fcc3..09463516bcd5 100644 --- a/platform/ide-core-impl/src/com/intellij/psi/impl/DocumentCommitThread.kt +++ b/platform/ide-core-impl/src/com/intellij/psi/impl/DocumentCommitThread.kt @@ -1,9 +1,9 @@ // Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.psi.impl -import com.intellij.codeInsight.multiverse.isEventSystemEnabled import com.intellij.diagnostic.PluginException import com.intellij.lang.FileASTNode +import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ReadAction @@ -12,6 +12,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.ex.DocumentEx +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProcessCanceledException import com.intellij.openapi.progress.ProgressIndicatorProvider @@ -20,48 +21,46 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.ProperTextRange import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil -import com.intellij.platform.util.coroutines.childScope import com.intellij.psi.* import com.intellij.psi.text.BlockSupport import com.intellij.util.SmartList -import com.intellij.util.concurrency.waitAllTasksExecuted +import com.intellij.util.concurrency.BoundedTaskExecutor +import com.intellij.util.concurrency.SequentialTaskExecutor +import com.intellij.util.concurrency.annotations.RequiresReadLock import com.intellij.util.ui.EDT -import kotlinx.coroutines.* import org.jetbrains.annotations.ApiStatus import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.TestOnly import java.lang.ref.Reference +import java.lang.ref.WeakReference import java.util.concurrent.Callable import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException private val LOG = logger() @ApiStatus.Internal -class DocumentCommitThread(coroutineScope: CoroutineScope) : DocumentCommitProcessor { - @OptIn(ExperimentalCoroutinesApi::class) - private val childScope = coroutineScope.childScope("Document Commit Pool", Dispatchers.Default.limitedParallelism(1)) - +class DocumentCommitThread : DocumentCommitProcessor, Disposable { @Volatile private var isDisposed = false + private val myExecutor = SequentialTaskExecutor.createSequentialApplicationPoolExecutor("Document Commit Pool") companion object { @JvmStatic fun getInstance(): DocumentCommitThread = service() as DocumentCommitThread } - init { - coroutineScope.coroutineContext.job.invokeOnCompletion { - isDisposed = true - } + override fun dispose() { + (myExecutor as BoundedTaskExecutor).clearAndCancelAll() + isDisposed = true } override fun commitAsynchronously( project: Project, documentManager: PsiDocumentManagerBase, document: Document, - reason: @NonNls Any, + reason: Any, modality: ModalityState, - cachedViewProviders: List, ) { assert(!isDisposed) { "already disposed" } if (!project.isInitialized()) { @@ -70,23 +69,15 @@ class DocumentCommitThread(coroutineScope: CoroutineScope) : DocumentCommitProce require(documentManager.myProject === project) { "Wrong project: $project; expected: ${documentManager.myProject}" } - assert(cachedViewProviders.isEventSystemEnabled()) { - "Asynchronous commit is only supported for physical PSI, " + - "document=$document, cachedViewProviders=$cachedViewProviders (${cachedViewProviders.map { it.javaClass }})" - } TransactionGuard.getInstance().assertWriteSafeContext(modality) - val task = CommitTask(project, document, reason, modality, documentManager.getLastCommittedText(document), cachedViewProviders) + val task = CommitTask(project, document, reason, modality) ReadAction .nonBlocking(Callable { commitUnderProgress(task, synchronously = false, documentManager) }) - .expireWhen { isDisposed || project.isDisposed() || !documentManager.isInUncommittedSet(document) || !task.isStillValid() } + .expireWhen { isDisposed || project.isDisposed() || task.stillValidDocument().let { document -> document == null || !documentManager.isInUncommittedSet(document) || FileDocumentManager.getInstance().getFile(document)?.isValid != true } } .coalesceBy(task) .finishOnUiThread(modality) { it() } - .submit { - childScope.launch { - it.run() - } - } + .submit(myExecutor) } override fun commitSynchronously(document: Document, project: Project, psiFile: PsiFile) { @@ -97,48 +88,54 @@ class DocumentCommitThread(coroutineScope: CoroutineScope) : DocumentCommitProce } val documentManager = PsiDocumentManager.getInstance(project) as PsiDocumentManagerBase - val task = CommitTask(project, document, "Sync commit", ModalityState.defaultModalityState(), documentManager.getLastCommittedText(document), listOf(psiFile.getViewProvider())) + val task = CommitTask(project, document, "Sync commit", ModalityState.defaultModalityState()) commitUnderProgress(task, synchronously = true, documentManager)() } + @RequiresReadLock // returns finish commit Runnable (to be invoked later in EDT) or null on failure private fun commitUnderProgress(task: CommitTask, synchronously: Boolean, documentManager: PsiDocumentManagerBase): () -> Unit { - val document = task.document - val project = task.project + if (!synchronously) { + ApplicationManager.getApplication().assertIsNonDispatchThread() + } + ApplicationManager.getApplication().assertReadAccessAllowed() + val document = task.myDocumentRef.get()?: return {} + val project = task.myProject val finishProcessors = SmartList() val reparseInjectedProcessors = SmartList() - val viewProviders = documentManager.getCachedViewProviders(document) - if (viewProviders.isEmpty()) { - finishProcessors.add(handleCommitWithoutPsi(task, documentManager)) + + val psiManager = PsiManagerEx.getInstanceEx(project) + val virtualFile = FileDocumentManager.getInstance().getFile(document) + val viewProvider = if (virtualFile == null) null else psiManager.findViewProvider(virtualFile) + if (viewProvider == null) { + finishProcessors.add(handleCommitWithoutPsi(task, document, documentManager)) } else { // While we were messing around transferring things to background thread, the ViewProvider can become obsolete // when, e.g., a virtual file was renamed. // Store new provider to retain it from GC - task.cachedViewProviders = viewProviders + task.cachedViewProvider = viewProvider // todo IJPL-339 check if this is correct - for (viewProvider in viewProviders) { - for (file in viewProvider.getAllFiles()) { - val oldFileNode = file.getNode() - if (oldFileNode == null) { - throw AssertionError("No node for " + file.javaClass + " in " + file.getViewProvider().javaClass + - " of size " + StringUtil.formatFileSize(document.textLength.toLong()) + - " (is too large = " + SingleRootFileViewProvider - .isTooLargeForIntelligence(viewProvider.getVirtualFile(), document.textLength.toLong()) + ")") - } - val changedPsiRange = ChangedPsiRangeUtil.getChangedPsiRange( - file, - document, - task.myLastCommittedText, - document.getImmutableCharSequence(), - ) - if (changedPsiRange != null) { - val finishProcessor = doCommit(task, file, oldFileNode, changedPsiRange, reparseInjectedProcessors, documentManager) - finishProcessors.add(finishProcessor) - } + for (psiFile in viewProvider.getAllFiles()) { + val oldFileNode = psiFile.getNode() + if (oldFileNode == null) { + throw AssertionError("No node for " + psiFile.javaClass + " in " + psiFile.getViewProvider().javaClass + + " of size " + StringUtil.formatFileSize(document.textLength.toLong()) + + " (is too large = " + SingleRootFileViewProvider + .isTooLargeForIntelligence(viewProvider.getVirtualFile(), document.textLength.toLong()) + ")") + } + val changedPsiRange = ChangedPsiRangeUtil.getChangedPsiRange( + psiFile, + document, + task.myLastCommittedText, + document.getImmutableCharSequence(), + ) + if (changedPsiRange != null) { + val finishProcessor = doCommit(task, synchronously, document, psiFile, oldFileNode, changedPsiRange, reparseInjectedProcessors, documentManager) + finishProcessors.add(finishProcessor) } } } @@ -148,16 +145,16 @@ class DocumentCommitThread(coroutineScope: CoroutineScope) : DocumentCommitProce return@task } - val success = documentManager.finishCommit(document, finishProcessors, reparseInjectedProcessors, synchronously, task.reason) + val success = documentManager.finishCommit(document, finishProcessors, reparseInjectedProcessors, synchronously, task.myReason) if (synchronously) { assert(success) } if (synchronously || success) { assert(!documentManager.isInUncommittedSet(document)) } - if (!success && viewProviders.isEventSystemEnabled()) { + if (!success && viewProvider?.isEventSystemEnabled() == true) { // add a document back to the queue - commitAsynchronously(project, documentManager, document, "Re-added back", task.myCreationModality, viewProviders) + commitAsynchronously(project, documentManager, document, "Re-added back", task.myCreationModality) } } } @@ -167,178 +164,200 @@ class DocumentCommitThread(coroutineScope: CoroutineScope) : DocumentCommitProce // NB: failures applying EDT tasks are not handled - i.e., failed documents are added back to the queue and the method returns @TestOnly fun waitForAllCommits(timeout: Long, timeUnit: TimeUnit) { + val boundedTaskExecutor = myExecutor as BoundedTaskExecutor if (!ApplicationManager.getApplication().isDispatchThread()) { - while (childScope.coroutineContext.job.children.any()) { - waitAllTasksExecuted(childScope, timeout, timeUnit) - } + boundedTaskExecutor.waitAllTasksExecuted(timeout, timeUnit) return } assert(!ApplicationManager.getApplication().isWriteAccessAllowed()) EDT.dispatchAllInvocationEvents() - while (childScope.coroutineContext.job.children.any()) { - waitAllTasksExecuted(childScope, timeout, timeUnit) + val deadLine = System.nanoTime() + timeUnit.toNanos(timeout) + while (!boundedTaskExecutor.isEmpty) { + try { + boundedTaskExecutor.waitAllTasksExecuted(10, TimeUnit.MILLISECONDS) + } + catch (e: TimeoutException) { + if (System.nanoTime() > deadLine) { + throw e + } + } EDT.dispatchAllInvocationEvents() } } -} -private class CommitTask( - val project: Project, - val document: Document, - val reason: @NonNls Any, - val myCreationModality: ModalityState, - val myLastCommittedText: CharSequence, - // to retain viewProvider to avoid surprising getCachedProvider() == null half-way through commit - @field:Volatile var cachedViewProviders: List, -) { - // store initial document modification sequence here to check if it changed later before commit in EDT - private val modificationSequence = (document as DocumentEx).modificationSequence + private class CommitTask { + val myProject: Project + val myReason: @NonNls Any + val myCreationModality: ModalityState + val myDocumentRef: Reference + val myLastCommittedText: CharSequence + // store initial document modification sequence here to check if it changed later before commit in EDT + private val myModificationSequence: Int + @Volatile var cachedViewProvider: FileViewProvider? = null - override fun toString(): @NonNls String { - val reasonInfo = " task reason: " + StringUtil.first(reason.toString(), 180, true) + - (if (isStillValid()) "" else "; changed: old seq=$modificationSequence, new seq=${(document as DocumentEx).modificationSequence}") - val contextInfo = " modality: $myCreationModality" - return System.identityHashCode(this).toString() + "; " + contextInfo + reasonInfo - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is CommitTask) return false - - return document == other.document && project == other.project - } - - override fun hashCode(): Int = 31 * document.hashCode() + project.hashCode() - - fun isStillValid(): Boolean = (document as DocumentEx).modificationSequence == modificationSequence -} - -private fun handleCommitWithoutPsi( - task: CommitTask, - documentManager: PsiDocumentManagerBase, -): BooleanRunnable { - return BooleanRunnable { - if (task.isStillValid() && documentManager.getCachedViewProviders(task.document).isEmpty()) { - documentManager.handleCommitWithoutPsi(task.document) - true + constructor( + project: Project, + document: Document, + reason: @NonNls Any, + creationModality: ModalityState, + ) { + myProject = project + myReason = reason.toString() // convert to string to avoid leaking document in case somebody passed a Document or DocumentEvent here + myCreationModality = creationModality + myDocumentRef = WeakReference(document) + myLastCommittedText = PsiDocumentManager.getInstance(project).getLastCommittedText(document) + myModificationSequence = (document as DocumentEx).modificationSequence } - else { - false + override fun toString(): @NonNls String { + val document = stillValidDocument() + val reasonInfo = " task reason: " + StringUtil.first(myReason.toString(), 180, true) + + document + "; changed: old seq=$myModificationSequence, new seq=${(document as? DocumentEx)?.modificationSequence}" + val contextInfo = " modality: $myCreationModality" + return System.identityHashCode(this).toString() + "; " + contextInfo + reasonInfo + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CommitTask) return false + + return myDocumentRef.get() == other.myDocumentRef.get() && myProject == other.myProject + } + + override fun hashCode(): Int = 31 * myLastCommittedText.hashCode() + myProject.hashCode() + // return null if the document is changed or gced + fun stillValidDocument(): Document? { + val document = myDocumentRef.get() + return if (document is DocumentEx && document.modificationSequence == myModificationSequence) { + document + } else { + null + } } } -} -// returns runnable to execute under the write action in AWT to finish the commit -private fun doCommit( - task: CommitTask, - file: PsiFile, - oldFileNode: FileASTNode, - changedPsiRange: ProperTextRange, - outReparseInjectedProcessors: MutableList, - documentManager: PsiDocumentManagerBase, -): BooleanRunnable { - val document = task.document - val newDocumentText = document.getImmutableCharSequence() + // returns runnable to execute under the write action in AWT to finish the commit + @RequiresReadLock + private fun doCommit( + task: CommitTask, + synchronously: Boolean, + document: Document, + psiFile: PsiFile, + oldFileNode: FileASTNode, + changedPsiRange: ProperTextRange, + outReparseInjectedProcessors: MutableList, + documentManager: PsiDocumentManagerBase, + ): BooleanRunnable { + if (!synchronously) { + ApplicationManager.getApplication().assertIsNonDispatchThread() + } + ApplicationManager.getApplication().assertReadAccessAllowed() + val newDocumentText = document.getImmutableCharSequence() - val data = document.getUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY) - if (data != null) { - document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null) - file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, data) - } + val data = document.getUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY) + if (data != null) { + document.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null) + psiFile.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, data) + } - val diffLog: DiffLog - val indicator = ProgressIndicatorProvider.getGlobalProgressIndicator() ?: EmptyProgressIndicator() - try { - val result = BlockSupportImpl.reparse(file, oldFileNode, changedPsiRange, newDocumentText, indicator, task.myLastCommittedText) - diffLog = result.log + val diffLog: DiffLog + val indicator = ProgressIndicatorProvider.getGlobalProgressIndicator() ?: EmptyProgressIndicator() + try { + val result = BlockSupportImpl.reparse(psiFile, oldFileNode, changedPsiRange, newDocumentText, indicator, task.myLastCommittedText) + diffLog = result.log + + val injectedRunnables = documentManager.reparseChangedInjectedFragments( + document, + psiFile, + changedPsiRange, + indicator, + result.oldRoot, + result.newRoot, + ) + outReparseInjectedProcessors.addAll(injectedRunnables) + } + catch (e: ProcessCanceledException) { + throw e + } + catch (e: Throwable) { + LOG.error(e) + return BooleanRunnable { + documentManager.forceReload(psiFile.getViewProvider().getVirtualFile(), listOf(psiFile.getViewProvider())) + true + } + } - val injectedRunnables = documentManager.reparseChangedInjectedFragments( - document, - file, - changedPsiRange, - indicator, - result.oldRoot, - result.newRoot, - ) - outReparseInjectedProcessors.addAll(injectedRunnables) - } - catch (e: ProcessCanceledException) { - throw e - } - catch (e: Throwable) { - LOG.error(e) return BooleanRunnable { - documentManager.forceReload(file.getViewProvider().getVirtualFile(), listOf(file.getViewProvider())) + val viewProvider = psiFile.getViewProvider() //todo IJPL-339 figure out correct check here + if (task.stillValidDocument() == null || viewProvider !in documentManager.getCachedViewProviders(document)) { // optimistic locking failed + return@BooleanRunnable false + } + + if (!ApplicationManager.getApplication().isWriteAccessAllowed() && documentManager.isEventSystemEnabled(document)) { + val vFile = viewProvider.getVirtualFile() + LOG.error("Write action expected" + "; document=" + document + "; file=" + psiFile + " of " + psiFile.javaClass + "; file.valid=" + psiFile.isValid() + "; file.eventSystemEnabled=" + viewProvider.isEventSystemEnabled() + "; viewProvider=" + viewProvider + " of " + viewProvider.javaClass + "; language=" + psiFile.getLanguage() + "; vFile=" + vFile + " of " + vFile.javaClass + "; free-threaded=" + AbstractFileViewProvider.isFreeThreaded(viewProvider)) + } + + diffLog.doActualPsiChange(psiFile) + + assertAfterCommit(document, psiFile, oldFileNode) // just to make an impression the field is used + Reference.reachabilityFence(task.cachedViewProvider) true } } - return BooleanRunnable { - val viewProvider = file.getViewProvider() - //todo IJPL-339 figure out correct check here - if (!task.isStillValid() || viewProvider !in documentManager.getCachedViewProviders(document)) { - // optimistic locking failed - return@BooleanRunnable false + private fun handleCommitWithoutPsi( + task: CommitTask, + document: Document, + documentManager: PsiDocumentManagerBase, + ): BooleanRunnable { + return BooleanRunnable { + if (task.stillValidDocument() != null && documentManager.getCachedViewProviders(document).isEmpty()) { + documentManager.handleCommitWithoutPsi(document) + true + } + else { + false + } + } + } + + private fun assertAfterCommit(document: Document, psiFile: PsiFile, oldFileNode: FileASTNode) { + if (oldFileNode.getTextLength() == document.textLength) { + return } - if (!ApplicationManager.getApplication().isWriteAccessAllowed() && documentManager.isEventSystemEnabled(document)) { - val vFile = viewProvider.getVirtualFile() - LOG.error("Write action expected" + - "; document=" + document + - "; file=" + file + " of " + file.javaClass + - "; file.valid=" + file.isValid() + - "; file.eventSystemEnabled=" + viewProvider.isEventSystemEnabled() + - "; viewProvider=" + viewProvider + " of " + viewProvider.javaClass + - "; language=" + file.getLanguage() + - "; vFile=" + vFile + " of " + vFile.javaClass + - "; free-threaded=" + AbstractFileViewProvider.isFreeThreaded(viewProvider)) + val documentText = document.text + val fileText = psiFile.getText() + val sameText = fileText == documentText + val errorMessage = "commitDocument() left PSI inconsistent: " + DebugUtil.diagnosePsiDocumentInconsistency(psiFile, document) + + "; node.length=" + oldFileNode.getTextLength() + + "; doc.text" + (if (sameText) "==" else "!=") + "file.text" + + "; file name:" + psiFile.getName() + + "; type:" + psiFile.getFileType() + + "; lang:" + psiFile.getLanguage() + PluginException.logPluginError(LOG, errorMessage, null, psiFile.getLanguage().javaClass) + + psiFile.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, true) + try { + val blockSupport = BlockSupport.getInstance(psiFile.getProject()) + val diffLog = blockSupport.reparseRange( + psiFile, + psiFile.getNode(), + TextRange(0, documentText.length), + documentText, + StandardProgressIndicatorBase(), + oldFileNode.getText(), + ) + diffLog.doActualPsiChange(psiFile) + + if (oldFileNode.getTextLength() != document.textLength) { + PluginException.logPluginError(LOG, "PSI is broken beyond repair in: $psiFile", null, psiFile.getLanguage().javaClass) + } } - - diffLog.doActualPsiChange(file) - - assertAfterCommit(document, file, oldFileNode) - // just to make an impression the field is used - Reference.reachabilityFence(task.cachedViewProviders) - true - } -} - -private fun assertAfterCommit(document: Document, file: PsiFile, oldFileNode: FileASTNode) { - if (oldFileNode.getTextLength() == document.textLength) { - return - } - - val documentText = document.text - val fileText = file.getText() - val sameText = fileText == documentText - val errorMessage = "commitDocument() left PSI inconsistent: " + DebugUtil.diagnosePsiDocumentInconsistency(file, document) + - "; node.length=" + oldFileNode.getTextLength() + - "; doc.text" + (if (sameText) "==" else "!=") + "file.text" + - "; file name:" + file.getName() + - "; type:" + file.getFileType() + - "; lang:" + file.getLanguage() - PluginException.logPluginError(LOG, errorMessage, null, file.getLanguage().javaClass) - - file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, true) - try { - val blockSupport = BlockSupport.getInstance(file.getProject()) - val diffLog = blockSupport.reparseRange( - file, - file.getNode(), - TextRange(0, documentText.length), - documentText, - StandardProgressIndicatorBase(), - oldFileNode.getText(), - ) - diffLog.doActualPsiChange(file) - - if (oldFileNode.getTextLength() != document.textLength) { - PluginException.logPluginError(LOG, "PSI is broken beyond repair in: $file", null, file.getLanguage().javaClass) + finally { + psiFile.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null) } - } - finally { - file.putUserData(BlockSupport.DO_NOT_REPARSE_INCREMENTALLY, null) } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/PsiChangeHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/PsiChangeHandler.java index b4d326fa1661..73a535796ecf 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/PsiChangeHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/daemon/impl/PsiChangeHandler.java @@ -111,7 +111,7 @@ final class PsiChangeHandler extends PsiTreeChangeAdapter implements Runnable { private PsiFile getRawCachedPsiFile(@NotNull Document document) { VirtualFile virtualFile = FileDocumentManager.getInstance().getFile(document); - return virtualFile == null ? null : TextEditorBackgroundHighlighter.getCachedFileToHighlight(myProject, virtualFile, CodeInsightContexts.anyContext()); + return virtualFile == null || !virtualFile.isValid() ? null : TextEditorBackgroundHighlighter.getCachedFileToHighlight(myProject, virtualFile, CodeInsightContexts.anyContext()); } private void addChangesFromCompositeDirtyRange(@NotNull PsiFile psiFile,