fix: report unavailable LSP servers explicitly
This commit is contained in:
70
index.ts
70
index.ts
@@ -18,8 +18,28 @@ import {
|
||||
UnsupportedExtensionError,
|
||||
} from "./src/types.ts";
|
||||
|
||||
type LspUnavailable = { piLspUnavailable: true; message: string };
|
||||
|
||||
function lspUnavailable(message: string): LspUnavailable {
|
||||
return { piLspUnavailable: true, message };
|
||||
}
|
||||
|
||||
function isLspUnavailable(result: unknown): result is LspUnavailable {
|
||||
return Boolean(
|
||||
result &&
|
||||
typeof result === "object" &&
|
||||
"piLspUnavailable" in result &&
|
||||
(result as { piLspUnavailable?: unknown }).piLspUnavailable === true,
|
||||
);
|
||||
}
|
||||
|
||||
function formatUnavailable(result: LspUnavailable): string {
|
||||
return `(LSP unavailable: ${result.message})`;
|
||||
}
|
||||
|
||||
// Format Hover - Turn an LSP hover response into readable text.
|
||||
function formatHover(result: unknown): string {
|
||||
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||
if (!result || typeof result !== "object") return "(no hover info)";
|
||||
const hover = result as { contents?: unknown };
|
||||
if (!hover.contents) return "(empty)";
|
||||
@@ -49,6 +69,7 @@ function formatHover(result: unknown): string {
|
||||
|
||||
// Format Definition - Turn definition locations into readable text.
|
||||
function formatDefinition(result: unknown): string {
|
||||
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||
if (!result) return "(no definition found)";
|
||||
const locations = Array.isArray(result) ? result : [result];
|
||||
if (locations.length === 0) return "(no definition found)";
|
||||
@@ -64,6 +85,7 @@ function formatDefinition(result: unknown): string {
|
||||
|
||||
// Format References - Turn reference locations into readable text.
|
||||
function formatReferences(result: unknown): string {
|
||||
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||
if (!result || !Array.isArray(result)) return "(no references found)";
|
||||
if (result.length === 0) return "(no references found)";
|
||||
|
||||
@@ -85,6 +107,7 @@ function formatReferences(result: unknown): string {
|
||||
|
||||
// Format Completions - Turn completion items into readable text.
|
||||
function formatCompletions(result: unknown): string {
|
||||
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||
if (!result) return "(no completions)";
|
||||
|
||||
// Resolve to CompletionItem[] if it's a CompletionList
|
||||
@@ -150,6 +173,7 @@ function formatCompletions(result: unknown): string {
|
||||
|
||||
// Format Document Symbols - Turn symbols into a tree outline.
|
||||
function formatDocumentSymbols(result: unknown): string {
|
||||
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||
if (!result || !Array.isArray(result)) return "(no symbols)";
|
||||
const symbols = result as any[];
|
||||
if (symbols.length === 0) return "(no symbols)";
|
||||
@@ -235,6 +259,7 @@ function formatServerDiagnostics(diags: any[], limit: number): string {
|
||||
// { [serverId]: { uri, diagnostics[] } }. Single-server results omit the
|
||||
// header to avoid noise.
|
||||
function formatDiagnostics(result: unknown, limit = 20): string {
|
||||
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||
if (!result || typeof result !== "object") return "(no diagnostics)";
|
||||
|
||||
const grouped = result as Record<string, any>;
|
||||
@@ -275,6 +300,34 @@ function isExpectedError(error: unknown): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function unavailableForFile(filePath: string, includeDiagnosticsOnly = false): LspUnavailable {
|
||||
const ext = path.extname(filePath).replace(/^\./, "");
|
||||
const label = ext ? `.${ext}` : "files without an extension";
|
||||
const matches = getServersForPath(filePath).filter((server) =>
|
||||
server.match.includes(ext) && (includeDiagnosticsOnly || !server.diagnosticsOnly)
|
||||
);
|
||||
|
||||
if (matches.length === 0) {
|
||||
return lspUnavailable(`no LSP server is registered for ${label}`);
|
||||
}
|
||||
|
||||
const disabled = matches.filter((server) => disabledServers.has(server.id));
|
||||
if (disabled.length === matches.length) {
|
||||
return lspUnavailable(
|
||||
`matching LSP server(s) are disabled: ${disabled.map((s) => s.id).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const unavailable = matches.filter((server) => !isServerAvailable(server));
|
||||
if (unavailable.length > 0) {
|
||||
return lspUnavailable(
|
||||
`matching LSP server(s) are not on PATH: ${unavailable.map((s) => s.command).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
return lspUnavailable(`no applicable LSP server is available for ${label}`);
|
||||
}
|
||||
|
||||
// Disabled Servers - In-memory set of disabled server IDs. Scoped to this
|
||||
// extension process; the shared daemon is never mutated by disable/enable.
|
||||
const lspToolNames = [
|
||||
@@ -301,14 +354,12 @@ async function runLsp(
|
||||
// touching the daemon so other pi instances sharing it are unaffected.
|
||||
const server = pickServer(filePath);
|
||||
if (disabledServers.has(server.id)) {
|
||||
throw new Error(
|
||||
`LSP server "${server.id}" is disabled. Use /lsp-enable ${server.id} to re-enable.`,
|
||||
);
|
||||
return unavailableForFile(filePath);
|
||||
}
|
||||
return await daemonRequest(filePath, server.id, method, params);
|
||||
} catch (error) {
|
||||
if (isExpectedError(error)) {
|
||||
return undefined;
|
||||
return unavailableForFile(filePath);
|
||||
}
|
||||
// Daemon-wrapped errors (plain Error with expected message) are also
|
||||
// expected — the daemon catches pickServer() throws and returns them
|
||||
@@ -318,7 +369,7 @@ async function runLsp(
|
||||
(error.message.includes("No LSP server registered") ||
|
||||
error.message.includes("not found on PATH"))
|
||||
) {
|
||||
return undefined;
|
||||
return unavailableForFile(filePath);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -347,22 +398,22 @@ function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
}
|
||||
|
||||
// Run LSP Diagnostics - Fans out to all matching servers in a single
|
||||
// daemon call. Returns the grouped result map or undefined if no servers.
|
||||
// daemon call. Returns an explicit unavailable result if no server applies.
|
||||
async function runDiagnostics(filePath: string): Promise<unknown> {
|
||||
try {
|
||||
const serverIds = pickDiagnosticServers(filePath);
|
||||
if (serverIds.length === 0) return undefined;
|
||||
if (serverIds.length === 0) return unavailableForFile(filePath, true);
|
||||
return await daemonDiagnostics(filePath, serverIds, 1500);
|
||||
} catch (error) {
|
||||
if (isExpectedError(error)) {
|
||||
return undefined;
|
||||
return unavailableForFile(filePath, true);
|
||||
}
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes("No LSP server registered") ||
|
||||
error.message.includes("not found on PATH"))
|
||||
) {
|
||||
return undefined;
|
||||
return unavailableForFile(filePath, true);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -572,6 +623,7 @@ export default function (pi: ExtensionAPI) {
|
||||
// Run LSP diagnostics with timeout - Prevent the auto-check hook from
|
||||
// blocking pi if the daemon or LSP server is slow or unresponsive.
|
||||
const result = await withTimeout(runDiagnostics(absolutePath), 3000);
|
||||
if (isLspUnavailable(result)) return;
|
||||
const formatted = formatDiagnostics(result, 10);
|
||||
|
||||
// Only send a message if there are actual diagnostics
|
||||
|
||||
Reference in New Issue
Block a user