[analyzer] fix initialisation progress reporting

Progress reporting during initialisation has a time-based debounce: the
sending coroutine (working concurrently with the passed-in block) is
waiting 100ms after sending a progress update, before proceeding with
the next update.

When the block returns, the sending coroutine is immediately cancelled,
robbing it of a chance to wake up and deliver the end progress message,
that the block requests at the very end. This caused the never-ending
progress displaying in Air.

The "withProgress" function has been fixed to always send begin/end
message out of bound and only debounce intermediate progress updates.
Since the end message may be a subject for customisation, depending on
the execution path taken, to enforce the begin/end contract on progress,
the signature was updated: the function now takes the begin progress
title, and the block is now required to return "WorkDoneProgress.End"
instance that will be sent to the client.

To keep the code simple, the function no longer returns a generic T. It
can be added back, if necessary, however, currently it only has one
usage that expects Unit.

GitOrigin-RevId: ea8b390cb7bca4c894c5c2b75e304b71eaf03481
This commit is contained in:
Ludwig Valda Vasquez
2025-11-07 05:12:05 +00:00
committed by intellij-monorepo-bot
parent 1750e77384
commit 8ca4e2c3d7
@@ -1,7 +1,6 @@
package com.jetbrains.lsp.implementation
import com.jetbrains.lsp.protocol.*
import com.jetbrains.lsp.implementation.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
@@ -11,19 +10,27 @@ fun interface ProgressReporter {
companion object {
val NOOP: ProgressReporter = ProgressReporter {}
}
fun report(progress: WorkDoneProgress)
}
suspend fun<T> LspClient.withProgress(
suspend fun LspClient.withProgress(
request: WorkDoneProgressParams,
body: suspend CoroutineScope.(ProgressReporter) -> T
): T =
beginTitle: String,
body: suspend CoroutineScope.(ProgressReporter) -> WorkDoneProgress.End,
) {
coroutineScope {
when (val token = request.workDoneToken) {
null -> body(ProgressReporter.NOOP)
else -> {
val beginProgress = WorkDoneProgress.Begin(title = beginTitle)
notify(
LSP.ProgressNotificationType,
ProgressParams(token, LSP.json.encodeToJsonElement(WorkDoneProgress.serializer(), beginProgress)),
)
val state = MutableStateFlow<WorkDoneProgress?>(null)
launch {
val endProgress = launch {
state.filterNotNull().collect { p ->
notify(LSP.ProgressNotificationType, ProgressParams(token, LSP.json.encodeToJsonElement(WorkDoneProgress.serializer(), p)))
delay(100.milliseconds)
@@ -31,6 +38,12 @@ suspend fun<T> LspClient.withProgress(
}.use {
body(ProgressReporter { p -> state.value = p })
}
notify(
LSP.ProgressNotificationType,
ProgressParams(token, LSP.json.encodeToJsonElement(WorkDoneProgress.serializer(), endProgress))
)
}
}
}
}
}