Răsfoiți Sursa

refactor(lsp): remove async facade exports (#22321)

Kit Langton 5 zile în urmă
părinte
comite
21d7a85e76

+ 14 - 5
packages/opencode/src/cli/cmd/debug/lsp.ts

@@ -1,4 +1,6 @@
 import { LSP } from "../../../lsp"
+import { AppRuntime } from "../../../effect/app-runtime"
+import { Effect } from "effect"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
 import { Log } from "../../../util/log"
@@ -19,9 +21,16 @@ const DiagnosticsCommand = cmd({
   builder: (yargs) => yargs.positional("file", { type: "string", demandOption: true }),
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
-      await LSP.touchFile(args.file, true)
-      await sleep(1000)
-      process.stdout.write(JSON.stringify(await LSP.diagnostics(), null, 2) + EOL)
+      const out = await AppRuntime.runPromise(
+        LSP.Service.use((lsp) =>
+          Effect.gen(function* () {
+            yield* lsp.touchFile(args.file, true)
+            yield* Effect.sleep(1000)
+            return yield* lsp.diagnostics()
+          }),
+        ),
+      )
+      process.stdout.write(JSON.stringify(out, null, 2) + EOL)
     })
   },
 })
@@ -33,7 +42,7 @@ export const SymbolsCommand = cmd({
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
       using _ = Log.Default.time("symbols")
-      const results = await LSP.workspaceSymbol(args.query)
+      const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.workspaceSymbol(args.query)))
       process.stdout.write(JSON.stringify(results, null, 2) + EOL)
     })
   },
@@ -46,7 +55,7 @@ export const DocumentSymbolsCommand = cmd({
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
       using _ = Log.Default.time("document-symbols")
-      const results = await LSP.documentSymbol(args.uri)
+      const results = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.documentSymbol(args.uri)))
       process.stdout.write(JSON.stringify(results, null, 2) + EOL)
     })
   },

+ 0 - 32
packages/opencode/src/lsp/index.ts

@@ -13,7 +13,6 @@ import { Process } from "../util/process"
 import { spawn as lspspawn } from "./launch"
 import { Effect, Layer, Context } from "effect"
 import { InstanceState } from "@/effect/instance-state"
-import { makeRuntime } from "@/effect/run-service"
 
 export namespace LSP {
   const log = Log.create({ service: "lsp" })
@@ -508,37 +507,6 @@ export namespace LSP {
 
   export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
 
-  const { runPromise } = makeRuntime(Service, defaultLayer)
-
-  export const init = async () => runPromise((svc) => svc.init())
-
-  export const status = async () => runPromise((svc) => svc.status())
-
-  export const hasClients = async (file: string) => runPromise((svc) => svc.hasClients(file))
-
-  export const touchFile = async (input: string, waitForDiagnostics?: boolean) =>
-    runPromise((svc) => svc.touchFile(input, waitForDiagnostics))
-
-  export const diagnostics = async () => runPromise((svc) => svc.diagnostics())
-
-  export const hover = async (input: LocInput) => runPromise((svc) => svc.hover(input))
-
-  export const definition = async (input: LocInput) => runPromise((svc) => svc.definition(input))
-
-  export const references = async (input: LocInput) => runPromise((svc) => svc.references(input))
-
-  export const implementation = async (input: LocInput) => runPromise((svc) => svc.implementation(input))
-
-  export const documentSymbol = async (uri: string) => runPromise((svc) => svc.documentSymbol(uri))
-
-  export const workspaceSymbol = async (query: string) => runPromise((svc) => svc.workspaceSymbol(query))
-
-  export const prepareCallHierarchy = async (input: LocInput) => runPromise((svc) => svc.prepareCallHierarchy(input))
-
-  export const incomingCalls = async (input: LocInput) => runPromise((svc) => svc.incomingCalls(input))
-
-  export const outgoingCalls = async (input: LocInput) => runPromise((svc) => svc.outgoingCalls(input))
-
   export namespace Diagnostic {
     const MAX_PER_FILE = 20
 

+ 0 - 5
packages/opencode/src/server/instance/file.ts

@@ -105,11 +105,6 @@ export const FileRoutes = lazy(() =>
         }),
       ),
       async (c) => {
-        /*
-      const query = c.req.valid("query").query
-      const result = await LSP.workspaceSymbol(query)
-      return c.json(result)
-      */
         return c.json([])
       },
     )

+ 2 - 1
packages/opencode/src/server/instance/index.ts

@@ -256,7 +256,8 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono =>
         },
       }),
       async (c) => {
-        return c.json(await LSP.status())
+        const items = await AppRuntime.runPromise(LSP.Service.use((lsp) => lsp.status()))
+        return c.json(items)
       },
     )
     .get(

+ 47 - 47
packages/opencode/test/lsp/index.test.ts

@@ -1,55 +1,55 @@
-import { describe, expect, spyOn, test } from "bun:test"
+import { describe, expect, spyOn } from "bun:test"
 import path from "path"
-import * as Lsp from "../../src/lsp/index"
+import { Effect, Layer } from "effect"
+import { LSP } from "../../src/lsp"
 import { LSPServer } from "../../src/lsp/server"
-import { Instance } from "../../src/project/instance"
-import { tmpdir } from "../fixture/fixture"
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
 
-describe("lsp.spawn", () => {
-  test("does not spawn builtin LSP for files outside instance", async () => {
-    await using tmp = await tmpdir()
-    const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
-
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await Lsp.LSP.touchFile(path.join(tmp.path, "..", "outside.ts"))
-          await Lsp.LSP.hover({
-            file: path.join(tmp.path, "..", "hover.ts"),
-            line: 0,
-            character: 0,
-          })
-        },
-      })
+const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
 
-      expect(spy).toHaveBeenCalledTimes(0)
-    } finally {
-      spy.mockRestore()
-      await Instance.disposeAll()
-    }
-  })
+describe("lsp.spawn", () => {
+  it.live("does not spawn builtin LSP for files outside instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
 
-  test("would spawn builtin LSP for files inside instance", async () => {
-    await using tmp = await tmpdir()
-    const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
+          try {
+            yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
+            yield* lsp.hover({
+              file: path.join(dir, "..", "hover.ts"),
+              line: 0,
+              character: 0,
+            })
+            expect(spy).toHaveBeenCalledTimes(0)
+          } finally {
+            spy.mockRestore()
+          }
+        }),
+      ),
+    ),
+  )
 
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: async () => {
-          await Lsp.LSP.hover({
-            file: path.join(tmp.path, "src", "inside.ts"),
-            line: 0,
-            character: 0,
-          })
-        },
-      })
+  it.live("would spawn builtin LSP for files inside instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
 
-      expect(spy).toHaveBeenCalledTimes(1)
-    } finally {
-      spy.mockRestore()
-      await Instance.disposeAll()
-    }
-  })
+          try {
+            yield* lsp.hover({
+              file: path.join(dir, "src", "inside.ts"),
+              line: 0,
+              character: 0,
+            })
+            expect(spy).toHaveBeenCalledTimes(1)
+          } finally {
+            spy.mockRestore()
+          }
+        }),
+      ),
+    ),
+  )
 })

+ 97 - 92
packages/opencode/test/lsp/lifecycle.test.ts

@@ -1,23 +1,13 @@
-import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test"
+import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
 import path from "path"
-import * as Lsp from "../../src/lsp/index"
+import { Effect, Layer } from "effect"
+import { LSP } from "../../src/lsp"
 import { LSPServer } from "../../src/lsp/server"
-import { Instance } from "../../src/project/instance"
-import { tmpdir } from "../fixture/fixture"
-
-function withInstance(fn: (dir: string) => Promise<void>) {
-  return async () => {
-    await using tmp = await tmpdir()
-    try {
-      await Instance.provide({
-        directory: tmp.path,
-        fn: () => fn(tmp.path),
-      })
-    } finally {
-      await Instance.disposeAll()
-    }
-  }
-}
+import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
+import { provideTmpdirInstance } from "../fixture/fixture"
+import { testEffect } from "../lib/effect"
+
+const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
 
 describe("LSP service lifecycle", () => {
   let spawnSpy: ReturnType<typeof spyOn>
@@ -30,97 +20,112 @@ describe("LSP service lifecycle", () => {
     spawnSpy.mockRestore()
   })
 
-  test(
-    "init() completes without error",
-    withInstance(async () => {
-      await Lsp.LSP.init()
-    }),
-  )
-
-  test(
-    "status() returns empty array initially",
-    withInstance(async () => {
-      const result = await Lsp.LSP.status()
-      expect(Array.isArray(result)).toBe(true)
-      expect(result.length).toBe(0)
-    }),
+  it.live("init() completes without error", () => provideTmpdirInstance(() => LSP.Service.use((lsp) => lsp.init())))
+
+  it.live("status() returns empty array initially", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.status()
+          expect(Array.isArray(result)).toBe(true)
+          expect(result.length).toBe(0)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "diagnostics() returns empty object initially",
-    withInstance(async () => {
-      const result = await Lsp.LSP.diagnostics()
-      expect(typeof result).toBe("object")
-      expect(Object.keys(result).length).toBe(0)
-    }),
+  it.live("diagnostics() returns empty object initially", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.diagnostics()
+          expect(typeof result).toBe("object")
+          expect(Object.keys(result).length).toBe(0)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "hasClients() returns true for .ts files in instance",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.hasClients(path.join(dir, "test.ts"))
-      expect(result).toBe(true)
-    }),
+  it.live("hasClients() returns true for .ts files in instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
+          expect(result).toBe(true)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "hasClients() returns false for files outside instance",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.hasClients(path.join(dir, "..", "outside.ts"))
-      // hasClients checks servers but doesn't check containsPath — getClients does
-      // So hasClients may return true even for outside files (it checks extension + root)
-      // The guard is in getClients, not hasClients
-      expect(typeof result).toBe("boolean")
-    }),
+  it.live("hasClients() returns false for files outside instance", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
+          expect(typeof result).toBe("boolean")
+        }),
+      ),
+    ),
   )
 
-  test(
-    "workspaceSymbol() returns empty array with no clients",
-    withInstance(async () => {
-      const result = await Lsp.LSP.workspaceSymbol("test")
-      expect(Array.isArray(result)).toBe(true)
-      expect(result.length).toBe(0)
-    }),
+  it.live("workspaceSymbol() returns empty array with no clients", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.workspaceSymbol("test")
+          expect(Array.isArray(result)).toBe(true)
+          expect(result.length).toBe(0)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "definition() returns empty array for unknown file",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.definition({
-        file: path.join(dir, "nonexistent.ts"),
-        line: 0,
-        character: 0,
-      })
-      expect(Array.isArray(result)).toBe(true)
-    }),
+  it.live("definition() returns empty array for unknown file", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.definition({
+            file: path.join(dir, "nonexistent.ts"),
+            line: 0,
+            character: 0,
+          })
+          expect(Array.isArray(result)).toBe(true)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "references() returns empty array for unknown file",
-    withInstance(async (dir) => {
-      const result = await Lsp.LSP.references({
-        file: path.join(dir, "nonexistent.ts"),
-        line: 0,
-        character: 0,
-      })
-      expect(Array.isArray(result)).toBe(true)
-    }),
+  it.live("references() returns empty array for unknown file", () =>
+    provideTmpdirInstance((dir) =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          const result = yield* lsp.references({
+            file: path.join(dir, "nonexistent.ts"),
+            line: 0,
+            character: 0,
+          })
+          expect(Array.isArray(result)).toBe(true)
+        }),
+      ),
+    ),
   )
 
-  test(
-    "multiple init() calls are idempotent",
-    withInstance(async () => {
-      await Lsp.LSP.init()
-      await Lsp.LSP.init()
-      await Lsp.LSP.init()
-      // Should not throw or create duplicate state
-    }),
+  it.live("multiple init() calls are idempotent", () =>
+    provideTmpdirInstance(() =>
+      LSP.Service.use((lsp) =>
+        Effect.gen(function* () {
+          yield* lsp.init()
+          yield* lsp.init()
+          yield* lsp.init()
+        }),
+      ),
+    ),
   )
 })
 
 describe("LSP.Diagnostic", () => {
   test("pretty() formats error diagnostic", () => {
-    const result = Lsp.LSP.Diagnostic.pretty({
+    const result = LSP.Diagnostic.pretty({
       range: { start: { line: 9, character: 4 }, end: { line: 9, character: 10 } },
       message: "Type 'string' is not assignable to type 'number'",
       severity: 1,
@@ -129,7 +134,7 @@ describe("LSP.Diagnostic", () => {
   })
 
   test("pretty() formats warning diagnostic", () => {
-    const result = Lsp.LSP.Diagnostic.pretty({
+    const result = LSP.Diagnostic.pretty({
       range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } },
       message: "Unused variable",
       severity: 2,
@@ -138,7 +143,7 @@ describe("LSP.Diagnostic", () => {
   })
 
   test("pretty() defaults to ERROR when no severity", () => {
-    const result = Lsp.LSP.Diagnostic.pretty({
+    const result = LSP.Diagnostic.pretty({
       range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
       message: "Something wrong",
     } as any)