client.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. import path from "path"
  2. import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
  3. import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
  4. import { Log } from "../util/log"
  5. import { LANGUAGE_EXTENSIONS } from "./language"
  6. import { Bus } from "../bus"
  7. import z from "zod"
  8. import type { LSPServer } from "./server"
  9. import { NamedError } from "../util/error"
  10. import { withTimeout } from "../util/timeout"
  11. import { Instance } from "../project/instance"
  12. export namespace LSPClient {
  13. const log = Log.create({ service: "lsp.client" })
  14. export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
  15. export type Diagnostic = VSCodeDiagnostic
  16. export const InitializeError = NamedError.create(
  17. "LSPInitializeError",
  18. z.object({
  19. serverID: z.string(),
  20. }),
  21. )
  22. export const Event = {
  23. Diagnostics: Bus.event(
  24. "lsp.client.diagnostics",
  25. z.object({
  26. serverID: z.string(),
  27. path: z.string(),
  28. }),
  29. ),
  30. }
  31. export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
  32. const l = log.clone().tag("serverID", input.serverID)
  33. l.info("starting client")
  34. const connection = createMessageConnection(
  35. new StreamMessageReader(input.server.process.stdout as any),
  36. new StreamMessageWriter(input.server.process.stdin as any),
  37. )
  38. const diagnostics = new Map<string, Diagnostic[]>()
  39. connection.onNotification("textDocument/publishDiagnostics", (params) => {
  40. const path = new URL(params.uri).pathname
  41. l.info("textDocument/publishDiagnostics", {
  42. path,
  43. })
  44. const exists = diagnostics.has(path)
  45. diagnostics.set(path, params.diagnostics)
  46. if (!exists && input.serverID === "typescript") return
  47. Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
  48. })
  49. connection.onRequest("window/workDoneProgress/create", (params) => {
  50. l.info("window/workDoneProgress/create", params)
  51. return null
  52. })
  53. connection.onRequest("workspace/configuration", async () => {
  54. // Return server initialization options
  55. return [input.server.initialization ?? {}]
  56. })
  57. connection.listen()
  58. l.info("sending initialize")
  59. await withTimeout(
  60. connection.sendRequest("initialize", {
  61. rootUri: "file://" + input.root,
  62. processId: input.server.process.pid,
  63. workspaceFolders: [
  64. {
  65. name: "workspace",
  66. uri: "file://" + input.root,
  67. },
  68. ],
  69. initializationOptions: {
  70. ...input.server.initialization,
  71. },
  72. capabilities: {
  73. window: {
  74. workDoneProgress: true,
  75. },
  76. workspace: {
  77. configuration: true,
  78. },
  79. textDocument: {
  80. synchronization: {
  81. didOpen: true,
  82. didChange: true,
  83. },
  84. publishDiagnostics: {
  85. versionSupport: true,
  86. },
  87. },
  88. },
  89. }),
  90. 5_000,
  91. ).catch((err) => {
  92. l.error("initialize error", { error: err })
  93. throw new InitializeError(
  94. { serverID: input.serverID },
  95. {
  96. cause: err,
  97. },
  98. )
  99. })
  100. await connection.sendNotification("initialized", {})
  101. if (input.server.initialization) {
  102. await connection.sendNotification("workspace/didChangeConfiguration", {
  103. settings: input.server.initialization,
  104. })
  105. }
  106. const files: {
  107. [path: string]: number
  108. } = {}
  109. const result = {
  110. root: input.root,
  111. get serverID() {
  112. return input.serverID
  113. },
  114. get connection() {
  115. return connection
  116. },
  117. notify: {
  118. async open(input: { path: string }) {
  119. input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
  120. const file = Bun.file(input.path)
  121. const text = await file.text()
  122. const extension = path.extname(input.path)
  123. const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
  124. const version = files[input.path]
  125. if (version !== undefined) {
  126. const next = version + 1
  127. files[input.path] = next
  128. log.info("textDocument/didChange", {
  129. path: input.path,
  130. version: next,
  131. })
  132. await connection.sendNotification("textDocument/didChange", {
  133. textDocument: {
  134. uri: `file://` + input.path,
  135. version: next,
  136. },
  137. contentChanges: [{ text }],
  138. })
  139. return
  140. }
  141. log.info("textDocument/didOpen", input)
  142. diagnostics.delete(input.path)
  143. await connection.sendNotification("textDocument/didOpen", {
  144. textDocument: {
  145. uri: `file://` + input.path,
  146. languageId,
  147. version: 0,
  148. text,
  149. },
  150. })
  151. files[input.path] = 0
  152. return
  153. },
  154. },
  155. get diagnostics() {
  156. return diagnostics
  157. },
  158. async waitForDiagnostics(input: { path: string }) {
  159. input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
  160. log.info("waiting for diagnostics", input)
  161. let unsub: () => void
  162. return await withTimeout(
  163. new Promise<void>((resolve) => {
  164. unsub = Bus.subscribe(Event.Diagnostics, (event) => {
  165. if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
  166. log.info("got diagnostics", input)
  167. unsub?.()
  168. resolve()
  169. }
  170. })
  171. }),
  172. 3000,
  173. )
  174. .catch(() => {})
  175. .finally(() => {
  176. unsub?.()
  177. })
  178. },
  179. async shutdown() {
  180. l.info("shutting down")
  181. connection.end()
  182. connection.dispose()
  183. input.server.process.kill()
  184. l.info("shutdown")
  185. },
  186. }
  187. l.info("initialized")
  188. return result
  189. }
  190. }