index.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. import { Log } from "../util/log"
  2. import { LSPClient } from "./client"
  3. import path from "path"
  4. import { LSPServer } from "./server"
  5. import z from "zod"
  6. import { Config } from "../config/config"
  7. import { spawn } from "child_process"
  8. import { Instance } from "../project/instance"
  9. import { Bus } from "../bus"
  10. export namespace LSP {
  11. const log = Log.create({ service: "lsp" })
  12. export const Event = {
  13. Updated: Bus.event("lsp.updated", z.object({})),
  14. }
  15. export const Range = z
  16. .object({
  17. start: z.object({
  18. line: z.number(),
  19. character: z.number(),
  20. }),
  21. end: z.object({
  22. line: z.number(),
  23. character: z.number(),
  24. }),
  25. })
  26. .meta({
  27. ref: "Range",
  28. })
  29. export type Range = z.infer<typeof Range>
  30. export const Symbol = z
  31. .object({
  32. name: z.string(),
  33. kind: z.number(),
  34. location: z.object({
  35. uri: z.string(),
  36. range: Range,
  37. }),
  38. })
  39. .meta({
  40. ref: "Symbol",
  41. })
  42. export type Symbol = z.infer<typeof Symbol>
  43. export const DocumentSymbol = z
  44. .object({
  45. name: z.string(),
  46. detail: z.string().optional(),
  47. kind: z.number(),
  48. range: Range,
  49. selectionRange: Range,
  50. })
  51. .meta({
  52. ref: "DocumentSymbol",
  53. })
  54. export type DocumentSymbol = z.infer<typeof DocumentSymbol>
  55. const state = Instance.state(
  56. async () => {
  57. const clients: LSPClient.Info[] = []
  58. const servers: Record<string, LSPServer.Info> = {}
  59. const cfg = await Config.get()
  60. if (cfg.lsp === false) {
  61. log.info("all LSPs are disabled")
  62. return {
  63. broken: new Set<string>(),
  64. servers,
  65. clients,
  66. spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
  67. }
  68. }
  69. for (const server of Object.values(LSPServer)) {
  70. servers[server.id] = server
  71. }
  72. for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
  73. const existing = servers[name]
  74. if (item.disabled) {
  75. log.info(`LSP server ${name} is disabled`)
  76. delete servers[name]
  77. continue
  78. }
  79. servers[name] = {
  80. ...existing,
  81. id: name,
  82. root: existing?.root ?? (async () => Instance.directory),
  83. extensions: item.extensions ?? existing?.extensions ?? [],
  84. spawn: async (root) => {
  85. return {
  86. process: spawn(item.command[0], item.command.slice(1), {
  87. cwd: root,
  88. env: {
  89. ...process.env,
  90. ...item.env,
  91. },
  92. }),
  93. initialization: item.initialization,
  94. }
  95. },
  96. }
  97. }
  98. log.info("enabled LSP servers", {
  99. serverIds: Object.values(servers)
  100. .map((server) => server.id)
  101. .join(", "),
  102. })
  103. return {
  104. broken: new Set<string>(),
  105. servers,
  106. clients,
  107. spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
  108. }
  109. },
  110. async (state) => {
  111. await Promise.all(state.clients.map((client) => client.shutdown()))
  112. },
  113. )
  114. export async function init() {
  115. return state()
  116. }
  117. export const Status = z
  118. .object({
  119. id: z.string(),
  120. name: z.string(),
  121. root: z.string(),
  122. status: z.union([z.literal("connected"), z.literal("error")]),
  123. })
  124. .meta({
  125. ref: "LSPStatus",
  126. })
  127. export type Status = z.infer<typeof Status>
  128. export async function status() {
  129. return state().then((x) => {
  130. const result: Status[] = []
  131. for (const client of x.clients) {
  132. result.push({
  133. id: client.serverID,
  134. name: x.servers[client.serverID].id,
  135. root: path.relative(Instance.directory, client.root),
  136. status: "connected",
  137. })
  138. }
  139. return result
  140. })
  141. }
  142. async function getClients(file: string) {
  143. const s = await state()
  144. const extension = path.parse(file).ext || file
  145. const result: LSPClient.Info[] = []
  146. async function schedule(server: LSPServer.Info, root: string, key: string) {
  147. const handle = await server
  148. .spawn(root)
  149. .then((value) => {
  150. if (!value) s.broken.add(key)
  151. return value
  152. })
  153. .catch((err) => {
  154. s.broken.add(key)
  155. log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
  156. return undefined
  157. })
  158. if (!handle) return undefined
  159. log.info("spawned lsp server", { serverID: server.id })
  160. const client = await LSPClient.create({
  161. serverID: server.id,
  162. server: handle,
  163. root,
  164. }).catch((err) => {
  165. s.broken.add(key)
  166. handle.process.kill()
  167. log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
  168. return undefined
  169. })
  170. if (!client) {
  171. handle.process.kill()
  172. return undefined
  173. }
  174. const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
  175. if (existing) {
  176. handle.process.kill()
  177. return existing
  178. }
  179. s.clients.push(client)
  180. return client
  181. }
  182. for (const server of Object.values(s.servers)) {
  183. if (server.extensions.length && !server.extensions.includes(extension)) continue
  184. const root = await server.root(file)
  185. if (!root) continue
  186. if (s.broken.has(root + server.id)) continue
  187. const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
  188. if (match) {
  189. result.push(match)
  190. continue
  191. }
  192. const inflight = s.spawning.get(root + server.id)
  193. if (inflight) {
  194. const client = await inflight
  195. if (!client) continue
  196. result.push(client)
  197. continue
  198. }
  199. const task = schedule(server, root, root + server.id)
  200. s.spawning.set(root + server.id, task)
  201. task.finally(() => {
  202. if (s.spawning.get(root + server.id) === task) {
  203. s.spawning.delete(root + server.id)
  204. }
  205. })
  206. const client = await task
  207. if (!client) continue
  208. result.push(client)
  209. Bus.publish(Event.Updated, {})
  210. }
  211. return result
  212. }
  213. export async function touchFile(input: string, waitForDiagnostics?: boolean) {
  214. log.info("touching file", { file: input })
  215. const clients = await getClients(input)
  216. await run(async (client) => {
  217. if (!clients.includes(client)) return
  218. const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
  219. await client.notify.open({ path: input })
  220. return wait
  221. }).catch((err) => {
  222. log.error("failed to touch file", { err, file: input })
  223. })
  224. }
  225. export async function diagnostics() {
  226. const results: Record<string, LSPClient.Diagnostic[]> = {}
  227. for (const result of await run(async (client) => client.diagnostics)) {
  228. for (const [path, diagnostics] of result.entries()) {
  229. const arr = results[path] || []
  230. arr.push(...diagnostics)
  231. results[path] = arr
  232. }
  233. }
  234. return results
  235. }
  236. export async function hover(input: { file: string; line: number; character: number }) {
  237. return run((client) => {
  238. return client.connection.sendRequest("textDocument/hover", {
  239. textDocument: {
  240. uri: `file://${input.file}`,
  241. },
  242. position: {
  243. line: input.line,
  244. character: input.character,
  245. },
  246. })
  247. })
  248. }
  249. enum SymbolKind {
  250. File = 1,
  251. Module = 2,
  252. Namespace = 3,
  253. Package = 4,
  254. Class = 5,
  255. Method = 6,
  256. Property = 7,
  257. Field = 8,
  258. Constructor = 9,
  259. Enum = 10,
  260. Interface = 11,
  261. Function = 12,
  262. Variable = 13,
  263. Constant = 14,
  264. String = 15,
  265. Number = 16,
  266. Boolean = 17,
  267. Array = 18,
  268. Object = 19,
  269. Key = 20,
  270. Null = 21,
  271. EnumMember = 22,
  272. Struct = 23,
  273. Event = 24,
  274. Operator = 25,
  275. TypeParameter = 26,
  276. }
  277. const kinds = [
  278. SymbolKind.Class,
  279. SymbolKind.Function,
  280. SymbolKind.Method,
  281. SymbolKind.Interface,
  282. SymbolKind.Variable,
  283. SymbolKind.Constant,
  284. SymbolKind.Struct,
  285. SymbolKind.Enum,
  286. ]
  287. export async function workspaceSymbol(query: string) {
  288. return run((client) =>
  289. client.connection
  290. .sendRequest("workspace/symbol", {
  291. query,
  292. })
  293. .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
  294. .then((result: any) => result.slice(0, 10))
  295. .catch(() => []),
  296. ).then((result) => result.flat() as LSP.Symbol[])
  297. }
  298. export async function documentSymbol(uri: string) {
  299. return run((client) =>
  300. client.connection
  301. .sendRequest("textDocument/documentSymbol", {
  302. textDocument: {
  303. uri,
  304. },
  305. })
  306. .catch(() => []),
  307. )
  308. .then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
  309. .then((result) => result.filter(Boolean))
  310. }
  311. async function run<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
  312. const clients = await state().then((x) => x.clients)
  313. const tasks = clients.map((x) => input(x))
  314. return Promise.all(tasks)
  315. }
  316. export namespace Diagnostic {
  317. export function pretty(diagnostic: LSPClient.Diagnostic) {
  318. const severityMap = {
  319. 1: "ERROR",
  320. 2: "WARN",
  321. 3: "INFO",
  322. 4: "HINT",
  323. }
  324. const severity = severityMap[diagnostic.severity || 1]
  325. const line = diagnostic.range.start.line + 1
  326. const col = diagnostic.range.start.character + 1
  327. return `${severity} [${line}:${col}] ${diagnostic.message}`
  328. }
  329. }
  330. }