client.ts 6.1 KB

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