fix: report unavailable LSP servers explicitly
This commit is contained in:
70
index.ts
70
index.ts
@@ -18,8 +18,28 @@ import {
|
|||||||
UnsupportedExtensionError,
|
UnsupportedExtensionError,
|
||||||
} from "./src/types.ts";
|
} 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.
|
// Format Hover - Turn an LSP hover response into readable text.
|
||||||
function formatHover(result: unknown): string {
|
function formatHover(result: unknown): string {
|
||||||
|
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||||
if (!result || typeof result !== "object") return "(no hover info)";
|
if (!result || typeof result !== "object") return "(no hover info)";
|
||||||
const hover = result as { contents?: unknown };
|
const hover = result as { contents?: unknown };
|
||||||
if (!hover.contents) return "(empty)";
|
if (!hover.contents) return "(empty)";
|
||||||
@@ -49,6 +69,7 @@ function formatHover(result: unknown): string {
|
|||||||
|
|
||||||
// Format Definition - Turn definition locations into readable text.
|
// Format Definition - Turn definition locations into readable text.
|
||||||
function formatDefinition(result: unknown): string {
|
function formatDefinition(result: unknown): string {
|
||||||
|
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||||
if (!result) return "(no definition found)";
|
if (!result) return "(no definition found)";
|
||||||
const locations = Array.isArray(result) ? result : [result];
|
const locations = Array.isArray(result) ? result : [result];
|
||||||
if (locations.length === 0) return "(no definition found)";
|
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.
|
// Format References - Turn reference locations into readable text.
|
||||||
function formatReferences(result: unknown): string {
|
function formatReferences(result: unknown): string {
|
||||||
|
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||||
if (!result || !Array.isArray(result)) return "(no references found)";
|
if (!result || !Array.isArray(result)) return "(no references found)";
|
||||||
if (result.length === 0) 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.
|
// Format Completions - Turn completion items into readable text.
|
||||||
function formatCompletions(result: unknown): string {
|
function formatCompletions(result: unknown): string {
|
||||||
|
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||||
if (!result) return "(no completions)";
|
if (!result) return "(no completions)";
|
||||||
|
|
||||||
// Resolve to CompletionItem[] if it's a CompletionList
|
// 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.
|
// Format Document Symbols - Turn symbols into a tree outline.
|
||||||
function formatDocumentSymbols(result: unknown): string {
|
function formatDocumentSymbols(result: unknown): string {
|
||||||
|
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||||
if (!result || !Array.isArray(result)) return "(no symbols)";
|
if (!result || !Array.isArray(result)) return "(no symbols)";
|
||||||
const symbols = result as any[];
|
const symbols = result as any[];
|
||||||
if (symbols.length === 0) return "(no symbols)";
|
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
|
// { [serverId]: { uri, diagnostics[] } }. Single-server results omit the
|
||||||
// header to avoid noise.
|
// header to avoid noise.
|
||||||
function formatDiagnostics(result: unknown, limit = 20): string {
|
function formatDiagnostics(result: unknown, limit = 20): string {
|
||||||
|
if (isLspUnavailable(result)) return formatUnavailable(result);
|
||||||
if (!result || typeof result !== "object") return "(no diagnostics)";
|
if (!result || typeof result !== "object") return "(no diagnostics)";
|
||||||
|
|
||||||
const grouped = result as Record<string, any>;
|
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
|
// Disabled Servers - In-memory set of disabled server IDs. Scoped to this
|
||||||
// extension process; the shared daemon is never mutated by disable/enable.
|
// extension process; the shared daemon is never mutated by disable/enable.
|
||||||
const lspToolNames = [
|
const lspToolNames = [
|
||||||
@@ -301,14 +354,12 @@ async function runLsp(
|
|||||||
// touching the daemon so other pi instances sharing it are unaffected.
|
// touching the daemon so other pi instances sharing it are unaffected.
|
||||||
const server = pickServer(filePath);
|
const server = pickServer(filePath);
|
||||||
if (disabledServers.has(server.id)) {
|
if (disabledServers.has(server.id)) {
|
||||||
throw new Error(
|
return unavailableForFile(filePath);
|
||||||
`LSP server "${server.id}" is disabled. Use /lsp-enable ${server.id} to re-enable.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return await daemonRequest(filePath, server.id, method, params);
|
return await daemonRequest(filePath, server.id, method, params);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isExpectedError(error)) {
|
if (isExpectedError(error)) {
|
||||||
return undefined;
|
return unavailableForFile(filePath);
|
||||||
}
|
}
|
||||||
// Daemon-wrapped errors (plain Error with expected message) are also
|
// Daemon-wrapped errors (plain Error with expected message) are also
|
||||||
// expected — the daemon catches pickServer() throws and returns them
|
// 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("No LSP server registered") ||
|
||||||
error.message.includes("not found on PATH"))
|
error.message.includes("not found on PATH"))
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return unavailableForFile(filePath);
|
||||||
}
|
}
|
||||||
throw error;
|
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
|
// 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> {
|
async function runDiagnostics(filePath: string): Promise<unknown> {
|
||||||
try {
|
try {
|
||||||
const serverIds = pickDiagnosticServers(filePath);
|
const serverIds = pickDiagnosticServers(filePath);
|
||||||
if (serverIds.length === 0) return undefined;
|
if (serverIds.length === 0) return unavailableForFile(filePath, true);
|
||||||
return await daemonDiagnostics(filePath, serverIds, 1500);
|
return await daemonDiagnostics(filePath, serverIds, 1500);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isExpectedError(error)) {
|
if (isExpectedError(error)) {
|
||||||
return undefined;
|
return unavailableForFile(filePath, true);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
(error.message.includes("No LSP server registered") ||
|
(error.message.includes("No LSP server registered") ||
|
||||||
error.message.includes("not found on PATH"))
|
error.message.includes("not found on PATH"))
|
||||||
) {
|
) {
|
||||||
return undefined;
|
return unavailableForFile(filePath, true);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -572,6 +623,7 @@ export default function (pi: ExtensionAPI) {
|
|||||||
// Run LSP diagnostics with timeout - Prevent the auto-check hook from
|
// Run LSP diagnostics with timeout - Prevent the auto-check hook from
|
||||||
// blocking pi if the daemon or LSP server is slow or unresponsive.
|
// blocking pi if the daemon or LSP server is slow or unresponsive.
|
||||||
const result = await withTimeout(runDiagnostics(absolutePath), 3000);
|
const result = await withTimeout(runDiagnostics(absolutePath), 3000);
|
||||||
|
if (isLspUnavailable(result)) return;
|
||||||
const formatted = formatDiagnostics(result, 10);
|
const formatted = formatDiagnostics(result, 10);
|
||||||
|
|
||||||
// Only send a message if there are actual diagnostics
|
// Only send a message if there are actual diagnostics
|
||||||
|
|||||||
Reference in New Issue
Block a user