mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 19:24:22 +01:00
323 lines
8.3 KiB
TypeScript
323 lines
8.3 KiB
TypeScript
import { Log } from "../util/log"
|
|
import { LSPClient } from "./client"
|
|
import path from "path"
|
|
import { LSPServer } from "./server"
|
|
import z from "zod"
|
|
import { Config } from "../config/config"
|
|
import { spawn } from "child_process"
|
|
import { Instance } from "../project/instance"
|
|
import { Bus } from "../bus"
|
|
|
|
export namespace LSP {
|
|
const log = Log.create({ service: "lsp" })
|
|
|
|
export const Event = {
|
|
Updated: Bus.event("lsp.updated", z.object({})),
|
|
}
|
|
|
|
export const Range = z
|
|
.object({
|
|
start: z.object({
|
|
line: z.number(),
|
|
character: z.number(),
|
|
}),
|
|
end: z.object({
|
|
line: z.number(),
|
|
character: z.number(),
|
|
}),
|
|
})
|
|
.meta({
|
|
ref: "Range",
|
|
})
|
|
export type Range = z.infer<typeof Range>
|
|
|
|
export const Symbol = z
|
|
.object({
|
|
name: z.string(),
|
|
kind: z.number(),
|
|
location: z.object({
|
|
uri: z.string(),
|
|
range: Range,
|
|
}),
|
|
})
|
|
.meta({
|
|
ref: "Symbol",
|
|
})
|
|
export type Symbol = z.infer<typeof Symbol>
|
|
|
|
export const DocumentSymbol = z
|
|
.object({
|
|
name: z.string(),
|
|
detail: z.string().optional(),
|
|
kind: z.number(),
|
|
range: Range,
|
|
selectionRange: Range,
|
|
})
|
|
.meta({
|
|
ref: "DocumentSymbol",
|
|
})
|
|
export type DocumentSymbol = z.infer<typeof DocumentSymbol>
|
|
|
|
const state = Instance.state(
|
|
async () => {
|
|
const clients: LSPClient.Info[] = []
|
|
const servers: Record<string, LSPServer.Info> = {}
|
|
for (const server of Object.values(LSPServer)) {
|
|
servers[server.id] = server
|
|
}
|
|
const cfg = await Config.get()
|
|
for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
|
|
const existing = servers[name]
|
|
if (item.disabled) {
|
|
log.info(`LSP server ${name} is disabled`)
|
|
delete servers[name]
|
|
continue
|
|
}
|
|
servers[name] = {
|
|
...existing,
|
|
id: name,
|
|
root: existing?.root ?? (async () => Instance.directory),
|
|
extensions: item.extensions ?? existing?.extensions ?? [],
|
|
spawn: async (root) => {
|
|
return {
|
|
process: spawn(item.command[0], item.command.slice(1), {
|
|
cwd: root,
|
|
env: {
|
|
...process.env,
|
|
...item.env,
|
|
},
|
|
}),
|
|
initialization: item.initialization,
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
log.info("enabled LSP servers", {
|
|
serverIds: Object.values(servers)
|
|
.map((server) => server.id)
|
|
.join(", "),
|
|
})
|
|
|
|
return {
|
|
broken: new Set<string>(),
|
|
servers,
|
|
clients,
|
|
}
|
|
},
|
|
async (state) => {
|
|
await Promise.all(state.clients.map((client) => client.shutdown()))
|
|
},
|
|
)
|
|
|
|
export async function init() {
|
|
return state()
|
|
}
|
|
|
|
export const Status = z
|
|
.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
root: z.string(),
|
|
status: z.union([z.literal("connected"), z.literal("error")]),
|
|
})
|
|
.meta({
|
|
ref: "LSPStatus",
|
|
})
|
|
export type Status = z.infer<typeof Status>
|
|
|
|
export async function status() {
|
|
return state().then((x) => {
|
|
const result: Status[] = []
|
|
for (const client of x.clients) {
|
|
result.push({
|
|
id: client.serverID,
|
|
name: x.servers[client.serverID].id,
|
|
root: path.relative(Instance.directory, client.root),
|
|
status: "connected",
|
|
})
|
|
}
|
|
return result
|
|
})
|
|
}
|
|
|
|
async function getClients(file: string) {
|
|
const s = await state()
|
|
const extension = path.parse(file).ext || file
|
|
const result: LSPClient.Info[] = []
|
|
for (const server of Object.values(s.servers)) {
|
|
if (server.extensions.length && !server.extensions.includes(extension)) continue
|
|
const root = await server.root(file)
|
|
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(root)
|
|
.then((h) => {
|
|
if (h === undefined) {
|
|
s.broken.add(root + server.id)
|
|
}
|
|
return h
|
|
})
|
|
.catch((err) => {
|
|
s.broken.add(root + server.id)
|
|
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
|
|
return undefined
|
|
})
|
|
if (!handle) continue
|
|
log.info("spawned lsp server", { serverID: server.id })
|
|
|
|
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,
|
|
})
|
|
return undefined
|
|
})
|
|
if (!client) continue
|
|
s.clients.push(client)
|
|
result.push(client)
|
|
Bus.publish(Event.Updated, {})
|
|
}
|
|
return result
|
|
}
|
|
|
|
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
|
const clients = await getClients(input)
|
|
await run(async (client) => {
|
|
if (!clients.includes(client)) return
|
|
|
|
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
|
|
await client.notify.open({ path: input })
|
|
return wait
|
|
}).catch((err) => {
|
|
log.error("failed to touch file", { err, file: input })
|
|
})
|
|
}
|
|
|
|
export async function diagnostics() {
|
|
const results: Record<string, LSPClient.Diagnostic[]> = {}
|
|
for (const result of await run(async (client) => client.diagnostics)) {
|
|
for (const [path, diagnostics] of result.entries()) {
|
|
const arr = results[path] || []
|
|
arr.push(...diagnostics)
|
|
results[path] = arr
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
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,
|
|
},
|
|
})
|
|
})
|
|
}
|
|
|
|
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,
|
|
]
|
|
|
|
export async function workspaceSymbol(query: string) {
|
|
return run((client) =>
|
|
client.connection
|
|
.sendRequest("workspace/symbol", {
|
|
query,
|
|
})
|
|
.then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
|
|
.then((result: any) => result.slice(0, 10))
|
|
.catch(() => []),
|
|
).then((result) => result.flat() as LSP.Symbol[])
|
|
}
|
|
|
|
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[]> {
|
|
const clients = await state().then((x) => x.clients)
|
|
const tasks = clients.map((x) => input(x))
|
|
return Promise.all(tasks)
|
|
}
|
|
|
|
export namespace Diagnostic {
|
|
export function pretty(diagnostic: LSPClient.Diagnostic) {
|
|
const severityMap = {
|
|
1: "ERROR",
|
|
2: "WARN",
|
|
3: "INFO",
|
|
4: "HINT",
|
|
}
|
|
|
|
const severity = severityMap[diagnostic.severity || 1]
|
|
const line = diagnostic.range.start.line + 1
|
|
const col = diagnostic.range.start.character + 1
|
|
|
|
return `${severity} [${line}:${col}] ${diagnostic.message}`
|
|
}
|
|
}
|
|
}
|