From 7abe4efa02f14bdb3cd66d86641b5fceb624a2d5 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Thu, 30 Apr 2026 08:27:13 -0400 Subject: [PATCH] refactor: replace string-matching error checks with custom error classes --- index.ts | 43 ++++++++++++++++++++++++++++++++++++------- src/client.ts | 6 ++---- src/root.ts | 3 ++- src/types.ts | 19 +++++++++++++++++++ 4 files changed, 59 insertions(+), 12 deletions(-) diff --git a/index.ts b/index.ts index 223710d..c74a6d7 100644 --- a/index.ts +++ b/index.ts @@ -7,6 +7,10 @@ import { Type } from "typebox"; import * as path from "node:path"; import { uriToPath } from "./src/client.ts"; import { daemonDiagnostics, daemonRequest } from "./src/daemonClient.ts"; +import { + ServerNotFoundError, + UnsupportedExtensionError, +} from "./src/types.ts"; // Format Hover - Turn an LSP hover response into readable text. function formatHover(result: unknown): string { @@ -202,6 +206,16 @@ function formatDiagnostics(result: unknown): string { .join("\n"); } +// Is Expected Error - Returns true if the error is an expected condition +// (unsupported file type or missing server binary) that should be +// suppressed rather than surfaced to the user. +function isExpectedError(error: unknown): boolean { + return ( + error instanceof UnsupportedExtensionError || + error instanceof ServerNotFoundError + ); +} + // Run LSP Request - Forwards to the daemon, which owns the long-lived // LspClient cache and handles didOpen/didChange syncing. The daemon // injects textDocument.uri from the file path, so we omit it here. @@ -210,13 +224,27 @@ async function runLsp( method: string, params: Record, ): Promise { - return daemonRequest(filePath, method, params); + try { + return await daemonRequest(filePath, method, params); + } catch (error) { + if (isExpectedError(error)) { + return undefined; + } + throw error; + } } // Run LSP Diagnostics - Diagnostics arrive as a notification, so the // daemon has a dedicated op that waits for the next publish. async function runDiagnostics(filePath: string): Promise { - return daemonDiagnostics(filePath, 1500); + try { + return await daemonDiagnostics(filePath, 1500); + } catch (error) { + if (isExpectedError(error)) { + return undefined; + } + throw error; + } } // Shared Parameters Schema - All position-based tools accept file + optional @@ -412,11 +440,12 @@ export default function (pi: ExtensionAPI) { } catch (error) { // Silently fail - don't interrupt the flow // Only log if there's an actual error we care about - if (error && typeof error === "object" && "message" in error) { - const msg = (error as { message: string }).message; - if (!msg.includes("not found on PATH")) { - console.error("LSP auto-check failed:", msg); - } + if (!isExpectedError(error)) { + const msg = + error && typeof error === "object" && "message" in error + ? (error as { message: string }).message + : String(error); + console.error("LSP auto-check failed:", msg); } } }); diff --git a/src/client.ts b/src/client.ts index 6e7fb28..dc2fe08 100644 --- a/src/client.ts +++ b/src/client.ts @@ -12,6 +12,7 @@ import type { } from "vscode-languageserver-protocol"; import * as path from "node:path"; import type { ServerConfig } from "./types.ts"; +import { ServerNotFoundError } from "./types.ts"; import { findRoot, pathToUri, uriToPath } from "./root.ts"; // Is On PATH - Returns true if `cmd` resolves to an executable via the @@ -73,10 +74,7 @@ export class LspClient { // letting spawn ENOENT surface as a generic error. It's the user's // responsibility to have the server installed & on PATH. if (!isOnPath(this.server.command)) { - throw new Error( - `LSP server binary "${this.server.command}" not found on PATH. ` + - `Install it and ensure it's on your PATH (required by server "${this.server.id}").`, - ); + throw new ServerNotFoundError(this.server.command); } this.proc = spawn(this.server.command, this.server.args, { stdio: ["pipe", "pipe", "pipe"], diff --git a/src/root.ts b/src/root.ts index 050490e..158f9da 100644 --- a/src/root.ts +++ b/src/root.ts @@ -3,6 +3,7 @@ import * as path from "node:path"; import { pathToFileURL, fileURLToPath } from "node:url"; import { servers } from "../server.ts"; import type { ServerConfig } from "./types.ts"; +import { UnsupportedExtensionError } from "./types.ts"; // Resolve File URI To Local Path export function uriToPath(uri: string): string { @@ -20,7 +21,7 @@ export function pickServer(filePath: string): ServerConfig { const ext = path.extname(filePath).replace(/^\./, ""); const hit = servers.find((s) => s.match.includes(ext)); if (!hit) { - throw new Error(`No LSP server registered for extension ".${ext}"`); + throw new UnsupportedExtensionError(`.${ext}`); } return hit; } diff --git a/src/types.ts b/src/types.ts index 5255aa5..3a529c9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,22 @@ +// LSP Errors - Custom error classes so callers can distinguish expected +// conditions (unsupported file type, missing binary) from unexpected ones. +export class UnsupportedExtensionError extends Error { + constructor(ext: string) { + super(`No LSP server registered for extension "${ext}"`); + this.name = "UnsupportedExtensionError"; + } +} + +export class ServerNotFoundError extends Error { + constructor(command: string) { + super( + `LSP server binary "${command}" not found on PATH. ` + + `Install it and ensure it's on your PATH.`, + ); + this.name = "ServerNotFoundError"; + } +} + export interface ServerConfig { // Stable identifier (useful for logs and future daemon cache keys). id: string;