index.ts 7.5 KB

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