summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/aggregate.ts
blob: b044e21bd384a154edc005afaa091d56192f1030 (plain)
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 };
}