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-06-30 20:45:13 +00:00
|
|
|
import { Ripgrep } from "../file/ripgrep"
|
2025-07-02 15:35:25 +00:00
|
|
|
import { z } from "zod"
|
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-02 15:35:25 +00:00
|
|
|
export const Symbol = z
|
|
|
|
|
.object({
|
|
|
|
|
name: z.string(),
|
|
|
|
|
kind: z.number(),
|
|
|
|
|
location: z.object({
|
|
|
|
|
uri: z.string(),
|
|
|
|
|
range: z.object({
|
|
|
|
|
start: z.object({
|
|
|
|
|
line: z.number(),
|
|
|
|
|
character: z.number(),
|
|
|
|
|
}),
|
|
|
|
|
end: z.object({
|
|
|
|
|
line: z.number(),
|
|
|
|
|
character: z.number(),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
.openapi({
|
|
|
|
|
ref: "LSP.Symbol",
|
|
|
|
|
})
|
|
|
|
|
export type Symbol = z.infer<typeof Symbol>
|
|
|
|
|
|
2025-05-26 18:52:38 +00:00
|
|
|
const state = App.state(
|
|
|
|
|
"lsp",
|
2025-07-01 02:46:42 +00:00
|
|
|
async (app) => {
|
2025-05-31 18:41:00 +00:00
|
|
|
log.info("initializing")
|
|
|
|
|
const clients = new Map<string, LSPClient.Info>()
|
2025-07-01 02:46:42 +00:00
|
|
|
for (const server of Object.values(LSPServer)) {
|
|
|
|
|
for (const extension of server.extensions) {
|
|
|
|
|
const [file] = await Ripgrep.files({
|
|
|
|
|
cwd: app.path.cwd,
|
|
|
|
|
glob: "*" + extension,
|
|
|
|
|
})
|
|
|
|
|
if (!file) continue
|
|
|
|
|
const handle = await server.spawn(App.info())
|
|
|
|
|
if (!handle) break
|
|
|
|
|
const client = await LSPClient.create(server.id, handle).catch(
|
2025-07-04 00:16:16 +00:00
|
|
|
(err) => log.error("", { error: err }),
|
2025-07-01 02:46:42 +00:00
|
|
|
)
|
|
|
|
|
if (!client) break
|
|
|
|
|
clients.set(server.id, client)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
log.info("initialized")
|
2025-05-26 18:52:38 +00:00
|
|
|
return {
|
|
|
|
|
clients,
|
2025-05-31 18:41:00 +00:00
|
|
|
}
|
2025-05-26 18:52:38 +00:00
|
|
|
},
|
|
|
|
|
async (state) => {
|
|
|
|
|
for (const client of state.clients.values()) {
|
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-06-06 21:20:01 +00:00
|
|
|
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
|
2025-05-31 18:41:00 +00:00
|
|
|
const extension = path.parse(input).ext
|
2025-07-01 02:46:42 +00:00
|
|
|
const matches = Object.values(LSPServer)
|
|
|
|
|
.filter((x) => x.extensions.includes(extension))
|
|
|
|
|
.map((x) => x.id)
|
|
|
|
|
await run(async (client) => {
|
|
|
|
|
if (!matches.includes(client.serverID)) return
|
|
|
|
|
const wait = waitForDiagnostics
|
|
|
|
|
? client.waitForDiagnostics({ path: input })
|
|
|
|
|
: Promise.resolve()
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2025-05-27 06:17:35 +00:00
|
|
|
export async function hover(input: {
|
2025-05-31 18:41:00 +00:00
|
|
|
file: string
|
|
|
|
|
line: number
|
|
|
|
|
character: number
|
2025-05-27 06:17:35 +00:00
|
|
|
}) {
|
|
|
|
|
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-05-27 06:17:35 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-30 20:45:13 +00:00
|
|
|
export async function workspaceSymbol(query: string) {
|
|
|
|
|
return run((client) =>
|
|
|
|
|
client.connection.sendRequest("workspace/symbol", {
|
|
|
|
|
query,
|
|
|
|
|
}),
|
2025-07-02 15:35:25 +00:00
|
|
|
).then((result) => result.flat() as LSP.Symbol[])
|
2025-06-30 20:45:13 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-26 21:15:41 +00:00
|
|
|
async function run<T>(
|
2025-05-21 02:00:00 +00:00
|
|
|
input: (client: LSPClient.Info) => Promise<T>,
|
|
|
|
|
): Promise<T[]> {
|
2025-05-31 18:41:00 +00:00
|
|
|
const clients = await state().then((x) => [...x.clients.values()])
|
|
|
|
|
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
|
|
|
}
|