1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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 };
}
|