mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 13:21:17 +08:00
refactor(lsp): simplify effect timeout flow
This commit is contained in:
@@ -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,
|
||||
},
|
||||
capabilities: {
|
||||
window: {
|
||||
workDoneProgress: true,
|
||||
},
|
||||
workspace: {
|
||||
configuration: true,
|
||||
didChangeWatchedFiles: {
|
||||
dynamicRegistration: 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 },
|
||||
yield* Effect.tryPromise(() =>
|
||||
connection.sendRequest("initialize", {
|
||||
rootUri: pathToFileURL(input.root).href,
|
||||
processId: input.server.process.pid,
|
||||
workspaceFolders: [
|
||||
{
|
||||
cause: error,
|
||||
name: "workspace",
|
||||
uri: pathToFileURL(input.root).href,
|
||||
},
|
||||
],
|
||||
initializationOptions: {
|
||||
...input.server.initialization,
|
||||
},
|
||||
capabilities: {
|
||||
window: {
|
||||
workDoneProgress: true,
|
||||
},
|
||||
workspace: {
|
||||
configuration: true,
|
||||
didChangeWatchedFiles: {
|
||||
dynamicRegistration: true,
|
||||
},
|
||||
},
|
||||
textDocument: {
|
||||
synchronization: {
|
||||
didOpen: true,
|
||||
didChange: true,
|
||||
},
|
||||
publishDiagnostics: {
|
||||
versionSupport: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).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)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user