diff options
Diffstat (limited to 'packages/lsp/src/config.ts')
| -rw-r--r-- | packages/lsp/src/config.ts | 292 |
1 files changed, 146 insertions, 146 deletions
diff --git a/packages/lsp/src/config.ts b/packages/lsp/src/config.ts index afec6bf..09cc9ce 100644 --- a/packages/lsp/src/config.ts +++ b/packages/lsp/src/config.ts @@ -11,22 +11,22 @@ */ export interface ResolvedServer { - readonly id: string; - readonly name: string; - readonly command: readonly string[]; - readonly env?: Readonly<Record<string, string>> | undefined; - readonly extensions: readonly string[]; - readonly rootMarkers: readonly string[]; - readonly initialization?: Readonly<Record<string, unknown>> | undefined; - readonly sidecar?: { readonly command: readonly string[] } | undefined; - /** - * Which config source this server was resolved from: `".dispatch/lsp.json"`, - * `"opencode.json"`, or `"built-in"`. Omitted when not yet resolved. Mirrors - * the wire `LspServerInfo.configSource` so config-shadow debugging surfaces - * to the status caller (a broken `.dispatch/lsp.json` silently shadowing - * `opencode.json`). - */ - readonly configSource?: string | undefined; + readonly id: string; + readonly name: string; + readonly command: readonly string[]; + readonly env?: Readonly<Record<string, string>> | undefined; + readonly extensions: readonly string[]; + readonly rootMarkers: readonly string[]; + readonly initialization?: Readonly<Record<string, unknown>> | undefined; + readonly sidecar?: { readonly command: readonly string[] } | undefined; + /** + * Which config source this server was resolved from: `".dispatch/lsp.json"`, + * `"opencode.json"`, or `"built-in"`. Omitted when not yet resolved. Mirrors + * the wire `LspServerInfo.configSource` so config-shadow debugging surfaces + * to the status caller (a broken `.dispatch/lsp.json` silently shadowing + * `opencode.json`). + */ + readonly configSource?: string | undefined; } /** Which config source a resolved server came from. */ @@ -35,153 +35,153 @@ export type ConfigSource = ".dispatch/lsp.json" | "opencode.json" | "built-in"; /** Result of resolving servers: the servers + whether `opencode.json`'s lsp key * was silently shadowed by a present `.dispatch/lsp.json`. */ export interface ResolveResult { - readonly servers: readonly ResolvedServer[]; - readonly shadowed: boolean; + readonly servers: readonly ResolvedServer[]; + readonly shadowed: boolean; } export interface ServerConfig { - readonly id?: string | undefined; - readonly name?: string | undefined; - readonly command: readonly string[]; - readonly env?: Readonly<Record<string, string>> | undefined; - readonly extensions?: readonly string[] | undefined; - readonly rootMarkers?: readonly string[] | undefined; - readonly initialization?: Readonly<Record<string, unknown>> | undefined; - readonly watch?: readonly string[] | undefined; + readonly id?: string | undefined; + readonly name?: string | undefined; + readonly command: readonly string[]; + readonly env?: Readonly<Record<string, string>> | undefined; + readonly extensions?: readonly string[] | undefined; + readonly rootMarkers?: readonly string[] | undefined; + readonly initialization?: Readonly<Record<string, unknown>> | undefined; + readonly watch?: readonly string[] | undefined; } export interface LspJsonConfig { - readonly servers?: Readonly<Record<string, ServerConfig>> | undefined; + readonly servers?: Readonly<Record<string, ServerConfig>> | undefined; } export interface OpencodeJsonConfig { - readonly lsp?: Readonly<Record<string, ServerConfig>> | undefined; + readonly lsp?: Readonly<Record<string, ServerConfig>> | undefined; } export interface ResolveServersDeps { - readonly cwd: string; - readonly dispatchLspJson: string | null; - readonly opencodeJson: string | null; - readonly exists: (path: string) => Promise<boolean>; + readonly cwd: string; + readonly dispatchLspJson: string | null; + readonly opencodeJson: string | null; + readonly exists: (path: string) => Promise<boolean>; } const BUILT_IN_REGISTRY: Record<string, ResolvedServer> = { - typescript: { - id: "typescript", - name: "TypeScript Language Server", - command: ["typescript-language-server", "--stdio"], - extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"], - rootMarkers: ["tsconfig.json", "package.json"], - configSource: "built-in", - }, + typescript: { + id: "typescript", + name: "TypeScript Language Server", + command: ["typescript-language-server", "--stdio"], + extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"], + rootMarkers: ["tsconfig.json", "package.json"], + configSource: "built-in", + }, }; export async function resolveServers(deps: ResolveServersDeps): Promise<ResolveResult> { - const result = new Map<string, ResolvedServer>(); - - // Parse opencode.json once — used both as the fallback source and to detect - // whether a present `.dispatch/lsp.json` silently shadows its `lsp` key. - let opencodeConfig: OpencodeJsonConfig | null = null; - if (deps.opencodeJson) { - try { - opencodeConfig = JSON.parse(deps.opencodeJson) as OpencodeJsonConfig; - } catch { - // ignore parse errors - } - } - const opencodeHasLsp = !!opencodeConfig?.lsp && Object.keys(opencodeConfig.lsp).length > 0; - - // 1. cwd/.dispatch/lsp.json (highest precedence) - let dispatchHadServers = false; - if (deps.dispatchLspJson) { - try { - const config = JSON.parse(deps.dispatchLspJson) as LspJsonConfig; - if (config.servers) { - for (const [key, server] of Object.entries(config.servers)) { - const resolved = resolveServer(key, server, ".dispatch/lsp.json"); - result.set(resolved.id, resolved); - } - dispatchHadServers = result.size > 0; - } - } catch { - // ignore parse errors - } - } - - // 2. fallback cwd/opencode.json lsp (only when dispatch yielded nothing) - if (result.size === 0 && opencodeConfig?.lsp) { - for (const [key, server] of Object.entries(opencodeConfig.lsp)) { - const resolved = resolveServer(key, server, "opencode.json"); - result.set(resolved.id, resolved); - } - } - - // 3. the built-in registry (when nothing else resolved) - if (result.size === 0) { - for (const [, server] of Object.entries(BUILT_IN_REGISTRY)) { - result.set(server.id, server); - } - } - - // `.dispatch/lsp.json` silently shadows `opencode.json`'s lsp key when both - // declare servers — the opencode entry is skipped with no warning otherwise. - const shadowed = dispatchHadServers && opencodeHasLsp; - return { servers: [...result.values()], shadowed }; + const result = new Map<string, ResolvedServer>(); + + // Parse opencode.json once — used both as the fallback source and to detect + // whether a present `.dispatch/lsp.json` silently shadows its `lsp` key. + let opencodeConfig: OpencodeJsonConfig | null = null; + if (deps.opencodeJson) { + try { + opencodeConfig = JSON.parse(deps.opencodeJson) as OpencodeJsonConfig; + } catch { + // ignore parse errors + } + } + const opencodeHasLsp = !!opencodeConfig?.lsp && Object.keys(opencodeConfig.lsp).length > 0; + + // 1. cwd/.dispatch/lsp.json (highest precedence) + let dispatchHadServers = false; + if (deps.dispatchLspJson) { + try { + const config = JSON.parse(deps.dispatchLspJson) as LspJsonConfig; + if (config.servers) { + for (const [key, server] of Object.entries(config.servers)) { + const resolved = resolveServer(key, server, ".dispatch/lsp.json"); + result.set(resolved.id, resolved); + } + dispatchHadServers = result.size > 0; + } + } catch { + // ignore parse errors + } + } + + // 2. fallback cwd/opencode.json lsp (only when dispatch yielded nothing) + if (result.size === 0 && opencodeConfig?.lsp) { + for (const [key, server] of Object.entries(opencodeConfig.lsp)) { + const resolved = resolveServer(key, server, "opencode.json"); + result.set(resolved.id, resolved); + } + } + + // 3. the built-in registry (when nothing else resolved) + if (result.size === 0) { + for (const [, server] of Object.entries(BUILT_IN_REGISTRY)) { + result.set(server.id, server); + } + } + + // `.dispatch/lsp.json` silently shadows `opencode.json`'s lsp key when both + // declare servers — the opencode entry is skipped with no warning otherwise. + const shadowed = dispatchHadServers && opencodeHasLsp; + return { servers: [...result.values()], shadowed }; } function resolveServer( - key: string, - config: ServerConfig, - configSource: ConfigSource, + key: string, + config: ServerConfig, + configSource: ConfigSource, ): ResolvedServer { - const id = config.id ?? key; - const name = config.name ?? id; - const extensions = config.extensions ?? []; - const rootMarkers = config.rootMarkers ?? []; - - let sidecar: { readonly command: readonly string[] } | undefined; - if (config.watch) { - sidecar = { command: config.watch }; - } else if (config.initialization) { - sidecar = detectSidecar(config.initialization); - } - - const result: ResolvedServer = { - id, - name, - command: config.command, - extensions, - rootMarkers, - configSource, - }; - if (config.env) { - (result as { env?: Readonly<Record<string, string>> }).env = config.env; - } - if (config.initialization) { - (result as { initialization?: Readonly<Record<string, unknown>> }).initialization = - config.initialization; - } - if (sidecar) { - (result as { sidecar?: { readonly command: readonly string[] } }).sidecar = sidecar; - } - return result; + const id = config.id ?? key; + const name = config.name ?? id; + const extensions = config.extensions ?? []; + const rootMarkers = config.rootMarkers ?? []; + + let sidecar: { readonly command: readonly string[] } | undefined; + if (config.watch) { + sidecar = { command: config.watch }; + } else if (config.initialization) { + sidecar = detectSidecar(config.initialization); + } + + const result: ResolvedServer = { + id, + name, + command: config.command, + extensions, + rootMarkers, + configSource, + }; + if (config.env) { + (result as { env?: Readonly<Record<string, string>> }).env = config.env; + } + if (config.initialization) { + (result as { initialization?: Readonly<Record<string, unknown>> }).initialization = + config.initialization; + } + if (sidecar) { + (result as { sidecar?: { readonly command: readonly string[] } }).sidecar = sidecar; + } + return result; } function detectSidecar( - init: Readonly<Record<string, unknown>>, + init: Readonly<Record<string, unknown>>, ): { readonly command: readonly string[] } | undefined { - const luauLsp = init["luau-lsp"]; - if (!luauLsp || typeof luauLsp !== "object") return undefined; - const luau = luauLsp as Record<string, unknown>; - const sourcemap = luau.sourcemap; - if (!sourcemap || typeof sourcemap !== "object") return undefined; - const sm = sourcemap as Record<string, unknown>; - if (sm.autogenerate !== true) return undefined; - const rojoProjectFile = sm.rojoProjectFile; - if (typeof rojoProjectFile !== "string") return undefined; - return { - command: ["rojo", "sourcemap", rojoProjectFile, "--watch", "-o", "sourcemap.json"], - }; + const luauLsp = init["luau-lsp"]; + if (!luauLsp || typeof luauLsp !== "object") return undefined; + const luau = luauLsp as Record<string, unknown>; + const sourcemap = luau.sourcemap; + if (!sourcemap || typeof sourcemap !== "object") return undefined; + const sm = sourcemap as Record<string, unknown>; + if (sm.autogenerate !== true) return undefined; + const rojoProjectFile = sm.rojoProjectFile; + if (typeof rojoProjectFile !== "string") return undefined; + return { + command: ["rojo", "sourcemap", rojoProjectFile, "--watch", "-o", "sourcemap.json"], + }; } /** @@ -195,20 +195,20 @@ function detectSidecar( * server stays broken until a bounded backoff elapses. */ export function configFingerprint(server: ResolvedServer): string { - return stableStringify(server); + return stableStringify(server); } /** Deterministic JSON: object keys sorted at every nesting level. */ function stableStringify(value: unknown): string { - return JSON.stringify(canonicalize(value)); + return JSON.stringify(canonicalize(value)); } function canonicalize(value: unknown): unknown { - if (value === null || typeof value !== "object") return value; - if (Array.isArray(value)) return value.map(canonicalize); - const sorted: Record<string, unknown> = {}; - for (const key of Object.keys(value as Record<string, unknown>).sort()) { - sorted[key] = canonicalize((value as Record<string, unknown>)[key]); - } - return sorted; + if (value === null || typeof value !== "object") return value; + if (Array.isArray(value)) return value.map(canonicalize); + const sorted: Record<string, unknown> = {}; + for (const key of Object.keys(value as Record<string, unknown>).sort()) { + sorted[key] = canonicalize((value as Record<string, unknown>)[key]); + } + return sorted; } |
