fix(lsp): improve error messages when LSP server is not installed (#305)

Previously, when an LSP server was configured but not installed, the error
message said "No LSP server configured" which was misleading. Now the
error message distinguishes between:

1. Server not configured at all
2. Server configured but not installed (with installation hints)

The new error messages include:
- Clear indication of whether server is configured vs installed
- Installation commands for each built-in server
- Supported file extensions
- Configuration examples for custom servers

Fixes #304

Co-authored-by: sisyphus-dev-ai <sisyphus-dev-ai@users.noreply.github.com>
This commit is contained in:
Sisyphus
2025-12-28 17:38:23 +09:00
committed by GitHub
parent 4d4273603a
commit 4d66ea9730
5 changed files with 126 additions and 23 deletions

View File

@@ -1,9 +1,8 @@
import { spawn, type Subprocess } from "bun" import { spawn, type Subprocess } from "bun"
import { readFileSync } from "fs" import { readFileSync } from "fs"
import { extname, resolve } from "path" import { extname, resolve } from "path"
import type { ResolvedServer } from "./config"
import { getLanguageId } from "./config" import { getLanguageId } from "./config"
import type { Diagnostic } from "./types" import type { Diagnostic, ResolvedServer } from "./types"
interface ManagedClient { interface ManagedClient {
client: LSPClient client: LSPClient

View File

@@ -1,16 +1,8 @@
import { existsSync, readFileSync } from "fs" import { existsSync, readFileSync } from "fs"
import { join } from "path" import { join } from "path"
import { homedir } from "os" import { homedir } from "os"
import { BUILTIN_SERVERS, EXT_TO_LANG } from "./constants" import { BUILTIN_SERVERS, EXT_TO_LANG, LSP_INSTALL_HINTS } from "./constants"
import type { ResolvedServer, ServerLookupResult } from "./types"
export interface ResolvedServer {
id: string
command: string[]
extensions: string[]
priority: number
env?: Record<string, string>
initialization?: Record<string, unknown>
}
interface LspEntry { interface LspEntry {
disabled?: boolean disabled?: boolean
@@ -120,23 +112,47 @@ function getMergedServers(): ServerWithSource[] {
}) })
} }
export function findServerForExtension(ext: string): ResolvedServer | null { export function findServerForExtension(ext: string): ServerLookupResult {
const servers = getMergedServers() const servers = getMergedServers()
for (const server of servers) { for (const server of servers) {
if (server.extensions.includes(ext) && isServerInstalled(server.command)) { if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
return { return {
id: server.id, status: "found",
command: server.command, server: {
extensions: server.extensions, id: server.id,
priority: server.priority, command: server.command,
env: server.env, extensions: server.extensions,
initialization: server.initialization, priority: server.priority,
env: server.env,
initialization: server.initialization,
},
} }
} }
} }
return null for (const server of servers) {
if (server.extensions.includes(ext)) {
const installHint =
LSP_INSTALL_HINTS[server.id] || `Install '${server.command[0]}' and ensure it's in your PATH`
return {
status: "not_installed",
server: {
id: server.id,
command: server.command,
extensions: server.extensions,
},
installHint,
}
}
}
const availableServers = [...new Set(servers.map((s) => s.id))]
return {
status: "not_configured",
extension: ext,
availableServers,
}
} }
export function getLanguageId(ext: string): string { export function getLanguageId(ext: string): string {

View File

@@ -40,6 +40,37 @@ export const DEFAULT_MAX_REFERENCES = 200
export const DEFAULT_MAX_SYMBOLS = 200 export const DEFAULT_MAX_SYMBOLS = 200
export const DEFAULT_MAX_DIAGNOSTICS = 200 export const DEFAULT_MAX_DIAGNOSTICS = 200
export const LSP_INSTALL_HINTS: Record<string, string> = {
typescript: "npm install -g typescript-language-server typescript",
deno: "Install Deno from https://deno.land",
vue: "npm install -g @vue/language-server",
eslint: "npm install -g vscode-langservers-extracted",
oxlint: "npm install -g oxlint",
biome: "npm install -g @biomejs/biome",
gopls: "go install golang.org/x/tools/gopls@latest",
"ruby-lsp": "gem install ruby-lsp",
basedpyright: "pip install basedpyright",
pyright: "pip install pyright",
ty: "pip install ty",
ruff: "pip install ruff",
"elixir-ls": "See https://github.com/elixir-lsp/elixir-ls",
zls: "See https://github.com/zigtools/zls",
csharp: "dotnet tool install -g csharp-ls",
fsharp: "dotnet tool install -g fsautocomplete",
"sourcekit-lsp": "Included with Xcode or Swift toolchain",
rust: "rustup component add rust-analyzer",
clangd: "See https://clangd.llvm.org/installation",
svelte: "npm install -g svelte-language-server",
astro: "npm install -g @astrojs/language-server",
"bash-ls": "npm install -g bash-language-server",
jdtls: "See https://github.com/eclipse-jdtls/eclipse.jdt.ls",
"yaml-ls": "npm install -g yaml-language-server",
"lua-ls": "See https://github.com/LuaLS/lua-language-server",
php: "npm install -g intelephense",
dart: "Included with Dart SDK",
"terraform-ls": "See https://github.com/hashicorp/terraform-ls",
}
// Synced with OpenCode's server.ts // Synced with OpenCode's server.ts
// https://github.com/sst/opencode/blob/main/packages/opencode/src/lsp/server.ts // https://github.com/sst/opencode/blob/main/packages/opencode/src/lsp/server.ts
export const BUILTIN_SERVERS: Record<string, Omit<LSPServerConfig, "id">> = { export const BUILTIN_SERVERS: Record<string, Omit<LSPServerConfig, "id">> = {

View File

@@ -135,3 +135,23 @@ export interface CodeAction {
command?: Command command?: Command
data?: unknown data?: unknown
} }
export interface ServerLookupInfo {
id: string
command: string[]
extensions: string[]
}
export type ServerLookupResult =
| { status: "found"; server: ResolvedServer }
| { status: "not_configured"; extension: string; availableServers: string[] }
| { status: "not_installed"; server: ServerLookupInfo; installHint: string }
export interface ResolvedServer {
id: string
command: string[]
extensions: string[]
priority: number
env?: Record<string, string>
initialization?: Record<string, unknown>
}

View File

@@ -17,6 +17,7 @@ import type {
TextEdit, TextEdit,
CodeAction, CodeAction,
Command, Command,
ServerLookupResult,
} from "./types" } from "./types"
export function findWorkspaceRoot(filePath: string): string { export function findWorkspaceRoot(filePath: string): string {
@@ -40,15 +41,51 @@ export function findWorkspaceRoot(filePath: string): string {
return require("path").dirname(resolve(filePath)) return require("path").dirname(resolve(filePath))
} }
export function formatServerLookupError(result: Exclude<ServerLookupResult, { status: "found" }>): string {
if (result.status === "not_installed") {
const { server, installHint } = result
return [
`LSP server '${server.id}' is configured but NOT INSTALLED.`,
``,
`Command not found: ${server.command[0]}`,
``,
`To install:`,
` ${installHint}`,
``,
`Supported extensions: ${server.extensions.join(", ")}`,
``,
`After installation, the server will be available automatically.`,
`Run 'lsp_servers' tool to verify installation status.`,
].join("\n")
}
return [
`No LSP server configured for extension: ${result.extension}`,
``,
`Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
``,
`To add a custom server, configure 'lsp' in oh-my-opencode.json:`,
` {`,
` "lsp": {`,
` "my-server": {`,
` "command": ["my-lsp", "--stdio"],`,
` "extensions": ["${result.extension}"]`,
` }`,
` }`,
` }`,
].join("\n")
}
export async function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T> { export async function withLspClient<T>(filePath: string, fn: (client: LSPClient) => Promise<T>): Promise<T> {
const absPath = resolve(filePath) const absPath = resolve(filePath)
const ext = extname(absPath) const ext = extname(absPath)
const server = findServerForExtension(ext) const result = findServerForExtension(ext)
if (!server) { if (result.status !== "found") {
throw new Error(`No LSP server configured for extension: ${ext}`) throw new Error(formatServerLookupError(result))
} }
const server = result.server
const root = findWorkspaceRoot(absPath) const root = findWorkspaceRoot(absPath)
const client = await lspManager.getClient(root, server) const client = await lspManager.getClient(root, server)