doc commit thread optimizations (part of IJPL-50077 freezing on replace)

- use executor instead of spawning tens of thousands of hard-to-manage coroutines littering the thread dump
- do not hard-retain changed documents, allowing them to be gced, because if nobody needs the document, it needn't be committed
- ditch cachedViewProviders because it leaks psi/Document while sitting in the executor queue; instead, cache the view provider right before the commit started but not sooner

GitOrigin-RevId: 7c9068cd7fe168e552970bb34493c311c1f376b8
This commit is contained in:
Alexey Kudravtsev
2025-07-14 17:26:26 +00:00
committed by intellij-monorepo-bot
parent cdb4b9a22b
commit eb3c74590b
5 changed files with 228 additions and 217 deletions
@@ -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<FileViewProvider> cachedViewProviders) {
@NotNull ModalityState modality) {
}
}
@@ -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<FileViewProvider> cachedViewProviders);
@NotNull ModalityState modality);
}
@@ -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<FileViewProvider> 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);
}
}
@@ -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<DocumentCommitThread>()
@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<DocumentCommitProcessor>() 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<FileViewProvider>,
) {
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<BooleanRunnable>()
val reparseInjectedProcessors = SmartList<BooleanRunnable>()
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<FileViewProvider>,
) {
// 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<Document>
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<BooleanRunnable>,
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<BooleanRunnable>,
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)
}
}
@@ -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,