feat(lsp): configurable language servers + merge C/C++ into one clangd
The built-in LANGUAGE_SPECS were hardcoded — users couldn't add Java/Ruby/Lua servers or override a built-in's command/args, and the file's own comment flagged this as a future extension. Also, c and cpp were separate specs both spawning clangd, so a mixed C/C++ project ran two indexing the same headers. - lspManager.ts: LANGUAGE_SPECS is now mutable; configureLanguageSpecs(overrides) merges user entries (keyed by languageId) into the built-ins. A built-in id override replaces command/args and, if extensions is given, rewrites routing. A new id adds a mapping but REQUIRES extensions (ignored otherwise — can't route files to it). C and C++ collapse into one 'c' clangd spec (all .c/.h/.cpp/.cc/.cxx/.hpp/.hh/.hxx route to a single clangd). - config.ts: resolveLspServers() reads stored.lspServers. - store.ts: StoredConfig.lspServers field. - cli.ts: 'locode config set lspServers <json>' (JSON object value, validated). - ui/ink/index.tsx: configureLanguageSpecs(resolveLspServers()) at startup. - lspManager.test.ts: 6 tests for the merge (add, override, rewrite exts, ignore-without-extensions, c/cpp collapse) via _specsForTests/_resetSpecsForTests — no servers spawned. Verified: typecheck clean, build 256.60 KB, 256 tests pass (+6).
This commit is contained in:
+19
-4
@@ -116,7 +116,7 @@ configCmd
|
||||
configCmd
|
||||
.command("set <key> <value>")
|
||||
.description(
|
||||
"Persist a config value (backend, model, baseUrl, contextWindow, maxOutputTokens, maxIterations, autoCompactThreshold, requestTimeoutMs, subagentTimeoutMs, maxRetries)",
|
||||
"Persist a config value (backend, model, baseUrl, contextWindow, maxOutputTokens, maxIterations, autoCompactThreshold, requestTimeoutMs, subagentTimeoutMs, maxRetries, lspServers)",
|
||||
)
|
||||
.action((key: string, value: string) => {
|
||||
if (
|
||||
@@ -129,15 +129,30 @@ configCmd
|
||||
key !== "autoCompactThreshold" &&
|
||||
key !== "requestTimeoutMs" &&
|
||||
key !== "subagentTimeoutMs" &&
|
||||
key !== "maxRetries"
|
||||
key !== "maxRetries" &&
|
||||
key !== "lspServers"
|
||||
) {
|
||||
console.error(
|
||||
`Unknown config key "${key}". Valid keys: backend, model, baseUrl, contextWindow, maxOutputTokens, maxIterations, autoCompactThreshold, requestTimeoutMs, subagentTimeoutMs, maxRetries`,
|
||||
`Unknown config key "${key}". Valid keys: backend, model, baseUrl, contextWindow, maxOutputTokens, maxIterations, autoCompactThreshold, requestTimeoutMs, subagentTimeoutMs, maxRetries, lspServers`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
const stored = loadStoredConfig();
|
||||
if (key === "contextWindow" || key === "maxIterations") {
|
||||
if (key === "lspServers") {
|
||||
// lspServers is a JSON object: { "<languageId>": { "command": "...", "args": [...], "extensions": [...] } }
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(value);
|
||||
} catch {
|
||||
console.error(`lspServers must be a JSON object, got invalid JSON: ${value}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
console.error(`lspServers must be a JSON object keyed by language id, got: ${value}`);
|
||||
process.exit(1);
|
||||
}
|
||||
stored.lspServers = parsed as Record<string, { command: string; args?: string[]; extensions?: string[] }>;
|
||||
} else if (key === "contextWindow" || key === "maxIterations") {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
console.error(`${key} must be a positive number, got "${value}".`);
|
||||
|
||||
Reference in New Issue
Block a user