Users can now drop a .pi-lsp.json at any ancestor of their working files to add new LSP servers, override built-in ones, or disable servers entirely. The nearest config (walking upward) wins. - New src/config.ts: walks upward for .pi-lsp.json, parses, and merges with the built-in registry. Cached per config-file path with mtime invalidation. Falls back to built-ins on parse error. - Merge rules: matching id shallow-merges (user wins); new id appends (must include match/command/args/rootMarkers); `disable` filters at the end. - src/root.ts: pickServer() now resolves servers via the per-repo registry. Adds findServerById(filePath, id) and re-exports getServersForPath() for callers. - src/daemon.ts: getOrCreateEntry() resolves serverId against the filePath's config so spawned servers reflect repo overrides. - index.ts and cli.ts: replace direct `servers` imports with path-aware getServersForPath() lookups. - Tests: 9 new unit tests covering merge semantics, walk-up discovery, mtime invalidation, and graceful fallback. - Docs: README "Per-Repo Config" section + AGENTS.md updates.
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
import { globalRootMarkers } from "../server.ts";
|
|
import { getServersForPath } from "./config.ts";
|
|
import type { ServerConfig } from "./types.ts";
|
|
import { UnsupportedExtensionError } from "./types.ts";
|
|
import { isOnPath } from "./util.ts";
|
|
|
|
// Re-Export - Centralizes the path-aware registry helper so callers can
|
|
// import it from `./root.ts` alongside pickServer/findRoot.
|
|
export { getServersForPath };
|
|
|
|
// Resolve File URI To Local Path
|
|
export function uriToPath(uri: string): string {
|
|
return fileURLToPath(uri);
|
|
}
|
|
|
|
// Resolve Local Path To File URI
|
|
export function pathToUri(p: string): string {
|
|
return pathToFileURL(path.resolve(p)).toString();
|
|
}
|
|
|
|
// Server Availability Cache - Checked once per process lifetime per server.
|
|
// Avoids repeated filesystem lookups on every tool call.
|
|
const serverAvailability = new Map<string, boolean>();
|
|
|
|
// Is Server Available - Returns true if the server binary is on PATH.
|
|
// Result is cached for the lifetime of this process.
|
|
export function isServerAvailable(server: ServerConfig): boolean {
|
|
if (serverAvailability.has(server.id)) return serverAvailability.get(server.id)!;
|
|
const available = isOnPath(server.command, process.env);
|
|
serverAvailability.set(server.id, available);
|
|
return available;
|
|
}
|
|
|
|
// Pick Server By File Extension - match[] entries are matched against the
|
|
// file's extension (no dot). First available, non-diagnosticsOnly server wins.
|
|
// Resolves the per-repo config from the file's directory before matching.
|
|
export function pickServer(filePath: string): ServerConfig {
|
|
const ext = path.extname(filePath).replace(/^\./, "");
|
|
const list = getServersForPath(filePath);
|
|
const hit = list.find((s) => s.match.includes(ext) && !s.diagnosticsOnly && isServerAvailable(s));
|
|
if (!hit) {
|
|
throw new UnsupportedExtensionError(`.${ext}`);
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
// Find Server By ID - Looks up a ServerConfig by id within the registry
|
|
// resolved for the given path. Throws if the id is not registered.
|
|
export function findServerById(filePath: string, id: string): ServerConfig {
|
|
const list = getServersForPath(filePath);
|
|
const hit = list.find((s) => s.id === id);
|
|
if (!hit) {
|
|
throw new Error(
|
|
`Unknown server ID: "${id}". Registered: ${list.map((s) => s.id).join(", ")}`,
|
|
);
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
// Find Project Root By Walking Upward - stops at the first directory
|
|
// containing any rootMarker. Falls back to the file's directory.
|
|
export function findRoot(filePath: string, markers: string[]): string {
|
|
let dir = path.dirname(path.resolve(filePath));
|
|
const { root } = path.parse(dir);
|
|
const allMarkers = [...markers, ...globalRootMarkers];
|
|
while (true) {
|
|
for (const m of allMarkers) {
|
|
if (fs.existsSync(path.join(dir, m))) return dir;
|
|
}
|
|
if (dir === root) return path.dirname(path.resolve(filePath));
|
|
dir = path.dirname(dir);
|
|
}
|
|
}
|