index.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import { BusEvent } from "@/bus/bus-event"
  2. import { Bus } from "@/bus"
  3. import { Log } from "../util/log"
  4. import { LSPClient } from "./client"
  5. import path from "path"
  6. import { pathToFileURL } from "url"
  7. import { LSPServer } from "./server"
  8. import z from "zod"
  9. import { Config } from "../config/config"
  10. import { spawn } from "child_process"
  11. import { Instance } from "../project/instance"
  12. import { Flag } from "@/flag/flag"
  13. export namespace LSP {
  14. const log = Log.create({ service: "lsp" })
  15. export const Event = {
  16. Updated: BusEvent.define("lsp.updated", z.object({})),
  17. }
  18. export const Range = z
  19. .object({
  20. start: z.object({
  21. line: z.number(),
  22. character: z.number(),
  23. }),
  24. end: z.object({
  25. line: z.number(),
  26. character: z.number(),
  27. }),
  28. })
  29. .meta({
  30. ref: "Range",
  31. })
  32. export type Range = z.infer<typeof Range>
  33. export const Symbol = z
  34. .object({
  35. name: z.string(),
  36. kind: z.number(),
  37. location: z.object({
  38. uri: z.string(),
  39. range: Range,
  40. }),
  41. })
  42. .meta({
  43. ref: "Symbol",
  44. })
  45. export type Symbol = z.infer<typeof Symbol>
  46. export const DocumentSymbol = z
  47. .object({
  48. name: z.string(),
  49. detail: z.string().optional(),
  50. kind: z.number(),
  51. range: Range,
  52. selectionRange: Range,
  53. })
  54. .meta({
  55. ref: "DocumentSymbol",
  56. })
  57. export type DocumentSymbol = z.infer<typeof DocumentSymbol>
  58. const filterExperimentalServers = (servers: Record<string, LSPServer.Info>) => {
  59. if (Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
  60. // If experimental flag is enabled, disable pyright
  61. if (servers["pyright"]) {
  62. log.info("LSP server pyright is disabled because OPENCODE_EXPERIMENTAL_LSP_TY is enabled")
  63. delete servers["pyright"]
  64. }
  65. } else {
  66. // If experimental flag is disabled, disable ty
  67. if (servers["ty"]) {
  68. delete servers["ty"]
  69. }
  70. }
  71. }
  72. const state = Instance.state(
  73. async () => {
  74. const clients: LSPClient.Info[] = []
  75. const servers: Record<string, LSPServer.Info> = {}
  76. const cfg = await Config.get()
  77. if (cfg.lsp === false) {
  78. log.info("all LSPs are disabled")
  79. return {
  80. broken: new Set<string>(),
  81. servers,
  82. clients,
  83. spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
  84. }
  85. }
  86. for (const server of Object.values(LSPServer)) {
  87. servers[server.id] = server
  88. }
  89. filterExperimentalServers(servers)
  90. for (const [name, item] of Object.entries(cfg.lsp ?? {})) {
  91. const existing = servers[name]
  92. if (item.disabled) {
  93. log.info(`LSP server ${name} is disabled`)
  94. delete servers[name]
  95. continue
  96. }
  97. servers[name] = {
  98. ...existing,
  99. id: name,
  100. root: existing?.root ?? (async () => Instance.directory),
  101. extensions: item.extensions ?? existing?.extensions ?? [],
  102. spawn: async (root) => {
  103. return {
  104. process: spawn(item.command[0], item.command.slice(1), {
  105. cwd: root,
  106. env: {
  107. ...process.env,
  108. ...item.env,
  109. },
  110. }),
  111. initialization: item.initialization,
  112. }
  113. },
  114. }
  115. }
  116. log.info("enabled LSP servers", {
  117. serverIds: Object.values(servers)
  118. .map((server) => server.id)
  119. .join(", "),
  120. })
  121. return {
  122. broken: new Set<string>(),
  123. servers,
  124. clients,
  125. spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
  126. }
  127. },
  128. async (state) => {
  129. await Promise.all(state.clients.map((client) => client.shutdown()))
  130. },
  131. )
  132. export async function init() {
  133. return state()
  134. }
  135. export const Status = z
  136. .object({
  137. id: z.string(),
  138. name: z.string(),
  139. root: z.string(),
  140. status: z.union([z.literal("connected"), z.literal("error")]),
  141. })
  142. .meta({
  143. ref: "LSPStatus",
  144. })
  145. export type Status = z.infer<typeof Status>
  146. export async function status() {
  147. return state().then((x) => {
  148. const result: Status[] = []
  149. for (const client of x.clients) {
  150. result.push({
  151. id: client.serverID,
  152. name: x.servers[client.serverID].id,
  153. root: path.relative(Instance.directory, client.root),
  154. status: "connected",
  155. })
  156. }
  157. return result
  158. })
  159. }
  160. async function getClients(file: string) {
  161. const s = await state()
  162. const extension = path.parse(file).ext || file
  163. const result: LSPClient.Info[] = []
  164. async function schedule(server: LSPServer.Info, root: string, key: string) {
  165. const handle = await server
  166. .spawn(root)
  167. .then((value) => {
  168. if (!value) s.broken.add(key)
  169. return value
  170. })
  171. .catch((err) => {
  172. s.broken.add(key)
  173. log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
  174. return undefined
  175. })
  176. if (!handle) return undefined
  177. log.info("spawned lsp server", { serverID: server.id })
  178. const client = await LSPClient.create({
  179. serverID: server.id,
  180. server: handle,
  181. root,
  182. }).catch((err) => {
  183. s.broken.add(key)
  184. handle.process.kill()
  185. log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
  186. return undefined
  187. })
  188. if (!client) {
  189. handle.process.kill()
  190. return undefined
  191. }
  192. const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
  193. if (existing) {
  194. handle.process.kill()
  195. return existing
  196. }
  197. s.clients.push(client)
  198. return client
  199. }
  200. for (const server of Object.values(s.servers)) {
  201. if (server.extensions.length && !server.extensions.includes(extension)) continue
  202. const root = await server.root(file)
  203. if (!root) continue
  204. if (s.broken.has(root + server.id)) continue
  205. const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
  206. if (match) {
  207. result.push(match)
  208. continue
  209. }
  210. const inflight = s.spawning.get(root + server.id)
  211. if (inflight) {
  212. const client = await inflight
  213. if (!client) continue
  214. result.push(client)
  215. continue
  216. }
  217. const task = schedule(server, root, root + server.id)
  218. s.spawning.set(root + server.id, task)
  219. task.finally(() => {
  220. if (s.spawning.get(root + server.id) === task) {
  221. s.spawning.delete(root + server.id)
  222. }
  223. })
  224. const client = await task
  225. if (!client) continue
  226. result.push(client)
  227. Bus.publish(Event.Updated, {})
  228. }
  229. return result
  230. }
  231. export async function hasClients(file: string) {
  232. const s = await state()
  233. const extension = path.parse(file).ext || file
  234. for (const server of Object.values(s.servers)) {
  235. if (server.extensions.length && !server.extensions.includes(extension)) continue
  236. const root = await server.root(file)
  237. if (!root) continue
  238. if (s.broken.has(root + server.id)) continue
  239. return true
  240. }
  241. return false
  242. }
  243. export async function touchFile(input: string, waitForDiagnostics?: boolean) {
  244. log.info("touching file", { file: input })
  245. const clients = await getClients(input)
  246. await Promise.all(
  247. clients.map(async (client) => {
  248. const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
  249. await client.notify.open({ path: input })
  250. return wait
  251. }),
  252. ).catch((err) => {
  253. log.error("failed to touch file", { err, file: input })
  254. })
  255. }
  256. export async function diagnostics() {
  257. const results: Record<string, LSPClient.Diagnostic[]> = {}
  258. for (const result of await runAll(async (client) => client.diagnostics)) {
  259. for (const [path, diagnostics] of result.entries()) {
  260. const arr = results[path] || []
  261. arr.push(...diagnostics)
  262. results[path] = arr
  263. }
  264. }
  265. return results
  266. }
  267. export async function hover(input: { file: string; line: number; character: number }) {
  268. return run(input.file, (client) => {
  269. return client.connection
  270. .sendRequest("textDocument/hover", {
  271. textDocument: {
  272. uri: pathToFileURL(input.file).href,
  273. },
  274. position: {
  275. line: input.line,
  276. character: input.character,
  277. },
  278. })
  279. .catch(() => null)
  280. })
  281. }
  282. enum SymbolKind {
  283. File = 1,
  284. Module = 2,
  285. Namespace = 3,
  286. Package = 4,
  287. Class = 5,
  288. Method = 6,
  289. Property = 7,
  290. Field = 8,
  291. Constructor = 9,
  292. Enum = 10,
  293. Interface = 11,
  294. Function = 12,
  295. Variable = 13,
  296. Constant = 14,
  297. String = 15,
  298. Number = 16,
  299. Boolean = 17,
  300. Array = 18,
  301. Object = 19,
  302. Key = 20,
  303. Null = 21,
  304. EnumMember = 22,
  305. Struct = 23,
  306. Event = 24,
  307. Operator = 25,
  308. TypeParameter = 26,
  309. }
  310. const kinds = [
  311. SymbolKind.Class,
  312. SymbolKind.Function,
  313. SymbolKind.Method,
  314. SymbolKind.Interface,
  315. SymbolKind.Variable,
  316. SymbolKind.Constant,
  317. SymbolKind.Struct,
  318. SymbolKind.Enum,
  319. ]
  320. export async function workspaceSymbol(query: string) {
  321. return runAll((client) =>
  322. client.connection
  323. .sendRequest("workspace/symbol", {
  324. query,
  325. })
  326. .then((result: any) => result.filter((x: LSP.Symbol) => kinds.includes(x.kind)))
  327. .then((result: any) => result.slice(0, 10))
  328. .catch(() => []),
  329. ).then((result) => result.flat() as LSP.Symbol[])
  330. }
  331. export async function documentSymbol(uri: string) {
  332. const file = new URL(uri).pathname
  333. return run(file, (client) =>
  334. client.connection
  335. .sendRequest("textDocument/documentSymbol", {
  336. textDocument: {
  337. uri,
  338. },
  339. })
  340. .catch(() => []),
  341. )
  342. .then((result) => result.flat() as (LSP.DocumentSymbol | LSP.Symbol)[])
  343. .then((result) => result.filter(Boolean))
  344. }
  345. export async function definition(input: { file: string; line: number; character: number }) {
  346. return run(input.file, (client) =>
  347. client.connection
  348. .sendRequest("textDocument/definition", {
  349. textDocument: { uri: pathToFileURL(input.file).href },
  350. position: { line: input.line, character: input.character },
  351. })
  352. .catch(() => null),
  353. ).then((result) => result.flat().filter(Boolean))
  354. }
  355. export async function references(input: { file: string; line: number; character: number }) {
  356. return run(input.file, (client) =>
  357. client.connection
  358. .sendRequest("textDocument/references", {
  359. textDocument: { uri: pathToFileURL(input.file).href },
  360. position: { line: input.line, character: input.character },
  361. context: { includeDeclaration: true },
  362. })
  363. .catch(() => []),
  364. ).then((result) => result.flat().filter(Boolean))
  365. }
  366. export async function implementation(input: { file: string; line: number; character: number }) {
  367. return run(input.file, (client) =>
  368. client.connection
  369. .sendRequest("textDocument/implementation", {
  370. textDocument: { uri: pathToFileURL(input.file).href },
  371. position: { line: input.line, character: input.character },
  372. })
  373. .catch(() => null),
  374. ).then((result) => result.flat().filter(Boolean))
  375. }
  376. export async function prepareCallHierarchy(input: { file: string; line: number; character: number }) {
  377. return run(input.file, (client) =>
  378. client.connection
  379. .sendRequest("textDocument/prepareCallHierarchy", {
  380. textDocument: { uri: pathToFileURL(input.file).href },
  381. position: { line: input.line, character: input.character },
  382. })
  383. .catch(() => []),
  384. ).then((result) => result.flat().filter(Boolean))
  385. }
  386. export async function incomingCalls(input: { file: string; line: number; character: number }) {
  387. return run(input.file, async (client) => {
  388. const items = (await client.connection
  389. .sendRequest("textDocument/prepareCallHierarchy", {
  390. textDocument: { uri: pathToFileURL(input.file).href },
  391. position: { line: input.line, character: input.character },
  392. })
  393. .catch(() => [])) as any[]
  394. if (!items?.length) return []
  395. return client.connection.sendRequest("callHierarchy/incomingCalls", { item: items[0] }).catch(() => [])
  396. }).then((result) => result.flat().filter(Boolean))
  397. }
  398. export async function outgoingCalls(input: { file: string; line: number; character: number }) {
  399. return run(input.file, async (client) => {
  400. const items = (await client.connection
  401. .sendRequest("textDocument/prepareCallHierarchy", {
  402. textDocument: { uri: pathToFileURL(input.file).href },
  403. position: { line: input.line, character: input.character },
  404. })
  405. .catch(() => [])) as any[]
  406. if (!items?.length) return []
  407. return client.connection.sendRequest("callHierarchy/outgoingCalls", { item: items[0] }).catch(() => [])
  408. }).then((result) => result.flat().filter(Boolean))
  409. }
  410. async function runAll<T>(input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
  411. const clients = await state().then((x) => x.clients)
  412. const tasks = clients.map((x) => input(x))
  413. return Promise.all(tasks)
  414. }
  415. async function run<T>(file: string, input: (client: LSPClient.Info) => Promise<T>): Promise<T[]> {
  416. const clients = await getClients(file)
  417. const tasks = clients.map((x) => input(x))
  418. return Promise.all(tasks)
  419. }
  420. export namespace Diagnostic {
  421. export function pretty(diagnostic: LSPClient.Diagnostic) {
  422. const severityMap = {
  423. 1: "ERROR",
  424. 2: "WARN",
  425. 3: "INFO",
  426. 4: "HINT",
  427. }
  428. const severity = severityMap[diagnostic.severity || 1]
  429. const line = diagnostic.range.start.line + 1
  430. const col = diagnostic.range.start.character + 1
  431. return `${severity} [${line}:${col}] ${diagnostic.message}`
  432. }
  433. }
  434. }