summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/aggregate.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/lsp/src/aggregate.ts')
-rw-r--r--packages/lsp/src/aggregate.ts80
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 };
+}