diff options
Diffstat (limited to 'packages/lsp/src/aggregate.ts')
| -rw-r--r-- | packages/lsp/src/aggregate.ts | 80 |
1 files changed, 80 insertions, 0 deletions
diff --git a/packages/lsp/src/aggregate.ts b/packages/lsp/src/aggregate.ts new file mode 100644 index 0000000..b044e21 --- /dev/null +++ b/packages/lsp/src/aggregate.ts @@ -0,0 +1,80 @@ +/** + * Concurrent multi-server diagnostics aggregation. + * + * Queries every matching language server AT ONCE (not one-at-a-time), each + * capped at `timeoutMs`. A server that doesn't push diagnostics within the cap + * is SKIPPED with a per-server notice rather than blocking the others — so one + * slow/dead server (e.g. a corrupted Steep) can't hold up the fast one's + * (ruby-lsp) results for the full timeout on every edit. + * + * The only I/O here is `client.waitForDiagnostics` (injected via `getClient`), + * so this is unit-testable with a fake client and no real process. + */ + +import type { LanguageServerClient } from "./client.js"; + +export interface AggregateServer { + readonly id: string; + readonly name: string; + readonly root: string; +} + +export interface AggregateOpts { + /** Post-edit buffer; when omitted the server reads from disk. */ + readonly text?: string | undefined; + /** Only include diagnostics with severity ≤ this (1=Error, 2=Warning). */ + readonly minSeverity?: number | undefined; +} + +export interface AggregateResult { + /** Merged diagnostics tagged by source + a per-skipped-server notice. */ + readonly formatted: string; + /** True if at least one server was skipped for exceeding the cap. */ + readonly timedOut: boolean; +} + +/** + * Query `servers` concurrently, each capped at `timeoutMs`. Returns merged + * diagnostics tagged by source (`[name]\n…`) and, for any server that did not + * respond in time, a `⚠️ [name] LSP took too long (>Ns), skipped — please raise + * this to the user.` notice. Never rejects: a client error yields an empty + * contribution for that server. + */ +export async function aggregateDiagnostics( + getClient: (id: string, root: string) => LanguageServerClient | undefined, + servers: readonly AggregateServer[], + absolutePath: string, + timeoutMs: number, + opts: AggregateOpts, +): Promise<AggregateResult> { + const entries = await Promise.all( + servers.map(async (server) => { + const client = getClient(server.id, server.root); + if (!client) return null; + const waitOpts: { text?: string; timeoutMs: number; minSeverity?: number } = { timeoutMs }; + if (opts.text !== undefined) waitOpts.text = opts.text; + if (opts.minSeverity !== undefined) waitOpts.minSeverity = opts.minSeverity; + const result = await client.waitForDiagnostics(absolutePath, waitOpts); + return { server, result }; + }), + ); + + const parts: string[] = []; + let timedOut = false; + const capSeconds = Math.round(timeoutMs / 1000); + + for (const entry of entries) { + if (!entry) continue; + const { server, result } = entry; + if (result.timedOut) { + timedOut = true; + parts.push( + `⚠️ [${server.name}] LSP took too long (>${capSeconds}s), diagnostics skipped — please raise this to the user.`, + ); + } else if (result.formatted) { + parts.push(`[${server.name}]\n${result.formatted}`); + } + } + + return { formatted: parts.join("\n\n"), timedOut }; +} |
