cloudaxe-opencode/packages/opencode/src/lsp/index.ts

277 lines
7.2 KiB
TypeScript
Raw Normal View History

2025-05-31 18:41:00 +00:00
import { App } from "../app/app"
import { Log } from "../util/log"
import { LSPClient } from "./client"
import path from "path"
2025-06-07 03:21:57 +00:00
import { LSPServer } from "./server"
2025-07-02 15:35:25 +00:00
import { z } from "zod"
2025-08-01 18:51:58 +00:00
import { Config } from "../config/config"
import { spawn } from "child_process"
2025-05-21 02:00:00 +00:00
export namespace LSP {
2025-05-31 18:41:00 +00:00
const log = Log.create({ service: "lsp" })
2025-05-21 02:00:00 +00:00
2025-07-10 10:51:47 +00:00
export const Range = z
.object({
start: z.object({
line: z.number(),
character: z.number(),
}),
end: z.object({
line: z.number(),
character: z.number(),
}),
})
.openapi({
ref: "Range",
})
export type Range = z.infer<typeof Range>
2025-07-02 15:35:25 +00:00
export const Symbol = z
.object({
name: z.string(),
kind: z.number(),
location: z.object({
uri: z.string(),
2025-07-10 10:51:47 +00:00
range: Range,
2025-07-02 15:35:25 +00:00
}),
})
.openapi({
2025-07-09 13:16:10 +00:00
ref: "Symbol",
2025-07-02 15:35:25 +00:00
})
export type Symbol = z.infer<typeof Symbol>
2025-07-10 10:51:47 +00:00
export const DocumentSymbol = z
.object({
name: z.string(),
detail: z.string().optional(),
kind: z.number(),
range: Range,
selectionRange: Range,
})
.openapi({
ref: "DocumentSymbol",
})
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
2025-05-26 18:52:38 +00:00
const state = App.state(
"lsp",
2025-07-10 13:37:28 +00:00
async () => {
2025-07-08 22:14:24 +00:00
const clients: LSPClient.Info[] = []
2025-08-17 02:59:18 +00:00
const servers: Record<string, LSPServer.Info> = {}
for (const server of Object.values(LSPServer)) {
servers[server.id] = server
}
2025-08-01 18:51:58 +00:00
const cfg = await Config.get()
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
const existing = servers[name]
if (item.disabled) {
2025-08-17 02:59:18 +00:00
log.info(`LSP server ${name} is disabled`)
2025-08-01 18:51:58 +00:00
delete servers[name]
continue
}
servers[name] = {
...existing,
root: existing?.root ?? (async (_file, app) => app.path.root),
2025-08-01 18:51:58 +00:00
extensions: item.extensions ?? existing.extensions,
spawn: async (_app, root) => {
return {
process: spawn(item.command[0], item.command.slice(1), {
cwd: root,
env: {
...process.env,
...item.env,
},
}),
initialization: item.initialization,
}
},
}
}
2025-08-17 02:59:18 +00:00
log.info("enabled LSP servers", {
serverIds: Object.values(servers)
.map((server) => server.id)
.join(", "),
})
2025-05-26 18:52:38 +00:00
return {
2025-07-10 13:37:28 +00:00
broken: new Set<string>(),
2025-08-01 21:48:01 +00:00
servers,
2025-05-26 18:52:38 +00:00
clients,
2025-05-31 18:41:00 +00:00
}
2025-05-26 18:52:38 +00:00
},
async (state) => {
2025-07-08 22:14:24 +00:00
for (const client of state.clients) {
2025-05-31 18:41:00 +00:00
await client.shutdown()
2025-05-26 18:52:38 +00:00
}
},
2025-05-31 18:41:00 +00:00
)
2025-05-21 02:00:00 +00:00
2025-06-30 20:45:13 +00:00
export async function init() {
2025-07-01 02:46:42 +00:00
return state()
2025-06-30 20:45:13 +00:00
}
2025-07-10 13:37:28 +00:00
async function getClients(file: string) {
const s = await state()
const extension = path.parse(file).ext
const result: LSPClient.Info[] = []
2025-08-17 02:59:18 +00:00
for (const server of Object.values(s.servers)) {
2025-08-01 18:51:58 +00:00
if (server.extensions.length && !server.extensions.includes(extension)) continue
2025-07-10 13:37:28 +00:00
const root = await server.root(file, App.info())
if (!root) continue
if (s.broken.has(root + server.id)) continue
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
if (match) {
result.push(match)
continue
}
const handle = await server.spawn(App.info(), root).catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
2025-07-10 13:37:28 +00:00
if (!handle) continue
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
2025-08-26 04:11:38 +00:00
return undefined
2025-07-10 13:37:28 +00:00
})
if (!client) continue
s.clients.push(client)
result.push(client)
}
return result
}
2025-06-06 21:20:01 +00:00
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
2025-07-10 13:37:28 +00:00
const clients = await getClients(input)
2025-07-01 02:46:42 +00:00
await run(async (client) => {
2025-07-10 13:37:28 +00:00
if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
2025-07-01 02:46:42 +00:00
await client.notify.open({ path: input })
return wait
})
2025-05-26 21:15:41 +00:00
}
export async function diagnostics() {
2025-05-31 18:41:00 +00:00
const results: Record<string, LSPClient.Diagnostic[]> = {}
2025-05-26 21:15:41 +00:00
for (const result of await run(async (client) => client.diagnostics)) {
for (const [path, diagnostics] of result.entries()) {
2025-05-31 18:41:00 +00:00
const arr = results[path] || []
arr.push(...diagnostics)
results[path] = arr
2025-05-26 21:15:41 +00:00
}
}
2025-05-31 18:41:00 +00:00
return results
2025-05-26 21:15:41 +00:00
}
export async function hover(input: { file: string; line: number; character: number }) {
return run((client) => {
return client.connection.sendRequest("textDocument/hover", {
textDocument: {
uri: `file://${input.file}`,
},
position: {
line: input.line,
character: input.character,
},
2025-05-31 18:41:00 +00:00
})
})
}
2025-07-10 10:51:47 +00:00
enum SymbolKind {
File = 1,
Module = 2,
Namespace = 3,
Package = 4,
Class = 5,
Method = 6,
Property = 7,
Field = 8,
Constructor = 9,
Enum = 10,
Interface = 11,
Function = 12,
Variable = 13,
Constant = 14,
String = 15,
Number = 16,
Boolean = 17,
Array = 18,
Object = 19,
Key = 20,
Null = 21,
EnumMember = 22,
Struct = 23,
Event = 24,
Operator = 25,
TypeParameter = 26,
}
const kinds = [
SymbolKind.Class,
SymbolKind.Function,
SymbolKind.Method,
SymbolKind.Interface,
SymbolKind.Variable,
SymbolKind.Constant,
SymbolKind.Struct,
SymbolKind.Enum,
]
2025-06-30 20:45:13 +00:00
export async function workspaceSymbol(query: string) {
return run((client) =>
2025-07-08 22:14:24 +00:00
client.connection
.sendRequest("workspace/symbol", {
query,
})
2025-07-10 10:51:47 +00:00
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
2025-07-08 22:14:24 +00:00
.then((result: any) => result.slice(0, 10))
.catch(() => []),
2025-07-02 15:35:25 +00:00
).then((result) => result.flat() as LSP.Symbol[])
2025-06-30 20:45:13 +00:00
}
2025-07-10 10:51:47 +00:00
export async function documentSymbol(uri: string) {
return run((client) =>
client.connection
.sendRequest("textDocument/documentSymbol", {
textDocument: {
uri,
},
})
.catch(() => []),
)
.then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
.then((result) => result.filter(Boolean))
}
async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
2025-07-08 22:14:24 +00:00
const clients = await state().then((x) => x.clients)
2025-05-31 18:41:00 +00:00
const tasks = clients.map((x) => input(x))
return Promise.all(tasks)
2025-05-21 02:00:00 +00:00
}
2025-05-26 17:39:16 +00:00
2025-05-26 21:15:41 +00:00
export namespace Diagnostic {
export function pretty(diagnostic: LSPClient.Diagnostic) {
const severityMap = {
1: "ERROR",
2: "WARN",
3: "INFO",
4: "HINT",
2025-05-31 18:41:00 +00:00
}
2025-05-26 21:15:41 +00:00
2025-05-31 18:41:00 +00:00
const severity = severityMap[diagnostic.severity || 1]
const line = diagnostic.range.start.line + 1
const col = diagnostic.range.start.character + 1
2025-05-26 21:15:41 +00:00
2025-05-31 18:41:00 +00:00
return `${severity} [${line}:${col}] ${diagnostic.message}`
2025-05-26 21:15:41 +00:00
}
}
2025-05-21 02:00:00 +00:00
}