diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index 52aef0f9e3..f58dc82d42 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -1,12 +1,13 @@ import z from "zod" +import { Effect } from "effect" import { Tool } from "./tool" import path from "path" import { LSP } from "../lsp" import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" import { pathToFileURL } from "url" -import { assertExternalDirectory } from "./external-directory" -import { Filesystem } from "../util/filesystem" +import { assertExternalDirectoryEffect } from "./external-directory" +import { AppFileSystem } from "../filesystem" const operations = [ "goToDefinition", @@ -20,78 +21,70 @@ const operations = [ "outgoingCalls", ] as const -export const LspTool = Tool.define("lsp", { - description: DESCRIPTION, - parameters: z.object({ - operation: z.enum(operations).describe("The LSP operation to perform"), - filePath: z.string().describe("The absolute or relative path to the file"), - line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"), - character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"), - }), - execute: async (args, ctx) => { - const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) - await assertExternalDirectory(ctx, file) - - await ctx.ask({ - permission: "lsp", - patterns: ["*"], - always: ["*"], - metadata: {}, - }) - const uri = pathToFileURL(file).href - const position = { - file, - line: args.line - 1, - character: args.character - 1, - } - - const relPath = path.relative(Instance.worktree, file) - const title = `${args.operation} ${relPath}:${args.line}:${args.character}` - - const exists = await Filesystem.exists(file) - if (!exists) { - throw new Error(`File not found: ${file}`) - } - - const available = await LSP.hasClients(file) - if (!available) { - throw new Error("No LSP server available for this file type.") - } - - await LSP.touchFile(file, true) - - const result: unknown[] = await (async () => { - switch (args.operation) { - case "goToDefinition": - return LSP.definition(position) - case "findReferences": - return LSP.references(position) - case "hover": - return LSP.hover(position) - case "documentSymbol": - return LSP.documentSymbol(uri) - case "workspaceSymbol": - return LSP.workspaceSymbol("") - case "goToImplementation": - return LSP.implementation(position) - case "prepareCallHierarchy": - return LSP.prepareCallHierarchy(position) - case "incomingCalls": - return LSP.incomingCalls(position) - case "outgoingCalls": - return LSP.outgoingCalls(position) - } - })() - - const output = (() => { - if (result.length === 0) return `No results found for ${args.operation}` - return JSON.stringify(result, null, 2) - })() +export const LspTool = Tool.defineEffect( + "lsp", + Effect.gen(function* () { + const lsp = yield* LSP.Service + const fs = yield* AppFileSystem.Service return { - title, - metadata: { result }, - output, + description: DESCRIPTION, + parameters: z.object({ + operation: z.enum(operations).describe("The LSP operation to perform"), + filePath: z.string().describe("The absolute or relative path to the file"), + line: z.number().int().min(1).describe("The line number (1-based, as shown in editors)"), + character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"), + }), + execute: (args: { operation: (typeof operations)[number]; filePath: string; line: number; character: number }, ctx: Tool.Context) => + Effect.gen(function* () { + const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) + yield* assertExternalDirectoryEffect(ctx, file) + yield* Effect.promise(() => + ctx.ask({ permission: "lsp", patterns: ["*"], always: ["*"], metadata: {} }), + ) + + const uri = pathToFileURL(file).href + const position = { file, line: args.line - 1, character: args.character - 1 } + const relPath = path.relative(Instance.worktree, file) + const title = `${args.operation} ${relPath}:${args.line}:${args.character}` + + const exists = yield* fs.existsSafe(file) + if (!exists) throw new Error(`File not found: ${file}`) + + const available = yield* lsp.hasClients(file) + if (!available) throw new Error("No LSP server available for this file type.") + + yield* lsp.touchFile(file, true) + + const result: unknown[] = yield* (() => { + switch (args.operation) { + case "goToDefinition": + return lsp.definition(position) + case "findReferences": + return lsp.references(position) + case "hover": + return lsp.hover(position) + case "documentSymbol": + return lsp.documentSymbol(uri) + case "workspaceSymbol": + return lsp.workspaceSymbol("") + case "goToImplementation": + return lsp.implementation(position) + case "prepareCallHierarchy": + return lsp.prepareCallHierarchy(position) + case "incomingCalls": + return lsp.incomingCalls(position) + case "outgoingCalls": + return lsp.outgoingCalls(position) + } + })() + + return { + title, + metadata: { result }, + output: result.length === 0 ? `No results found for ${args.operation}` : JSON.stringify(result, null, 2), + } + }).pipe(Effect.runPromise), } - }, -}) + }), +) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index 9c0771b8df..b653c336ef 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -92,6 +92,7 @@ export namespace ToolRegistry { const read = yield* ReadTool const question = yield* QuestionTool const todo = yield* TodoWriteTool + const lsptool = yield* LspTool const state = yield* InstanceState.make( Effect.fn("ToolRegistry.state")(function* (ctx) { @@ -164,7 +165,7 @@ export namespace ToolRegistry { skill: Tool.init(SkillTool), patch: Tool.init(ApplyPatchTool), question: Tool.init(question), - lsp: Tool.init(LspTool), + lsp: Tool.init(lsptool), plan: Tool.init(PlanExitTool), })