fix: anchor LSP diagnostics to server readiness

This commit is contained in:
2026-07-26 10:25:32 +04:00
parent 516af1ca96
commit a95475be2a
2 changed files with 134 additions and 8 deletions
@@ -146,23 +146,52 @@ class Lsp4jDiagnosticsRunner(
/**
* Block until every opened URI has received at least one push (or the timeout elapses), then
* a short settle window so servers that push an empty report first, real diagnostics second
* (tsserver does this after project load) land their final result before we read it.
* wait for a quiet period anchored to the last server publication. Servers such as tsserver
* can publish an empty report while loading the project and real diagnostics afterward; a
* timer started immediately after didOpen reads the workspace before the server is ready.
*/
private fun awaitDiagnostics(client: CollectingLanguageClient, uris: Set<String>) {
val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds)
while (System.nanoTime() < deadline && !client.hasAll(uris)) {
Thread.sleep(POLL_MS)
}
Thread.sleep(SETTLE_MS)
client.awaitAll(uris, deadline)
client.awaitQuiet(deadline, TimeUnit.MILLISECONDS.toNanos(SETTLE_MILLIS))
}
private class CollectingLanguageClient : LanguageClient {
private val byUri = ConcurrentHashMap<String, List<Diagnostic>>()
private val updates = Object()
@Volatile private var lastUpdateNanos = 0L
fun latestFor(uri: String): List<Diagnostic> = byUri[uri].orEmpty()
fun hasAll(uris: Set<String>): Boolean = byUri.keys.containsAll(uris)
fun awaitAll(uris: Set<String>, deadline: Long) {
synchronized(updates) {
while (!hasAll(uris)) {
val remaining = deadline - System.nanoTime()
if (remaining <= 0) return
updates.wait(TimeUnit.NANOSECONDS.toMillis(remaining).coerceAtLeast(1))
}
}
}
fun awaitQuiet(deadline: Long, quietNanos: Long) {
synchronized(updates) {
while (true) {
val remaining = deadline - System.nanoTime()
if (remaining <= 0) return
val sinceUpdate = System.nanoTime() - lastUpdateNanos
if (lastUpdateNanos != 0L && sinceUpdate >= quietNanos) return
val waitNanos = minOf(remaining, quietNanos - sinceUpdate)
updates.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos).coerceAtLeast(1))
}
}
}
override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams) {
byUri[diagnostics.uri] = diagnostics.diagnostics.orEmpty()
synchronized(updates) {
lastUpdateNanos = System.nanoTime()
updates.notifyAll()
}
}
override fun telemetryEvent(`object`: Any?) = Unit
override fun showMessage(messageParams: MessageParams) = Unit
@@ -172,7 +201,6 @@ class Lsp4jDiagnosticsRunner(
}
private companion object {
const val POLL_MS = 100L
const val SETTLE_MS = 750L
const val SETTLE_MILLIS = 750L
}
}