| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- 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> = {}
- const cfg = await Config.get()
- if (cfg.lsp === false) {
- log.info("all LSPs are disabled")
- return {
- broken: new Set<string>(),
- servers,
- clients,
- spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
- }
- }
- for (const server of Object.values(LSPServer)) {
- servers[server.id] = server
- }
- 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,
- spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
- }
- },
- 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[] = []
- async function schedule(server: LSPServer.Info, root: string, key: string) {
- const handle = await server
- .spawn(root)
- .then((value) => {
- if (!value) s.broken.add(key)
- return value
- })
- .catch((err) => {
- s.broken.add(key)
- log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
- return undefined
- })
- if (!handle) return undefined
- log.info("spawned lsp server", { serverID: server.id })
- const client = await LSPClient.create({
- serverID: server.id,
- server: handle,
- root,
- }).catch((err) => {
- s.broken.add(key)
- handle.process.kill()
- log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
- return undefined
- })
- if (!client) {
- handle.process.kill()
- return undefined
- }
- const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
- if (existing) {
- handle.process.kill()
- return existing
- }
- s.clients.push(client)
- return client
- }
- 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 inflight = s.spawning.get(root + server.id)
- if (inflight) {
- const client = await inflight
- if (!client) continue
- result.push(client)
- continue
- }
- const task = schedule(server, root, root + server.id)
- s.spawning.set(root + server.id, task)
- task.finally(() => {
- if (s.spawning.get(root + server.id) === task) {
- s.spawning.delete(root + server.id)
- }
- })
- const client = await task
- if (!client) continue
- result.push(client)
- Bus.publish(Event.Updated, {})
- }
- return result
- }
- export async function touchFile(input: string, waitForDiagnostics?: boolean) {
- log.info("touching file", { file: input })
- 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}`
- }
- }
- }
|