refactor(daemon): require explicit serverId on all daemon ops
Move all server matching logic to the extension/CLI side. The daemon no longer calls pickServer() — it receives an explicit serverId (or serverIds[] for diagnostics) and uses it directly for cache lookup and server spawning. Key changes: - request op requires serverId: string - diagnostics op requires serverIds: string[] — daemon fans out in parallel via Promise.allSettled and returns grouped map - formatDiagnostics() handles grouped results with per-server headers when multiple servers contribute (single-server omits header) - CLI picks servers locally before calling daemon helpers - New pickDiagnosticServers() in extension returns all available, non-disabled servers matching the file extension This makes multi-server diagnostics (e.g., typescript-language-server + oxlint) work naturally — the extension decides which servers to query, the daemon just executes.
This commit is contained in:
83
index.ts
83
index.ts
@@ -12,7 +12,7 @@ import {
|
||||
daemonRequest,
|
||||
daemonStatus,
|
||||
} from "./src/daemonClient.ts";
|
||||
import { pickServer } from "./src/root.ts";
|
||||
import { pickServer, isServerAvailable } from "./src/root.ts";
|
||||
import { servers } from "./server.ts";
|
||||
import {
|
||||
ServerNotFoundError,
|
||||
@@ -201,17 +201,8 @@ function formatDocumentSymbols(result: unknown): string {
|
||||
}
|
||||
|
||||
// Format Diagnostics - Turn diagnostic messages into readable text.
|
||||
function formatDiagnostics(result: unknown, limit = 20): string {
|
||||
if (
|
||||
!result ||
|
||||
typeof result !== "object" ||
|
||||
!("diagnostics" in result)
|
||||
) {
|
||||
return "(no diagnostics)";
|
||||
}
|
||||
const diags = (result as any).diagnostics;
|
||||
if (!Array.isArray(diags) || diags.length === 0) return "(no diagnostics)";
|
||||
|
||||
// Format Single Server Diagnostics - Renders one server's diagnostics list.
|
||||
function formatServerDiagnostics(diags: any[], limit: number): string {
|
||||
const severityNames: Record<number, string> = {
|
||||
1: "Error",
|
||||
2: "Warning",
|
||||
@@ -236,11 +227,45 @@ function formatDiagnostics(result: unknown, limit = 20): string {
|
||||
.join("\n");
|
||||
|
||||
if (diags.length > limit) {
|
||||
return `${formatted}\n\n... and ${diags.length - limit} more diagnostics (showing first ${limit})`;
|
||||
return `${formatted}\n\n... and ${diags.length - limit} more (showing first ${limit})`;
|
||||
}
|
||||
return formatted;
|
||||
}
|
||||
|
||||
// Format Diagnostics - Handles the grouped result map from the daemon:
|
||||
// { [serverId]: { uri, diagnostics[] } }. Single-server results omit the
|
||||
// header to avoid noise.
|
||||
function formatDiagnostics(result: unknown, limit = 20): string {
|
||||
if (!result || typeof result !== "object") return "(no diagnostics)";
|
||||
|
||||
const grouped = result as Record<string, any>;
|
||||
const serverIds = Object.keys(grouped);
|
||||
if (serverIds.length === 0) return "(no diagnostics)";
|
||||
|
||||
// Collect Servers With Diagnostics
|
||||
const sections: { id: string; diags: any[] }[] = [];
|
||||
for (const id of serverIds) {
|
||||
const entry = grouped[id];
|
||||
const diags = entry?.diagnostics;
|
||||
if (Array.isArray(diags) && diags.length > 0) {
|
||||
sections.push({ id, diags });
|
||||
}
|
||||
}
|
||||
|
||||
if (sections.length === 0) return "(no diagnostics)";
|
||||
|
||||
// Single Server - Skip header for brevity.
|
||||
if (sections.length === 1) {
|
||||
return formatServerDiagnostics(sections[0].diags, limit);
|
||||
}
|
||||
|
||||
// Multiple Servers - Group with headers.
|
||||
const perServer = Math.max(5, Math.floor(limit / sections.length));
|
||||
return sections
|
||||
.map((s) => `## ${s.id}\n${formatServerDiagnostics(s.diags, perServer)}`)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
// Is Expected Error - Returns true if the error is an expected condition
|
||||
// (unsupported file type or missing server binary) that should be
|
||||
// suppressed rather than surfaced to the user.
|
||||
@@ -281,7 +306,7 @@ async function runLsp(
|
||||
`LSP server "${server.id}" is disabled. Use /lsp-enable ${server.id} to re-enable.`,
|
||||
);
|
||||
}
|
||||
return await daemonRequest(filePath, method, params);
|
||||
return await daemonRequest(filePath, server.id, method, params);
|
||||
} catch (error) {
|
||||
if (isExpectedError(error)) {
|
||||
return undefined;
|
||||
@@ -300,19 +325,26 @@ async function runLsp(
|
||||
}
|
||||
}
|
||||
|
||||
// Run LSP Diagnostics - Diagnostics arrive as a notification, so the
|
||||
// daemon has a dedicated op that waits for the next publish. Expected
|
||||
// errors (unsupported file type, missing binary) are suppressed.
|
||||
// Pick Diagnostic Servers - Returns all available, non-disabled servers
|
||||
// matching the file's extension. Used for fan-out diagnostics.
|
||||
function pickDiagnosticServers(filePath: string): string[] {
|
||||
const ext = path.extname(filePath).replace(/^\./, "");
|
||||
return servers
|
||||
.filter((s) => s.match.includes(ext) && isServerAvailable(s) && !disabledServers.has(s.id))
|
||||
.map((s) => s.id);
|
||||
}
|
||||
|
||||
// Run LSP Diagnostics - Fans out to all matching servers in a single
|
||||
// daemon call. Returns the grouped result map or undefined if no servers.
|
||||
async function runDiagnostics(filePath: string): Promise<unknown> {
|
||||
try {
|
||||
return await daemonDiagnostics(filePath, 1500);
|
||||
const serverIds = pickDiagnosticServers(filePath);
|
||||
if (serverIds.length === 0) return undefined;
|
||||
return await daemonDiagnostics(filePath, serverIds, 1500);
|
||||
} catch (error) {
|
||||
if (isExpectedError(error)) {
|
||||
return undefined;
|
||||
}
|
||||
// Daemon-wrapped errors (plain Error with expected message) are also
|
||||
// expected — the daemon catches pickServer() throws and returns them
|
||||
// as string error messages.
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes("No LSP server registered") ||
|
||||
@@ -492,9 +524,12 @@ export default function (pi: ExtensionAPI) {
|
||||
|
||||
try {
|
||||
const absolutePath = path.resolve(ctx.cwd, filePath);
|
||||
// daemonDiagnostics triggers getOrCreateEntry + syncFile in the daemon.
|
||||
// We don't await it — just fire and forget so the server starts warming up.
|
||||
void daemonDiagnostics(absolutePath).catch(() => {});
|
||||
// Warm Diagnostic Servers - Fire-and-forget so servers are ready by
|
||||
// the time an LSP tool is called.
|
||||
const serverIds = pickDiagnosticServers(absolutePath);
|
||||
if (serverIds.length > 0) {
|
||||
void daemonDiagnostics(absolutePath, serverIds).catch(() => {});
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore — unsupported file type, missing binary, etc.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user