diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionPhase.kt b/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionPhase.kt index 6858efad80aa..7475755d62a4 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionPhase.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionPhase.kt @@ -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) {