Procházet zdrojové kódy

refactor(lsp): simplify effect timeout flow

Kit Langton před 16 hodinami
rodič
revize
bd5b892234

+ 56 - 49
packages/opencode/src/lsp/client.ts

@@ -11,7 +11,6 @@ import { LANGUAGE_EXTENSIONS } from "./language"
 import z from "zod"
 import type * as LSPServer from "./server"
 import { NamedError } from "@opencode-ai/shared/util/error"
-import { withTimeout } from "../util/timeout"
 import { Instance } from "../project/instance"
 import { Filesystem } from "../util"
 
@@ -93,54 +92,65 @@ export const create = Effect.fn("LSPClient.create")(function* (input: {
   connection.listen()
 
   l.info("sending initialize")
-  yield* Effect.tryPromise({
-    try: () =>
-      withTimeout(
-        connection.sendRequest("initialize", {
-          rootUri: pathToFileURL(input.root).href,
-          processId: input.server.process.pid,
-          workspaceFolders: [
-            {
-              name: "workspace",
-              uri: pathToFileURL(input.root).href,
-            },
-          ],
-          initializationOptions: {
-            ...input.server.initialization,
+  yield* Effect.tryPromise(() =>
+    connection.sendRequest("initialize", {
+      rootUri: pathToFileURL(input.root).href,
+      processId: input.server.process.pid,
+      workspaceFolders: [
+        {
+          name: "workspace",
+          uri: pathToFileURL(input.root).href,
+        },
+      ],
+      initializationOptions: {
+        ...input.server.initialization,
+      },
+      capabilities: {
+        window: {
+          workDoneProgress: true,
+        },
+        workspace: {
+          configuration: true,
+          didChangeWatchedFiles: {
+            dynamicRegistration: true,
           },
-          capabilities: {
-            window: {
-              workDoneProgress: true,
-            },
-            workspace: {
-              configuration: true,
-              didChangeWatchedFiles: {
-                dynamicRegistration: true,
-              },
-            },
-            textDocument: {
-              synchronization: {
-                didOpen: true,
-                didChange: true,
-              },
-              publishDiagnostics: {
-                versionSupport: true,
-              },
-            },
+        },
+        textDocument: {
+          synchronization: {
+            didOpen: true,
+            didChange: true,
+          },
+          publishDiagnostics: {
+            versionSupport: true,
           },
-        }),
-        45_000,
-      ),
-    catch: (error) => {
-      l.error("initialize error", { error })
-      return new InitializeError(
-        { serverID: input.serverID },
-        {
-          cause: error,
         },
+      },
+    }),
+  ).pipe(
+    Effect.timeoutOrElse({
+      duration: 45_000,
+      orElse: () =>
+        Effect.fail(
+          new InitializeError(
+            { serverID: input.serverID },
+            { cause: new Error("LSP initialize timed out after 45 seconds") },
+          ),
+        ),
+    }),
+    Effect.catch((error) => {
+      l.error("initialize error", { error })
+      return Effect.fail(
+        error instanceof InitializeError
+          ? error
+          : new InitializeError(
+              { serverID: input.serverID },
+              {
+                cause: error,
+              },
+            ),
       )
-    },
-  })
+    }),
+  )
 
   yield* Effect.tryPromise(() => connection.sendNotification("initialized", {}))
 
@@ -227,7 +237,6 @@ export const create = Effect.fn("LSPClient.create")(function* (input: {
     let unsub: (() => void) | undefined
     let debounceTimer: ReturnType<typeof setTimeout> | undefined
     yield* Effect.promise(() =>
-      withTimeout(
         new Promise<void>((resolve) => {
           unsub = Bus.subscribe(Event.Diagnostics, (event) => {
             if (event.properties.path === normalizedPath && event.properties.serverID === input.serverID) {
@@ -240,10 +249,8 @@ export const create = Effect.fn("LSPClient.create")(function* (input: {
             }
           })
         }),
-        3000,
-      ),
     ).pipe(
-      Effect.catch(() => Effect.void),
+      Effect.timeoutOrElse({ duration: 3000, orElse: () => Effect.void }),
       Effect.ensuring(
         Effect.sync(() => {
           if (debounceTimer) clearTimeout(debounceTimer)

+ 12 - 8
packages/opencode/src/lsp/lsp.ts

@@ -11,7 +11,7 @@ import { Instance } from "../project/instance"
 import { Flag } from "@/flag/flag"
 import { Process } from "../util"
 import { spawn as lspspawn } from "./launch"
-import { Effect, Layer, Context } from "effect"
+import { Effect, Fiber, Layer, Context, Scope } from "effect"
 import { InstanceState } from "@/effect"
 
 const log = Log.create({ service: "lsp" })
@@ -160,6 +160,7 @@ export const layer = Layer.effect(
   Service,
   Effect.gen(function* () {
     const config = yield* Config.Service
+    const scope = yield* Scope.Scope
 
     const state = yield* InstanceState.make<State>(
       Effect.fn("LSP.state")(function* () {
@@ -367,14 +368,17 @@ export const layer = Layer.effect(
     const touchFile = Effect.fn("LSP.touchFile")(function* (input: string, waitForDiagnostics?: boolean) {
       log.info("touching file", { file: input })
       const clients = yield* getClients(input)
-      yield* Effect.tryPromise(() =>
-        Promise.all(
-          clients.map(async (client) => {
-            const wait = waitForDiagnostics ? Effect.runPromise(client.waitForDiagnostics({ path: input })) : Promise.resolve()
-            await Effect.runPromise(client.notify.open({ path: input }))
-            return wait
+      yield* Effect.forEach(
+        clients,
+        (client) =>
+          Effect.gen(function* () {
+            const waiting = waitForDiagnostics
+              ? yield* client.waitForDiagnostics({ path: input }).pipe(Effect.forkIn(scope))
+              : undefined
+            yield* client.notify.open({ path: input })
+            if (waiting) yield* Fiber.join(waiting)
           }),
-        ),
+        { concurrency: "unbounded", discard: true },
       ).pipe(
         Effect.catch((err: unknown) =>
           Effect.sync(() => {

+ 2 - 7
packages/opencode/src/lsp/server.ts

@@ -17,11 +17,6 @@ import { Npm } from "../npm"
 import { Effect } from "effect"
 
 const log = Log.create({ service: "lsp.server" })
-const pathExists = async (p: string) =>
-  fs
-    .stat(p)
-    .then(() => true)
-    .catch(() => false)
 const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true })
 const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true })
 
@@ -1131,7 +1126,7 @@ export const JDTLS: RawInfo = {
     }
     const distPath = path.join(Global.Path.bin, "jdtls")
     const launcherDir = path.join(distPath, "plugins")
-    const installed = await pathExists(launcherDir)
+    const installed = await Filesystem.exists(launcherDir)
     if (!installed) {
       if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
       log.info("Downloading JDTLS LSP server.")
@@ -1163,7 +1158,7 @@ export const JDTLS: RawInfo = {
         .find((item) => /^org\.eclipse\.equinox\.launcher_.*\.jar$/.test(item))
         ?.trim() ?? ""
     const launcherJar = path.join(launcherDir, jarFileName)
-    if (!(await pathExists(launcherJar))) {
+    if (!(await Filesystem.exists(launcherJar))) {
       log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
       return
     }

+ 22 - 34
packages/opencode/test/lsp/client.test.ts

@@ -3,8 +3,8 @@ import path from "path"
 import { Effect } from "effect"
 import { LSPClient } from "../../src/lsp"
 import { LSPServer } from "../../src/lsp"
-import { Instance } from "../../src/project/instance"
 import { Log } from "../../src/util"
+import { provideInstance } from "../fixture/fixture"
 
 // Minimal fake LSP server that speaks JSON-RPC over stdio
 function spawnFakeServer() {
@@ -25,17 +25,13 @@ describe("LSPClient interop", () => {
   test("handles workspace/workspaceFolders request", async () => {
     const handle = spawnFakeServer() as any
 
-    const client = await Instance.provide({
-      directory: process.cwd(),
-      fn: () =>
-        Effect.runPromise(
-          LSPClient.create({
-            serverID: "fake",
-            server: handle as unknown as LSPServer.Handle,
-            root: process.cwd(),
-          }),
-        ),
-    })
+    const client = await Effect.runPromise(
+      LSPClient.create({
+        serverID: "fake",
+        server: handle as unknown as LSPServer.Handle,
+        root: process.cwd(),
+      }).pipe(provideInstance(process.cwd())),
+    )
 
     await client.connection.sendNotification("test/trigger", {
       method: "workspace/workspaceFolders",
@@ -51,17 +47,13 @@ describe("LSPClient interop", () => {
   test("handles client/registerCapability request", async () => {
     const handle = spawnFakeServer() as any
 
-    const client = await Instance.provide({
-      directory: process.cwd(),
-      fn: () =>
-        Effect.runPromise(
-          LSPClient.create({
-            serverID: "fake",
-            server: handle as unknown as LSPServer.Handle,
-            root: process.cwd(),
-          }),
-        ),
-    })
+    const client = await Effect.runPromise(
+      LSPClient.create({
+        serverID: "fake",
+        server: handle as unknown as LSPServer.Handle,
+        root: process.cwd(),
+      }).pipe(provideInstance(process.cwd())),
+    )
 
     await client.connection.sendNotification("test/trigger", {
       method: "client/registerCapability",
@@ -77,17 +69,13 @@ describe("LSPClient interop", () => {
   test("handles client/unregisterCapability request", async () => {
     const handle = spawnFakeServer() as any
 
-    const client = await Instance.provide({
-      directory: process.cwd(),
-      fn: () =>
-        Effect.runPromise(
-          LSPClient.create({
-            serverID: "fake",
-            server: handle as unknown as LSPServer.Handle,
-            root: process.cwd(),
-          }),
-        ),
-    })
+    const client = await Effect.runPromise(
+      LSPClient.create({
+        serverID: "fake",
+        server: handle as unknown as LSPServer.Handle,
+        root: process.cwd(),
+      }).pipe(provideInstance(process.cwd())),
+    )
 
     await client.connection.sendNotification("test/trigger", {
       method: "client/unregisterCapability",