summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/opencode/src/cli/cmd/debug/lsp.ts19
-rw-r--r--packages/opencode/src/lsp/index.ts32
-rw-r--r--packages/opencode/src/server/instance/file.ts5
-rw-r--r--packages/opencode/src/server/instance/index.ts3
-rw-r--r--packages/opencode/test/lsp/index.test.ts94
-rw-r--r--packages/opencode/test/lsp/lifecycle.test.ts189
6 files changed, 160 insertions, 182 deletions
diff --git a/packages/opencode/src/cli/cmd/debug/lsp.ts b/packages/opencode/src/cli/cmd/debug/lsp.ts
index 4b8a3e7d4..5f0a1807d 100644
--- a/packages/opencode/src/cli/cmd/debug/lsp.ts
+++ b/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)
})
},
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index 8e34a8854..0c83890e5 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/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
diff --git a/packages/opencode/src/server/instance/file.ts b/packages/opencode/src/server/instance/file.ts
index 713513b38..a869cf367 100644
--- a/packages/opencode/src/server/instance/file.ts
+++ b/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([])
},
)
diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts
index 6009130a2..6d383afa7 100644
--- a/packages/opencode/src/server/instance/index.ts
+++ b/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(
diff --git a/packages/opencode/test/lsp/index.test.ts b/packages/opencode/test/lsp/index.test.ts
index 7e514e39b..b12a61ae3 100644
--- a/packages/opencode/test/lsp/index.test.ts
+++ b/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()
+ }
+ }),
+ ),
+ ),
+ )
})
diff --git a/packages/opencode/test/lsp/lifecycle.test.ts b/packages/opencode/test/lsp/lifecycle.test.ts
index fb3ed8c21..a6de869fc 100644
--- a/packages/opencode/test/lsp/lifecycle.test.ts
+++ b/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)