// LSP server configuration — load defaults, merge with supi config per language key. import * as fs from "node:fs"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { loadSupiConfigForScope } from "@mrclrchtr/supi-core/config"; import type { LspConfig, ServerConfig } from "./types.ts"; const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url)); // Load defaults at module level — resolve relative to this file. const DEFAULTS: LspConfig = JSON.parse( fs.readFileSync(path.join(CONFIG_DIR, "defaults.json"), "utf-8"), ) as LspConfig; // ── Public API ──────────────────────────────────────────────────────── export interface LoadConfigOptions { homeDir?: string; } /** Map from language alias → canonical config key. */ export const LANGUAGE_ALIASES: Record = { cpp: "c", }; /** Resolve a language name through aliases. */ export function resolveLanguageAlias(name: string): string { return LANGUAGE_ALIASES[name] ?? name; } function resolveAliasesInOverrides(servers: Record>): void { for (const [alias, target] of Object.entries(LANGUAGE_ALIASES)) { if (servers[alias]) { servers[target] = { ...(servers[target] ?? {}), ...servers[alias] }; delete servers[alias]; } } } /** * Load LSP config: built-in defaults merged with per-language-key overrides * from supi config (`~/.pi/agent/supi/config.json` and `.pi/supi/config.json`). * Each language key merges individually; omitted fields fall back to defaults. */ export function loadConfig(cwd: string, options?: LoadConfigOptions): LspConfig { const defaults = DEFAULTS; const globalLsp = loadSupiConfigForScope( "lsp", cwd, { enabled: true, active: [], servers: {} as Record }, { scope: "global", homeDir: options?.homeDir }, ); const projectLsp = loadSupiConfigForScope( "lsp", cwd, { enabled: true, active: [], servers: {} as Record }, { scope: "project" }, ); const merged = mergeServerConfigs(defaults.servers, globalLsp.servers, projectLsp.servers); return { servers: merged }; } /** * Find which server config handles a given file extension. * Returns [languageName, config] or null. */ export function getServerForFile( config: LspConfig, filePath: string, ): [string, ServerConfig] | null { const ext = path.extname(filePath).slice(1).toLowerCase(); if (!ext) return null; for (const [name, server] of Object.entries(config.servers)) { if (server.fileTypes.includes(ext)) { return [name, server]; } } return null; } // ── Private ─────────────────────────────────────────────────────────── function mergeServerConfigs( defaults: Record, globalOverrides: unknown, projectOverrides: unknown, ): Record { const merged: Record = { ...defaults }; const globalServers = isServerRecord(globalOverrides) ? globalOverrides : {}; const projectServers = isServerRecord(projectOverrides) ? projectOverrides : {}; resolveAliasesInOverrides(globalServers); resolveAliasesInOverrides(projectServers); // Apply global per-key overrides against defaults for (const [lang, override] of Object.entries(globalServers)) { const result = mergeSingleServer(defaults[lang], override); if (result) merged[lang] = result; } // Apply project per-key overrides against the result so far for (const [lang, override] of Object.entries(projectServers)) { const result = mergeSingleServer(merged[lang] ?? defaults[lang], override); if (result) merged[lang] = result; } // Remove servers whose final merged config has enabled === false for (const [lang, config] of Object.entries(merged)) { if (config.enabled === false) { delete merged[lang]; } } return merged; } function mergeSingleServer( base: ServerConfig | undefined, override: Partial, ): ServerConfig | null { if (!base) { // A custom server needs a command and at least one file extension. if ( typeof override.command === "string" && override.command.length > 0 && Array.isArray(override.fileTypes) && override.fileTypes.length > 0 && (override.rootMarkers === undefined || Array.isArray(override.rootMarkers)) ) { return { ...override, rootMarkers: override.rootMarkers ?? [] } as ServerConfig; } return null; } return { ...base, ...override }; } function isServerRecord(value: unknown): value is Record> { return typeof value === "object" && value !== null && !Array.isArray(value); }