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:
2026-05-04 07:39:03 -04:00
parent d24e2e94f4
commit b9808a8b1f
5 changed files with 117 additions and 41 deletions

View File

@@ -6,8 +6,9 @@ import * as fs from "node:fs";
import * as net from "node:net";
import * as path from "node:path";
import { LspClient } from "./client.ts";
import { findRoot, pickServer, pathToUri } from "./root.ts";
import { findRoot, pathToUri } from "./root.ts";
import type { ServerConfig } from "./types.ts";
import { servers } from "../server.ts";
import {
logPath,
socketPath,
@@ -50,14 +51,25 @@ function log(...args: unknown[]) {
);
}
// Get Or Create Entry - Looks up the cached client for a file, spawning a
// fresh LspClient if needed. The returned entry is guaranteed to have its
// `ready` promise resolved before the caller uses it.
// Find Server By ID - Looks up a ServerConfig from the registry by ID.
// Throws if the ID is not registered.
function findServerById(serverId: string): ServerConfig {
const server = servers.find((s) => s.id === serverId);
if (!server) {
throw new Error(`Unknown server ID: "${serverId}". Registered: ${servers.map((s) => s.id).join(", ")}`);
}
return server;
}
// Get Or Create Entry - Looks up the cached client for a server+file,
// spawning a fresh LspClient if needed. The returned entry is guaranteed
// to have its `ready` promise resolved before the caller uses it.
async function getOrCreateEntry(
filePath: string,
serverId: string,
launch: LaunchContext,
): Promise<ClientEntry> {
const server = pickServer(filePath);
const server = findServerById(serverId);
const rootDir = findRoot(filePath, server.rootMarkers);
const key = `${server.id}::${rootDir}`;
const existing = entries.get(key);
@@ -161,7 +173,7 @@ async function handle(req: DaemonRequest): Promise<DaemonResponse> {
switch (req.op) {
case "request": {
const filePath = path.resolve(req.file);
const entry = await getOrCreateEntry(filePath, req.launch);
const entry = await getOrCreateEntry(filePath, req.serverId, req.launch);
const { uri } = await syncFile(entry, filePath);
bumpIdle(entry);
const result = await entry.client.sendRequest(
@@ -172,15 +184,27 @@ async function handle(req: DaemonRequest): Promise<DaemonResponse> {
}
case "diagnostics": {
const filePath = path.resolve(req.file);
const entry = await getOrCreateEntry(filePath, req.launch);
const { uri, changed } = await syncFile(entry, filePath);
bumpIdle(entry);
if (changed) entry.client.clearDiagnostics(uri);
const result = await entry.client.waitForDiagnostics(
uri,
req.timeoutMs ?? 1500,
const timeoutMs = req.timeoutMs ?? 1500;
// Fan-Out - Run diagnostics against all requested servers in
// parallel. Individual failures are captured, not thrown.
const results: Record<string, unknown> = {};
const settled = await Promise.allSettled(
req.serverIds.map(async (serverId) => {
const entry = await getOrCreateEntry(filePath, serverId, req.launch);
const { uri, changed } = await syncFile(entry, filePath);
bumpIdle(entry);
if (changed) entry.client.clearDiagnostics(uri);
const diag = await entry.client.waitForDiagnostics(uri, timeoutMs);
return { serverId, diag };
}),
);
return { id: req.id, ok: true, result };
for (const outcome of settled) {
if (outcome.status === "fulfilled") {
results[outcome.value.serverId] = outcome.value.diag;
}
}
return { id: req.id, ok: true, result: results };
}
case "status": {
const result = {