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:
@@ -1,9 +1,8 @@
|
||||
import { spawn, type Subprocess } from "bun"
|
||||
import { readFileSync } from "fs"
|
||||
import { extname, resolve } from "path"
|
||||
import type { ResolvedServer } from "./config"
|
||||
import { getLanguageId } from "./config"
|
||||
import type { Diagnostic } from "./types"
|
||||
import type { Diagnostic, ResolvedServer } from "./types"
|
||||
|
||||
interface ManagedClient {
|
||||
client: LSPClient
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { existsSync, readFileSync } from "fs"
|
||||
import { join } from "path"
|
||||
import { homedir } from "os"
|
||||
import { BUILTIN_SERVERS, EXT_TO_LANG } from "./constants"
|
||||
|
||||
export interface ResolvedServer {
|
||||
id: string
|
||||
command: string[]
|
||||
extensions: string[]
|
||||
priority: number
|
||||
env?: Record<string, string>
|
||||
initialization?: Record<string, unknown>
|
||||
}
|
||||
import { BUILTIN_SERVERS, EXT_TO_LANG, LSP_INSTALL_HINTS } from "./constants"
|
||||
import type { ResolvedServer, ServerLookupResult } from "./types"
|
||||
|
||||
interface LspEntry {
|
||||
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()
|
||||
|
||||
for (const server of servers) {
|
||||
if (server.extensions.includes(ext) && isServerInstalled(server.command)) {
|
||||
return {
|
||||
id: server.id,
|
||||
command: server.command,
|
||||
extensions: server.extensions,
|
||||
priority: server.priority,
|
||||
env: server.env,
|
||||
initialization: server.initialization,
|
||||
status: "found",
|
||||
server: {
|
||||
id: server.id,
|
||||
command: server.command,
|
||||
extensions: server.extensions,
|
||||
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 {
|
||||
|
||||
@@ -40,6 +40,37 @@ export const DEFAULT_MAX_REFERENCES = 200
|
||||
export const DEFAULT_MAX_SYMBOLS = 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
|
||||
// https://github.com/sst/opencode/blob/main/packages/opencode/src/lsp/server.ts
|
||||
export const BUILTIN_SERVERS: Record<string, Omit<LSPServerConfig, "id">> = {
|
||||
|
||||
@@ -135,3 +135,23 @@ export interface CodeAction {
|
||||
command?: Command
|
||||
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>
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
TextEdit,
|
||||
CodeAction,
|
||||
Command,
|
||||
ServerLookupResult,
|
||||
} from "./types"
|
||||
|
||||
export function findWorkspaceRoot(filePath: string): string {
|
||||
@@ -40,15 +41,51 @@ export function findWorkspaceRoot(filePath: string): string {
|
||||
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> {
|
||||
const absPath = resolve(filePath)
|
||||
const ext = extname(absPath)
|
||||
const server = findServerForExtension(ext)
|
||||
const result = findServerForExtension(ext)
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`No LSP server configured for extension: ${ext}`)
|
||||
if (result.status !== "found") {
|
||||
throw new Error(formatServerLookupError(result))
|
||||
}
|
||||
|
||||
const server = result.server
|
||||
const root = findWorkspaceRoot(absPath)
|
||||
const client = await lspManager.getClient(root, server)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user