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 { 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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
status: "found",
|
||||||
|
server: {
|
||||||
id: server.id,
|
id: server.id,
|
||||||
command: server.command,
|
command: server.command,
|
||||||
extensions: server.extensions,
|
extensions: server.extensions,
|
||||||
priority: server.priority,
|
priority: server.priority,
|
||||||
env: server.env,
|
env: server.env,
|
||||||
initialization: server.initialization,
|
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 {
|
||||||
|
|||||||
@@ -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">> = {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user