diff --git a/packages/opencode/src/lsp/lsp.ts b/packages/opencode/src/lsp/lsp.ts index 307e85ae73..ada9aba671 100644 --- a/packages/opencode/src/lsp/lsp.ts +++ b/packages/opencode/src/lsp/lsp.ts @@ -13,8 +13,11 @@ import { InstanceState } from "@/effect/instance-state" import { containsPath } from "@/project/instance-context" import { NonNegativeInt } from "@opencode-ai/core/schema" import { RuntimeFlags } from "@/effect/runtime-flags" +import { InstanceRef } from "@/effect/instance-ref" +import { makeRuntime } from "@/effect/run-service" const log = Log.create({ service: "lsp" }) +const busRuntime = makeRuntime(Bus.Service, Bus.layer) export const Event = { Updated: BusEvent.define("lsp.updated", Schema.Struct({})), @@ -291,7 +294,9 @@ export const layer = Layer.effect( if (!client) continue result.push(client) - Bus.publish(Event.Updated, {}) + void busRuntime.runPromise((bus) => + bus.publish(Event.Updated, {}).pipe(Effect.provideService(InstanceRef, ctx)), + ) } return result diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts index b69963b301..78543c4583 100644 --- a/packages/opencode/test/lsp/index.test.ts +++ b/packages/opencode/test/lsp/index.test.ts @@ -1,13 +1,14 @@ import { describe, expect, spyOn } from "bun:test" import path from "path" -import { Effect, Layer } from "effect" +import { Deferred, Effect, Layer } from "effect" +import { Bus } from "@/bus" import { Config } from "@/config/config" import { RuntimeFlags } from "@/effect/runtime-flags" import { LSP } from "@/lsp/lsp" import * as LSPServer from "@/lsp/server" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" -import { testEffect } from "../lib/effect" +import { awaitWithTimeout, testEffect } from "../lib/effect" const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer)) const experimentalTyIt = testEffect( @@ -16,6 +17,7 @@ const experimentalTyIt = testEffect( CrossSpawnSpawner.defaultLayer, ), ) +const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js") const disabledDownloadIt = testEffect( Layer.mergeAll( LSP.layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.layer({ disableLspDownload: true }))), @@ -92,6 +94,35 @@ describe("lsp.spawn", () => { ), ) + it.live("publishes lsp.updated after custom LSP initialization", () => + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const lsp = yield* LSP.Service + const updated = yield* Deferred.make() + const unsubscribe = Bus.subscribe(LSP.Event.Updated, () => + Effect.runSync(Deferred.succeed(updated, undefined)), + ) + yield* Effect.addFinalizer(() => Effect.sync(unsubscribe)) + + const file = path.join(dir, "sample.repro") + yield* Effect.promise(() => Bun.write(file, "sample\n")) + yield* lsp.touchFile(file) + yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published") + }), + { + config: { + lsp: { + fake: { + command: [process.execPath, fakeServerPath], + extensions: [".repro"], + }, + }, + }, + }, + ), + ) + it.live("would spawn builtin LSP for files inside instance when config object is provided", () => provideTmpdirInstance( (dir) =>