tnorbye / kdoc-formatter

Reformats Kotlin KDoc comments, reflowing text and other cleanup, both via IDE plugin and command line utility
Apache License 2.0
73 stars 3 forks source link

With latest IntelliJ & plugin version 1.5.6, getting IllegalStateException: Attempt to modify PSI for non-committed Document! #87

Open matthewadams opened 1 year ago

matthewadams commented 1 year ago

Ever since updating to the latest IntelliJ version, we've been getting the exception with root cause message Attempt to modify PSI for non-committed Document!.

IntelliJ version info:

image

Plugin version: 1.5.6

image

Getting exception:

Got unexpected exception during formatting file:///Users/matthewadams/dev/artesion/site-microservice/site-microservice-support/src/main/kotlin/app/site/cqrs/read/canonical/PersistenceSupport.kt

java.util.concurrent.ExecutionException: java.lang.IllegalStateException: Attempt to modify PSI for non-committed Document!
    at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
    at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.checkStop(AbstractLayoutCodeProcessor.java:485)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.performFileProcessing(AbstractLayoutCodeProcessor.java:479)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.lambda$iteration$3(AbstractLayoutCodeProcessor.java:435)
    at com.intellij.openapi.project.DumbService.withAlternativeResolveEnabled(DumbService.java:354)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.iteration(AbstractLayoutCodeProcessor.java:435)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.lambda$process$10(AbstractLayoutCodeProcessor.java:522)
    at com.intellij.codeInsight.actions.FileRecursiveIterator.lambda$processAll$4(FileRecursiveIterator.java:69)
    at com.intellij.openapi.roots.impl.FileIndexBase.lambda$toContentIteratorEx$0(FileIndexBase.java:82)
    at com.intellij.openapi.roots.impl.FileIndexBase$1.visitFileEx(FileIndexBase.java:65)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:295)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.vfs.VfsUtilCore.visitChildrenRecursively(VfsUtilCore.java:327)
    at com.intellij.openapi.roots.impl.FileIndexBase.iterateContentUnderDirectory(FileIndexBase.java:46)
    at com.intellij.openapi.roots.impl.ProjectFileIndexImpl.iterateContentUnderDirectory(ProjectFileIndexImpl.java:35)
    at com.intellij.openapi.roots.impl.FileIndexBase.iterateContentUnderDirectory(FileIndexBase.java:87)
    at com.intellij.openapi.roots.impl.ProjectFileIndexImpl.iterateContentUnderDirectory(ProjectFileIndexImpl.java:35)
    at com.intellij.codeInsight.actions.FileRecursiveIterator.processAll(FileRecursiveIterator.java:64)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.process(AbstractLayoutCodeProcessor.java:520)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor.processFilesUnderProgress(AbstractLayoutCodeProcessor.java:371)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor.lambda$runProcessFiles$1(AbstractLayoutCodeProcessor.java:327)
    at com.intellij.openapi.progress.impl.CoreProgressManager$1.run(CoreProgressManager.java:252)
    at com.intellij.openapi.progress.impl.CoreProgressManager.startTask(CoreProgressManager.java:429)
    at com.intellij.openapi.progress.impl.ProgressManagerImpl.startTask(ProgressManagerImpl.java:114)
    at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$runProcessWithProgressSynchronously$9(CoreProgressManager.java:513)
    at com.intellij.openapi.progress.impl.ProgressRunner.lambda$new$0(ProgressRunner.java:84)
    at com.intellij.openapi.progress.impl.ProgressRunner.lambda$submit$3(ProgressRunner.java:252)
    at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$runProcess$2(CoreProgressManager.java:186)
    at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeProcessUnderProgress$13(CoreProgressManager.java:604)
    at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:679)
    at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:635)
    at com.intellij.openapi.progress.impl.CoreProgressManager.executeProcessUnderProgress(CoreProgressManager.java:603)
    at com.intellij.openapi.progress.impl.ProgressManagerImpl.executeProcessUnderProgress(ProgressManagerImpl.java:60)
    at com.intellij.openapi.progress.impl.CoreProgressManager.runProcess(CoreProgressManager.java:173)
    at com.intellij.openapi.progress.impl.ProgressRunner.lambda$submit$4(ProgressRunner.java:252)
    at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base/java.util.concurrent.Executors$PrivilegedThreadFactory$1$1.run(Executors.java:702)
    at java.base/java.util.concurrent.Executors$PrivilegedThreadFactory$1$1.run(Executors.java:699)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.util.concurrent.Executors$PrivilegedThreadFactory$1.run(Executors.java:699)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.IllegalStateException: Attempt to modify PSI for non-committed Document!
    at com.intellij.pom.core.impl.PomModelImpl.startTransaction(PomModelImpl.java:266)
    at com.intellij.pom.core.impl.PomModelImpl.lambda$runTransaction$2(PomModelImpl.java:96)
    at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$executeNonCancelableSection$3(CoreProgressManager.java:222)
    at com.intellij.openapi.progress.impl.CoreProgressManager.registerIndicatorAndRun(CoreProgressManager.java:679)
    at com.intellij.openapi.progress.impl.CoreProgressManager.computeUnderProgress(CoreProgressManager.java:635)
    at com.intellij.openapi.progress.impl.CoreProgressManager.lambda$computeInNonCancelableSection$4(CoreProgressManager.java:230)
    at com.intellij.openapi.progress.Cancellation.computeInNonCancelableSection(Cancellation.java:99)
    at com.intellij.openapi.progress.impl.CoreProgressManager.computeInNonCancelableSection(CoreProgressManager.java:230)
    at com.intellij.openapi.progress.impl.CoreProgressManager.executeNonCancelableSection(CoreProgressManager.java:221)
    at com.intellij.pom.core.impl.PomModelImpl.runTransaction(PomModelImpl.java:93)
    at com.intellij.psi.impl.source.tree.ChangeUtil.prepareAndRunChangeAction(ChangeUtil.java:142)
    at com.intellij.psi.impl.source.tree.CompositeElement.replaceChild(CompositeElement.java:625)
    at com.intellij.psi.impl.source.codeStyle.CodeEditUtil.replaceChild(CodeEditUtil.java:162)
    at com.intellij.psi.impl.source.tree.CompositeElement.replaceChildInternal(CompositeElement.java:455)
    at com.intellij.psi.impl.source.tree.SharedImplUtil.doReplace(SharedImplUtil.java:196)
    at com.intellij.psi.impl.source.tree.LazyParseablePsiElement.replace(LazyParseablePsiElement.java:232)
    at kdocformatter.plugin.KDocPostFormatProcessor.processElement(KDocPostFormatProcessor.kt:30)
    at kdocformatter.plugin.KDocPostFormatProcessor.processText(KDocPostFormatProcessor.kt:44)
    at com.intellij.psi.impl.source.codeStyle.CoreCodeStyleUtil.postProcessText(CoreCodeStyleUtil.java:104)
    at com.intellij.formatting.service.CoreFormattingService.lambda$formatRanges$0(CoreFormattingService.java:64)
    at com.intellij.psi.impl.source.codeStyle.CoreCodeStyleUtil.postProcessRanges(CoreCodeStyleUtil.java:94)
    at com.intellij.formatting.service.CoreFormattingService.formatRanges(CoreFormattingService.java:64)
    at com.intellij.formatting.service.FormattingServiceUtil.formatRanges(FormattingServiceUtil.java:93)
    at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.reformatText(CodeStyleManagerImpl.java:167)
    at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.reformatText(CodeStyleManagerImpl.java:132)
    at com.intellij.psi.impl.source.codeStyle.CodeStyleManagerImpl.reformatText(CodeStyleManagerImpl.java:114)
    at com.google.googlejavaformat.intellij.CodeStyleManagerDecorator.reformatText(CodeStyleManagerDecorator.java:96)
    at com.google.googlejavaformat.intellij.GoogleJavaFormatCodeStyleManager.reformatText(GoogleJavaFormatCodeStyleManager.java:71)
    at com.intellij.codeInsight.actions.ReformatCodeProcessor.lambda$doReformat$5(ReformatCodeProcessor.java:196)
    at com.intellij.util.SlowOperations.allowSlowOperations(SlowOperations.java:167)
    at com.intellij.codeInsight.actions.ReformatCodeProcessor.lambda$doReformat$6(ReformatCodeProcessor.java:186)
    at com.intellij.openapi.editor.ex.util.EditorScrollingPositionKeeper.perform(EditorScrollingPositionKeeper.java:100)
    at com.intellij.codeInsight.actions.ReformatCodeProcessor.doReformat(ReformatCodeProcessor.java:186)
    at com.intellij.codeInsight.actions.ReformatCodeProcessor.lambda$prepareTask$2(ReformatCodeProcessor.java:134)
    at com.intellij.application.options.CodeStyle.doWithTemporarySettings(CodeStyle.java:338)
    at com.intellij.codeInsight.actions.ReformatCodeProcessor.lambda$prepareTask$3(ReformatCodeProcessor.java:130)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.lambda$performFileProcessing$7(AbstractLayoutCodeProcessor.java:476)
    at com.intellij.openapi.command.WriteCommandAction$BuilderImpl.lambda$doRunWriteCommandAction$1(WriteCommandAction.java:150)
    at com.intellij.openapi.application.impl.ApplicationImpl.runWriteAction(ApplicationImpl.java:980)
    at com.intellij.openapi.command.WriteCommandAction$BuilderImpl.lambda$doRunWriteCommandAction$2(WriteCommandAction.java:148)
    at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:219)
    at com.intellij.openapi.command.impl.CoreCommandProcessor.executeCommand(CoreCommandProcessor.java:184)
    at com.intellij.openapi.command.WriteCommandAction$BuilderImpl.doRunWriteCommandAction(WriteCommandAction.java:157)
    at com.intellij.openapi.command.WriteCommandAction$BuilderImpl.run(WriteCommandAction.java:124)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor$ProcessingTask.lambda$performFileProcessing$8(AbstractLayoutCodeProcessor.java:476)
    at com.intellij.openapi.application.TransactionGuardImpl.runWithWritingAllowed(TransactionGuardImpl.java:209)
    at com.intellij.openapi.application.TransactionGuardImpl.access$100(TransactionGuardImpl.java:21)
    at com.intellij.openapi.application.TransactionGuardImpl$1.run(TransactionGuardImpl.java:191)
    at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:838)
    at com.intellij.openapi.application.impl.ApplicationImpl$3.run(ApplicationImpl.java:454)
    at com.intellij.openapi.application.impl.LaterInvocator$1.run(LaterInvocator.java:97)
    at com.intellij.openapi.application.impl.FlushQueue.doRun(FlushQueue.java:74)
    at com.intellij.openapi.application.impl.FlushQueue.runNextEvent(FlushQueue.java:114)
    at com.intellij.openapi.application.impl.FlushQueue.flushNow(FlushQueue.java:36)
    at java.desktop/java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:318)
    at java.desktop/java.awt.EventQueue.dispatchEventImpl(EventQueue.java:779)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:730)
    at java.desktop/java.awt.EventQueue$4.run(EventQueue.java:724)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
    at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:86)
    at java.desktop/java.awt.EventQueue.dispatchEvent(EventQueue.java:749)
    at com.intellij.ide.IdeEventQueue.defaultDispatchEvent(IdeEventQueue.java:909)
    at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:756)
    at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$5(IdeEventQueue.java:437)
    at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:787)
    at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$6(IdeEventQueue.java:436)
    at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:113)
    at com.intellij.ide.IdeEventQueue.performActivity(IdeEventQueue.java:615)
    at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$7(IdeEventQueue.java:434)
    at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:838)
    at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:480)
    at com.intellij.ide.IdeEventQueue.pumpEventsForHierarchy(IdeEventQueue.java:956)
    at com.intellij.openapi.progress.util.ProgressWindow.lambda$startBlocking$4(ProgressWindow.java:215)
    at com.intellij.openapi.application.impl.ApplicationImpl.runUnlockingIntendedWrite(ApplicationImpl.java:864)
    at com.intellij.openapi.progress.util.ProgressWindow.lambda$startBlocking$5(ProgressWindow.java:210)
    at com.intellij.openapi.progress.util.ProgressWindow.executeInModalContext(ProgressWindow.java:191)
    at com.intellij.openapi.progress.util.ProgressWindow.startBlocking(ProgressWindow.java:208)
    at com.intellij.openapi.progress.impl.ProgressRunner.lambda$execFromEDT$6(ProgressRunner.java:329)
    at java.base/java.util.concurrent.CompletableFuture.uniAcceptNow(CompletableFuture.java:757)
    at java.base/java.util.concurrent.CompletableFuture.uniAcceptStage(CompletableFuture.java:735)
    at java.base/java.util.concurrent.CompletableFuture.thenAccept(CompletableFuture.java:2182)
    at com.intellij.openapi.progress.impl.ProgressRunner.execFromEDT(ProgressRunner.java:326)
    at com.intellij.openapi.progress.impl.ProgressRunner.submit(ProgressRunner.java:267)
    at com.intellij.openapi.progress.impl.ProgressRunner.submitAndGet(ProgressRunner.java:193)
    at com.intellij.openapi.application.impl.ApplicationImpl.runProcessWithProgressSynchronously(ApplicationImpl.java:420)
    at com.intellij.openapi.progress.impl.CoreProgressManager.runProcessWithProgressSynchronously(CoreProgressManager.java:524)
    at com.intellij.openapi.progress.impl.ProgressManagerImpl.runProcessWithProgressSynchronously(ProgressManagerImpl.java:85)
    at com.intellij.openapi.progress.impl.CoreProgressManager.runProcessWithProgressSynchronously(CoreProgressManager.java:248)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor.runProcessFiles(AbstractLayoutCodeProcessor.java:325)
    at com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor.run(AbstractLayoutCodeProcessor.java:226)
    at com.intellij.codeInsight.actions.ReformatCodeAction.reformatDirectory(ReformatCodeAction.java:188)
    at com.intellij.codeInsight.actions.ReformatCodeAction.actionPerformed(ReformatCodeAction.java:118)
    at com.intellij.openapi.actionSystem.ex.ActionUtil.doPerformActionOrShowPopup(ActionUtil.java:327)
    at com.intellij.openapi.keymap.impl.ActionProcessor.performAction(ActionProcessor.java:47)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher$1.performAction(IdeKeyEventDispatcher.java:584)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.lambda$doPerformActionInner$9(IdeKeyEventDispatcher.java:706)
    at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:105)
    at com.intellij.openapi.application.TransactionGuardImpl.performUserActivity(TransactionGuardImpl.java:94)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.lambda$doPerformActionInner$10(IdeKeyEventDispatcher.java:706)
    at com.intellij.openapi.actionSystem.ex.ActionUtil.performDumbAwareWithCallbacks(ActionUtil.java:350)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.doPerformActionInner(IdeKeyEventDispatcher.java:703)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.processAction(IdeKeyEventDispatcher.java:647)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.processAction(IdeKeyEventDispatcher.java:595)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.processActionOrWaitSecondStroke(IdeKeyEventDispatcher.java:478)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.inInitState(IdeKeyEventDispatcher.java:467)
    at com.intellij.openapi.keymap.impl.IdeKeyEventDispatcher.dispatchKeyEvent(IdeKeyEventDispatcher.java:225)
    at com.intellij.ide.IdeEventQueue.dispatchKeyEvent(IdeEventQueue.java:815)
    at com.intellij.ide.IdeEventQueue._dispatchEvent(IdeEventQueue.java:750)
    at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$5(IdeEventQueue.java:437)
    at com.intellij.openapi.progress.impl.CoreProgressManager.computePrioritized(CoreProgressManager.java:787)
    at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$6(IdeEventQueue.java:436)
    at com.intellij.openapi.application.TransactionGuardImpl.performActivity(TransactionGuardImpl.java:113)
    at com.intellij.ide.IdeEventQueue.performActivity(IdeEventQueue.java:615)
    at com.intellij.ide.IdeEventQueue.lambda$dispatchEvent$7(IdeEventQueue.java:434)
    at com.intellij.openapi.application.impl.ApplicationImpl.runIntendedWriteActionOnCurrentThread(ApplicationImpl.java:838)
    at com.intellij.ide.IdeEventQueue.dispatchEvent(IdeEventQueue.java:480)
    at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:207)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:128)
    at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:117)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:113)
    at java.desktop/java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:105)
    at java.desktop/java.awt.EventDispatchThread.run(EventDispatchThread.java:92)
matthewadams commented 1 year ago

@tnorbye Do you think you might be able to have a look at this, please?

tnorbye commented 1 year ago

Sorry, I somehow missed this bug when it was filed in March.

I just took a look -- and I can't repro this, and I've never seen it happen. And from the thread dump, this is the formatting processor which is called from IntelliJ's own formatting hook. The formatting hook does use PSI to manipulate the document -- and the error message is "Attempt to modify PSI for non-committed Document!". So in theory, the fix would be for the plugin to call PsiDocumentManager.getInstance(project).commitDocument(document) before performing the PSI manipulation.

But -- this should be done before formatting begins, not in the callback for an individual PSI element -- so I would think the formatting action itself should do it, not a formatting processor. And I looked at a handful of other formatting processors; all of them are doing PSI manipulation, and none of them are trying to do document manipulation.

Do you have any other third party plugins installed? I wonder if one of them is doing document manipulation without committing it back to PSI, leaving the document dirty and triggering the above error on the next PSI manipulation.

matthewadams commented 1 year ago

Sorry, took me a while to get back to this. It looks like the culprit is the ktlint plugin. I'll see about filing an issue there & referencing this one.

tnorbye commented 1 year ago

Great, thanks for chasing this down!

matthewadams commented 1 year ago

Here's the issue I filed: https://github.com/nbadal/ktlint-intellij-plugin/issues/323

paul-dingemans commented 11 months ago

Hi Tor, I have been investigating this issue from the ktlint plugin perspective. Your suggestion about using PsiDocumentManager.getInstance(project).commitDocument(document) is indeed key to solving the issue. But I do believe that it needs to be fixed in the KDoc Formatter. I have created a reproducable scenario with two dummy formatters. One formatter modifies the PSI like KDoc Formatter does. The other formatter modifies the document by replacing the entire text of the document like the Ktlint plugin does.

The extensions are defined like this:

    <extensions defaultExtensionNs="com.intellij">
        <postFormatProcessor implementation="plugins.DummyKtlintPostFormatProcessor" />
        <postFormatProcessor implementation="plugins.DummyKdocPostFormatProcessor1" />
    </extensions>

The order of the postFormatterProcessors is important for reproducing the problem. Only when DummyKdocPostFormatProcessor1 runs after DummyKtlintPostFormatProcessor the problem with Attempt to modify PSI for non-committed Document! occurs. In real live it is out of control of the developer which of the plugins in invoked before the others. So it is also important to test the scenario with the plugins in different order.

The DummyKtlintPostFormatProcessor prepends the existing content of the document with a comment // Formatted with DummyKtlintPostFormatProcessor and is defined as follows:

package plugins

import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor

const val KTLINT_FORMATTER_COMMENT = "// Formatted with DummyKtlintPostFormatProcessor"

/**
 * Like the ktlint-intellij-plugin this post format processor replaces the entire content of the document instead of
 * manipulating the PSI.
 */
class DummyKtlintPostFormatProcessor : PostFormatProcessor {
    val className = this::class.simpleName

    override fun processElement(
        source: PsiElement,
        settings: CodeStyleSettings,
    ) = source

    override fun processText(
        psiFile: PsiFile,
        rangeToReformat: TextRange,
        settings: CodeStyleSettings,
    ): TextRange {
        val document = psiFile.viewProvider.document
        PsiDocumentManager
            .getInstance(psiFile.project)
            .doPostponedOperationsAndUnblockDocument(document)
        WriteCommandAction.runWriteCommandAction(psiFile.project) {
            println("$className start")
            document.setText("$KTLINT_FORMATTER_COMMENT\n${psiFile.text}")
            println("$className finished")
        }
        return rangeToReformat
    }
}

The DummyKdocPostFormatProcessor1 looks for a comment with text // Formatted with DummyKtlintPostFormatProcessor and alters that text. It is defined as follows and is comparable with current implementation of KDoc Formatter:

package plugins

import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.KtPsiFactory

/**
 * Like the current Kdoc Formatter this post format processor manipulates the PSI, but it does not commit document
 * changes and does not use the WriteCommandAction while manipulating the PSI. As a result this post formatter throws an
 * exception if it runs after [DummyKtlintPostFormatProcessor].
 */
class DummyKdocPostFormatProcessor1 : PostFormatProcessor {
    val className = this::class.simpleName

    override fun processElement(
        source: PsiElement,
        settings: CodeStyleSettings,
    ): PsiElement {
        return if (source is PsiComment && source.text == KTLINT_FORMATTER_COMMENT) {
            val newComment =
                KtPsiFactory(source.project)
                    .createComment("$KTLINT_FORMATTER_COMMENT (altered by $className)")
            source.replace(newComment)
        } else {
            source
        }
    }

    override fun processText(
        psiFile: PsiFile,
        rangeToReformat: TextRange,
        settings: CodeStyleSettings,
    ): TextRange {
        println("$className start")
        // Format all top-level comments in this range
        for (element in PsiTreeUtil.findChildrenOfType(psiFile, PsiComment::class.java)) {
            if (rangeToReformat.intersects(element.textRange)) {
                processElement(element, settings)
            }
        }
        println("$className start")
        return rangeToReformat
    }
}

Now run the plugin with setup above and start with a simple document containing text:

class Foo

After the first invocation of reformatting the document, it will be changed to:

// Formatted with DummyKtlintPostFormatProcessor
class Foo

At the second invocation of reformatting the document both the post formatters processors will be run and the Attempt to modify PSI for non-committed Document! exception is thrown.

When DummyKdocPostFormatProcessor1 runs after DummyKtlintPostFormatProcessor it has to ensure that the document does not contain any uncommitted changes before starting to change the PSI. Next to committing the document, it should also execute the PSI change within a WriteActionCommand. Replace DummyKdocPostFormatProcessor1 with DummyKdocPostFormatProcessor2 below:

package plugins

import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.KtPsiFactory

/**
 * Like the current Kdoc Formatter this post format processor manipulates the PSI, but it does only so after committing
 * the document changes and uses the WriteCommandAction while manipulating the PSI. As a result this post formatter
 * no longer throws an exception if it runs after [DummyKtlintPostFormatProcessor].
 */
class DummyKdocPostFormatProcessor2 : PostFormatProcessor {
    val className = this::class.simpleName

    override fun processElement(
        source: PsiElement,
        settings: CodeStyleSettings,
    ): PsiElement {
        return if (source is PsiComment && source.text == KTLINT_FORMATTER_COMMENT) {
            val newComment =
                KtPsiFactory(source.project)
                    .createComment("$KTLINT_FORMATTER_COMMENT (altered by $className)")
            source.replace(newComment)
        } else {
            source
        }
    }

    override fun processText(
        psiFile: PsiFile,
        rangeToReformat: TextRange,
        settings: CodeStyleSettings,
    ): TextRange {
        PsiDocumentManager
            .getInstance(psiFile.project)
            .commitDocument(psiFile.viewProvider.document)
        WriteCommandAction.runWriteCommandAction(psiFile.project) {
            println("$className start")
            // Format all top-level comments in this range
            for (element in PsiTreeUtil.findChildrenOfType(psiFile, PsiComment::class.java)) {
                if (rangeToReformat.intersects(element.textRange)) {
                    processElement(element, settings)
                }
            }
            println("$className finished")
        }
        return rangeToReformat
    }
}

Rerun the plugin and start again with file:

class Foo

After the first run of reformatting the document, it is changed to:

// Formatted with DummyKtlintPostFormatProcessor (altered by DummyKdocPostFormatProcessor2)
class Foo
paul-dingemans commented 11 months ago

Btw, ktlint plugin has been changed as well to prevent problems in case the KDoc Formatter runs before Ktlint formatter.

paul-dingemans commented 9 months ago

@matthewadams Did you have any chance to look into this?

tnorbye commented 8 months ago

No, apologies still haven't had a chance -- very busy these days and it sounds like nobody is actually blocked since it sounds like you changed ktlint to work around this?

rmcmk commented 6 months ago

Our team is encountering this issue in a large multi-module project, which is quite frustrating. We've tried the latest versions of ktlint, but to no avail. If I can find some spare time this weekend, I plan to examine the reproduction provided by @paul-dingemans to gain a better understanding of the problem. Maybe I'll even submit a PR if I'm feeling real frisky.

I suspect this issue does not have more traction as this only happens if you have two Intellij formatters enabled and used in the same workspace. Most projects, including many of the other Kotlin projects we have internally, use a build system plugin or CI for linting/format corrections.