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:
@@ -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 = {
|
||||
|
||||
@@ -22,6 +22,7 @@ function unwrap(resp: DaemonResponse): unknown {
|
||||
// daemon injects textDocument.uri from `file`, so callers omit it.
|
||||
export async function daemonRequest(
|
||||
file: string,
|
||||
serverId: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
@@ -29,6 +30,7 @@ export async function daemonRequest(
|
||||
await sendOnce({
|
||||
op: "request",
|
||||
file,
|
||||
serverId,
|
||||
method,
|
||||
params,
|
||||
launch: buildLaunchContext(),
|
||||
@@ -38,14 +40,18 @@ export async function daemonRequest(
|
||||
|
||||
// Wait For Diagnostics - Diagnostics arrive as a notification, not a
|
||||
// response, so the daemon has a dedicated op that awaits the next publish.
|
||||
// Accepts an array of server IDs; daemon fans out in parallel and returns
|
||||
// a grouped map: { [serverId]: { uri, diagnostics[] } }.
|
||||
export async function daemonDiagnostics(
|
||||
file: string,
|
||||
serverIds: string[],
|
||||
timeoutMs = 1500,
|
||||
): Promise<unknown> {
|
||||
return unwrap(
|
||||
await sendOnce({
|
||||
op: "diagnostics",
|
||||
file,
|
||||
serverIds,
|
||||
timeoutMs,
|
||||
launch: buildLaunchContext(),
|
||||
}),
|
||||
|
||||
@@ -38,6 +38,7 @@ export type DaemonRequest =
|
||||
id: number;
|
||||
op: "request";
|
||||
file: string;
|
||||
serverId: string;
|
||||
method: string;
|
||||
params: Record<string, unknown>;
|
||||
launch: LaunchContext;
|
||||
@@ -46,6 +47,7 @@ export type DaemonRequest =
|
||||
id: number;
|
||||
op: "diagnostics";
|
||||
file: string;
|
||||
serverIds: string[];
|
||||
timeoutMs?: number;
|
||||
launch: LaunchContext;
|
||||
}
|
||||
@@ -57,6 +59,7 @@ export type DaemonRequestWithoutId =
|
||||
| {
|
||||
op: "request";
|
||||
file: string;
|
||||
serverId: string;
|
||||
method: string;
|
||||
params: Record<string, unknown>;
|
||||
launch: LaunchContext;
|
||||
@@ -64,6 +67,7 @@ export type DaemonRequestWithoutId =
|
||||
| {
|
||||
op: "diagnostics";
|
||||
file: string;
|
||||
serverIds: string[];
|
||||
timeoutMs?: number;
|
||||
launch: LaunchContext;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user