index.ts 9.2 KB

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