IJPL-199180 add proper state for CommittingDocuments phase

Several typed handlers can trigger auto-popup and we need to process them independently
because each request has its own condition, and we need to start auto-popup if any of
them succeeds.

GitOrigin-RevId: c54148d8de7ff2e384cc37071294535baa6a9e50
This commit is contained in:
Max Medvedev
2025-09-05 09:58:32 +00:00
committed by intellij-monorepo-bot
parent 9239a0120f
commit 33d20484c4
@@ -1,6 +1,7 @@
// 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.codeInsight.completion
import com.intellij.codeInsight.completion.CompletionPhase.CommittingDocuments.CommittingState.*
import com.intellij.codeInsight.completion.CompletionPhase.Companion.NoCompletion
import com.intellij.codeInsight.completion.impl.CompletionServiceImpl
import com.intellij.codeInsight.completion.impl.CompletionServiceImpl.Companion.assertPhase
@@ -76,32 +77,69 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
internal var replaced: Boolean = false
private val myTracker: ActionTracker = ActionTracker(editor, this)
private var myRequestCount = 1
private var myState: CommittingState = InProgress(1)
fun ignoreCurrentDocumentChange() {
myTracker.ignoreCurrentDocumentChange()
}
/**
* @return `true` if the phase won't trigger the completion process.
*/
@get:ApiStatus.Internal
val isExpired: Boolean
get() = myTracker.hasAnythingHappened() || myRequestCount <= 0
get() = myTracker.hasAnythingHappened() || myState !is InProgress
private val expirationReason: String
get() = myTracker.describeChangeEvent() + "; myRequestCount = $myRequestCount"
internal fun incrementRequestCount() {
myRequestCount++
LOG.trace { "Increment request count :: new myRequestCount=$myRequestCount" }
/**
* Several typedHandlers can request auto-popup completion during processing of a single event.
* We need to trigger read-action for them independently because they can have different conditions for starting completion.
*/
private fun addRequest() {
when (val cur = myState) {
Cancelled, Disposed, Success -> {
LOG.error("Cannot add request for a finished phase: $cur")
return
}
is InProgress -> {
myState = InProgress(cur.requests + 1)
}
}
LOG.trace { "Increment request count :: new myState=$myState" }
}
private fun decrementRequestCount() {
myRequestCount--
LOG.trace { "Decrement request count :: new myRequestCount=$myRequestCount" }
/**
* the current request was not successful (most likely because of the failed condition), but other requests still have a chance to succeed.
*/
private fun cancelThisRequest() {
when (val cur = myState) {
Disposed, Cancelled, Success -> {
/* do nothing */
}
is InProgress -> {
val requests = cur.requests
if (requests > 1) {
myState = InProgress(cur.requests - 1)
}
else {
if (requests < 1) {
LOG.error("Invalid request count: $requests")
}
myState = Cancelled
}
}
}
LOG.trace { "Cancel request :: new myState=$myState" }
}
/**
* At least one of the requests succeeded, the completion process has been started, no more requests are necessary.
*/
private fun requestCompleted() {
LOG.trace { "Request completed" }
myRequestCount = 0
LOG.assertTrue(myState is InProgress, "myState=$myState")
myState = Success
}
override fun newCompletionStarted(time: Int, repeated: Boolean): Int {
@@ -110,7 +148,7 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
override fun dispose() {
LOG.trace { "Dispose completion phase: $this" }
myRequestCount = 0
myState = Disposed
if (!replaced && indicator != null) {
indicator.closeAndFinish(true)
}
@@ -120,6 +158,18 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
return "CommittingDocuments{hasIndicator=${indicator != null}}"
}
/**
* InProgress(1) -> InProgress(2...Xxx) -> InProgress(1) -> Cancelled -> Disposed
* |--------------|------------------------|---------> Success-------^
*
*/
private sealed interface CommittingState {
object Success : CommittingState
object Cancelled : CommittingState
object Disposed : CommittingState
data class InProgress(val requests: Int) : CommittingState
}
@ApiStatus.Internal
companion object {
private val ourExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("Completion Preparation", 1)
@@ -178,8 +228,8 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
autopopup: Boolean,
project: Project,
): Editor? {
if (phase.isExpired) {
LOG.trace("Phase is expired")
if (phase.myState !is InProgress) {
LOG.trace { "Phase is expired ${phase.myState}" }
return null
}
LOG.trace { "Start non-blocking read action :: phase=${phase.replaced}" }
@@ -206,17 +256,36 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
) {
LOG.trace { "Finish on UI thread :: completionEditor=$completionEditor" }
if (completionEditor == null || phase.isExpired) {
if (phase.myState !is InProgress) {
LOG.trace { "Phase is expired :: myState=${phase.myState}" }
return
}
if (completionEditor == null) {
// preparation has failed for this specific request. We must cancel only this request.
// If no other requests are pending, we can cancel the phase altogether.
if (phase == CompletionServiceImpl.completionPhase) {
LOG.trace { "Setting NoCompletion phase :: completionEditor=$completionEditor, expirationReason=${phase.expirationReason}" }
phase.decrementRequestCount()
if (phase.isExpired) {
LOG.trace { "Setting NoCompletion phase :: completionEditor=$completionEditor, expirationReason=editor is null" }
phase.cancelThisRequest()
if (phase.myState == Cancelled) {
CompletionServiceImpl.setCompletionPhase(NoCompletion)
}
}
return
}
if (phase.myTracker.hasAnythingHappened()) {
// activity has happened in the editor. We must cancel all the requests altogether
if (phase == CompletionServiceImpl.completionPhase) {
LOG.trace { "Setting NoCompletion phase :: completionEditor=$completionEditor, expirationReason=${phase.myTracker.describeChangeEvent()}" }
phase.cancelPhase()
CompletionServiceImpl.setCompletionPhase(NoCompletion)
}
return
}
LOG.trace { "Starting completion phase :: completionEditor=$completionEditor" }
phase.requestCompleted()
@@ -237,7 +306,7 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
if (currentPhase is CommittingDocuments && !currentPhase.isExpired && event == currentPhase.event) {
LOG.assertTrue(prevIndicator == currentPhase.indicator, "Indicators must match. prevIndicator=$prevIndicator, currentPhase=$currentPhase, currentPhase.indicator=${currentPhase.indicator}")
currentPhase.incrementRequestCount()
currentPhase.addRequest()
return currentPhase
}
}
@@ -280,6 +349,13 @@ sealed class CompletionPhase @ApiStatus.Internal constructor(
return false
}
}
private fun cancelPhase() {
when (myState) {
Cancelled, Disposed, Success -> {}
is InProgress -> myState = Cancelled
}
}
}
class Synchronous internal constructor(indicator: CompletionProgressIndicator) : CompletionPhase(indicator) {