lsp.test.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { afterEach, describe, expect } from "bun:test"
  2. import { Effect, Layer } from "effect"
  3. import path from "path"
  4. import { Agent } from "../../src/agent/agent"
  5. import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
  6. import { AppFileSystem } from "@opencode-ai/core/filesystem"
  7. import { LSP } from "@/lsp/lsp"
  8. import { Permission } from "../../src/permission"
  9. import { MessageID, SessionID } from "../../src/session/schema"
  10. import { Tool } from "@/tool/tool"
  11. import { Truncate } from "@/tool/truncate"
  12. import { LspTool } from "../../src/tool/lsp"
  13. import { disposeAllInstances, provideTmpdirInstance } from "../fixture/fixture"
  14. import { testEffect } from "../lib/effect"
  15. afterEach(async () => {
  16. await disposeAllInstances()
  17. })
  18. const ctx = {
  19. sessionID: SessionID.make("ses_test"),
  20. messageID: MessageID.make("msg_test"),
  21. callID: "",
  22. agent: "build",
  23. abort: AbortSignal.any([]),
  24. messages: [],
  25. metadata: () => Effect.void,
  26. ask: () => Effect.void,
  27. }
  28. const workspaceSymbolQueries: string[] = []
  29. const lsp = Layer.succeed(
  30. LSP.Service,
  31. LSP.Service.of({
  32. init: () => Effect.void,
  33. status: () => Effect.succeed([]),
  34. hasClients: () => Effect.succeed(true),
  35. touchFile: () => Effect.void,
  36. diagnostics: () => Effect.succeed({}),
  37. hover: () => Effect.succeed([]),
  38. definition: () => Effect.succeed([]),
  39. references: () => Effect.succeed([]),
  40. implementation: () => Effect.succeed([]),
  41. documentSymbol: () => Effect.succeed([]),
  42. workspaceSymbol: (query) =>
  43. Effect.sync(() => {
  44. workspaceSymbolQueries.push(query)
  45. return []
  46. }),
  47. prepareCallHierarchy: () => Effect.succeed([]),
  48. incomingCalls: () => Effect.succeed([]),
  49. outgoingCalls: () => Effect.succeed([]),
  50. }),
  51. )
  52. const it = testEffect(
  53. Layer.mergeAll(
  54. Agent.defaultLayer,
  55. AppFileSystem.defaultLayer,
  56. CrossSpawnSpawner.defaultLayer,
  57. Truncate.defaultLayer,
  58. lsp,
  59. ),
  60. )
  61. const init = Effect.fn("LspToolTest.init")(function* () {
  62. const info = yield* LspTool
  63. return yield* info.init()
  64. })
  65. const run = Effect.fn("LspToolTest.run")(function* (
  66. args: Tool.InferParameters<typeof LspTool>,
  67. next: Tool.Context = ctx,
  68. ) {
  69. const tool = yield* init()
  70. return yield* tool.execute(args, next)
  71. })
  72. const put = Effect.fn("LspToolTest.put")(function* (file: string) {
  73. const fs = yield* AppFileSystem.Service
  74. yield* fs.writeWithDirs(file, "export const x = 1\n")
  75. })
  76. const asks = () => {
  77. const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
  78. return {
  79. items,
  80. next: {
  81. ...ctx,
  82. ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
  83. Effect.sync(() => {
  84. items.push(req)
  85. }),
  86. },
  87. }
  88. }
  89. describe("tool.lsp", () => {
  90. describe("permission metadata", () => {
  91. it.live("keeps cursor details for position-based operations", () =>
  92. provideTmpdirInstance(
  93. (dir) =>
  94. Effect.gen(function* () {
  95. const file = path.join(dir, "test.ts")
  96. yield* put(file)
  97. const { items, next } = asks()
  98. const result = yield* run({ operation: "goToDefinition", filePath: file, line: 3, character: 7 }, next)
  99. const req = items.find((item) => item.permission === "lsp")
  100. expect(req).toBeDefined()
  101. expect(req!.metadata).toEqual({
  102. operation: "goToDefinition",
  103. filePath: file,
  104. line: 3,
  105. character: 7,
  106. })
  107. expect(result.title).toBe("goToDefinition test.ts:3:7")
  108. }),
  109. { git: true },
  110. ),
  111. )
  112. it.live("omits cursor details for documentSymbol", () =>
  113. provideTmpdirInstance(
  114. (dir) =>
  115. Effect.gen(function* () {
  116. const file = path.join(dir, "test.ts")
  117. yield* put(file)
  118. const { items, next } = asks()
  119. const result = yield* run({ operation: "documentSymbol", filePath: file, line: 3, character: 7 }, next)
  120. const req = items.find((item) => item.permission === "lsp")
  121. expect(req).toBeDefined()
  122. expect(req!.metadata).toEqual({
  123. operation: "documentSymbol",
  124. filePath: file,
  125. })
  126. expect(result.title).toBe("documentSymbol test.ts")
  127. }),
  128. { git: true },
  129. ),
  130. )
  131. it.live("omits file and cursor details for workspaceSymbol", () =>
  132. provideTmpdirInstance(
  133. (dir) =>
  134. Effect.gen(function* () {
  135. workspaceSymbolQueries.length = 0
  136. const file = path.join(dir, "test.ts")
  137. yield* put(file)
  138. const { items, next } = asks()
  139. const result = yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 }, next)
  140. const req = items.find((item) => item.permission === "lsp")
  141. expect(req).toBeDefined()
  142. expect(req!.metadata).toEqual({
  143. operation: "workspaceSymbol",
  144. })
  145. expect(result.title).toBe("workspaceSymbol")
  146. }),
  147. { git: true },
  148. ),
  149. )
  150. it.live("passes workspaceSymbol query to LSP", () =>
  151. provideTmpdirInstance(
  152. (dir) =>
  153. Effect.gen(function* () {
  154. workspaceSymbolQueries.length = 0
  155. const file = path.join(dir, "test.ts")
  156. yield* put(file)
  157. yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7, query: "TestSymbol" })
  158. yield* run({ operation: "workspaceSymbol", filePath: file, line: 3, character: 7 })
  159. expect(workspaceSymbolQueries).toEqual(["TestSymbol", ""])
  160. }),
  161. { git: true },
  162. ),
  163. )
  164. })
  165. })