server.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
  2. import type { App } from "../app/app"
  3. import path from "path"
  4. import { Global } from "../global"
  5. import { Log } from "../util/log"
  6. import { BunProc } from "../bun"
  7. import { $ } from "bun"
  8. import fs from "fs/promises"
  9. import { unique } from "remeda"
  10. import { Ripgrep } from "../file/ripgrep"
  11. import type { LSPClient } from "./client"
  12. import { withTimeout } from "../util/timeout"
  13. export namespace LSPServer {
  14. const log = Log.create({ service: "lsp.server" })
  15. export interface Handle {
  16. process: ChildProcessWithoutNullStreams
  17. initialization?: Record<string, any>
  18. onInitialized?: (lsp: LSPClient.Info) => Promise<void>
  19. }
  20. type RootsFunction = (app: App.Info) => Promise<string[]>
  21. const SimpleRoots = (patterns: string[]): RootsFunction => {
  22. return async (app) => {
  23. const glob = `**/*/{${patterns.join(",")}}`
  24. const files = await Ripgrep.files({
  25. glob: [glob],
  26. cwd: app.path.root,
  27. })
  28. const dirs = files.map((file) => path.dirname(file))
  29. return unique(dirs).map((dir) => path.join(app.path.root, dir))
  30. }
  31. }
  32. export interface Info {
  33. id: string
  34. extensions: string[]
  35. global?: boolean
  36. roots: (app: App.Info) => Promise<string[]>
  37. spawn(app: App.Info, root: string): Promise<Handle | undefined>
  38. }
  39. export const Typescript: Info = {
  40. id: "typescript",
  41. roots: async (app) => [app.path.root],
  42. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
  43. async spawn(app, root) {
  44. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
  45. if (!tsserver) return
  46. const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
  47. cwd: root,
  48. env: {
  49. ...process.env,
  50. BUN_BE_BUN: "1",
  51. },
  52. })
  53. return {
  54. process: proc,
  55. initialization: {
  56. tsserver: {
  57. path: tsserver,
  58. },
  59. },
  60. // tsserver sucks and won't start processing codebase until you open a file
  61. onInitialized: async (lsp) => {
  62. const [hint] = await Ripgrep.files({
  63. cwd: lsp.root,
  64. glob: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs", "*.cjs", "*.mts", "*.cts"],
  65. limit: 1,
  66. })
  67. const wait = new Promise<void>(async (resolve) => {
  68. const notif = lsp.connection.onNotification("$/progress", (params) => {
  69. if (params.value.kind !== "end") return
  70. notif.dispose()
  71. resolve()
  72. })
  73. await lsp.notify.open({ path: path.join(lsp.root, hint) })
  74. })
  75. await withTimeout(wait, 5_000)
  76. },
  77. }
  78. },
  79. }
  80. export const Gopls: Info = {
  81. id: "golang",
  82. roots: SimpleRoots(["go.mod", "go.sum"]),
  83. extensions: [".go"],
  84. async spawn(_, root) {
  85. let bin = Bun.which("gopls", {
  86. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  87. })
  88. if (!bin) {
  89. if (!Bun.which("go")) return
  90. log.info("installing gopls")
  91. const proc = Bun.spawn({
  92. cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
  93. env: { ...process.env, GOBIN: Global.Path.bin },
  94. stdout: "pipe",
  95. stderr: "pipe",
  96. stdin: "pipe",
  97. })
  98. const exit = await proc.exited
  99. if (exit !== 0) {
  100. log.error("Failed to install gopls")
  101. return
  102. }
  103. bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
  104. log.info(`installed gopls`, {
  105. bin,
  106. })
  107. }
  108. return {
  109. process: spawn(bin!, {
  110. cwd: root,
  111. }),
  112. }
  113. },
  114. }
  115. export const RubyLsp: Info = {
  116. id: "ruby-lsp",
  117. roots: SimpleRoots(["Gemfile"]),
  118. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  119. async spawn(_, root) {
  120. let bin = Bun.which("ruby-lsp", {
  121. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  122. })
  123. if (!bin) {
  124. const ruby = Bun.which("ruby")
  125. const gem = Bun.which("gem")
  126. if (!ruby || !gem) {
  127. log.info("Ruby not found, please install Ruby first")
  128. return
  129. }
  130. log.info("installing ruby-lsp")
  131. const proc = Bun.spawn({
  132. cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
  133. stdout: "pipe",
  134. stderr: "pipe",
  135. stdin: "pipe",
  136. })
  137. const exit = await proc.exited
  138. if (exit !== 0) {
  139. log.error("Failed to install ruby-lsp")
  140. return
  141. }
  142. bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
  143. log.info(`installed ruby-lsp`, {
  144. bin,
  145. })
  146. }
  147. return {
  148. process: spawn(bin!, ["--stdio"], {
  149. cwd: root,
  150. }),
  151. }
  152. },
  153. }
  154. export const Pyright: Info = {
  155. id: "pyright",
  156. extensions: [".py", ".pyi"],
  157. roots: SimpleRoots([
  158. "pyproject.toml",
  159. "setup.py",
  160. "setup.cfg",
  161. "requirements.txt",
  162. "Pipfile",
  163. "pyrightconfig.json",
  164. ]),
  165. async spawn(_, root) {
  166. const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
  167. cwd: root,
  168. env: {
  169. ...process.env,
  170. BUN_BE_BUN: "1",
  171. },
  172. })
  173. return {
  174. process: proc,
  175. }
  176. },
  177. }
  178. export const ElixirLS: Info = {
  179. id: "elixir-ls",
  180. extensions: [".ex", ".exs"],
  181. roots: SimpleRoots(["mix.exs", "mix.lock"]),
  182. async spawn(_, root) {
  183. let binary = Bun.which("elixir-ls")
  184. if (!binary) {
  185. const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
  186. binary = path.join(
  187. Global.Path.bin,
  188. "elixir-ls-master",
  189. "release",
  190. process.platform === "win32" ? "language_server.bar" : "language_server.sh",
  191. )
  192. if (!(await Bun.file(binary).exists())) {
  193. const elixir = Bun.which("elixir")
  194. if (!elixir) {
  195. log.error("elixir is required to run elixir-ls")
  196. return
  197. }
  198. log.info("downloading elixir-ls from GitHub releases")
  199. const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
  200. if (!response.ok) return
  201. const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
  202. await Bun.file(zipPath).write(response)
  203. await $`unzip -o -q ${zipPath}`.cwd(Global.Path.bin).nothrow()
  204. await fs.rm(zipPath, {
  205. force: true,
  206. recursive: true,
  207. })
  208. await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
  209. .quiet()
  210. .cwd(path.join(Global.Path.bin, "elixir-ls-master"))
  211. .env({ MIX_ENV: "prod", ...process.env })
  212. log.info(`installed elixir-ls`, {
  213. path: elixirLsPath,
  214. })
  215. }
  216. }
  217. return {
  218. process: spawn(binary, {
  219. cwd: root,
  220. }),
  221. }
  222. },
  223. }
  224. }