|
|
@@ -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,
|
|
|
- }
|
|
|
+export const LspTool = Tool.defineEffect(
|
|
|
+ "lsp",
|
|
|
+ Effect.gen(function* () {
|
|
|
+ const lsp = yield* LSP.Service
|
|
|
+ const fs = yield* AppFileSystem.Service
|
|
|
|
|
|
- const relPath = path.relative(Instance.worktree, file)
|
|
|
- const title = `${args.operation} ${relPath}:${args.line}:${args.character}`
|
|
|
+ return {
|
|
|
+ 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 exists = await Filesystem.exists(file)
|
|
|
- if (!exists) {
|
|
|
- throw new Error(`File not found: ${file}`)
|
|
|
- }
|
|
|
+ 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 available = await LSP.hasClients(file)
|
|
|
- if (!available) {
|
|
|
- throw new Error("No LSP server available for this file type.")
|
|
|
- }
|
|
|
+ const exists = yield* fs.existsSafe(file)
|
|
|
+ if (!exists) throw new Error(`File not found: ${file}`)
|
|
|
|
|
|
- await LSP.touchFile(file, true)
|
|
|
+ const available = yield* lsp.hasClients(file)
|
|
|
+ if (!available) throw new Error("No LSP server available for this file type.")
|
|
|
|
|
|
- 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)
|
|
|
- }
|
|
|
- })()
|
|
|
+ yield* lsp.touchFile(file, true)
|
|
|
|
|
|
- const output = (() => {
|
|
|
- if (result.length === 0) return `No results found for ${args.operation}`
|
|
|
- return JSON.stringify(result, null, 2)
|
|
|
- })()
|
|
|
+ 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,
|
|
|
+ return {
|
|
|
+ title,
|
|
|
+ metadata: { result },
|
|
|
+ output: result.length === 0 ? `No results found for ${args.operation}` : JSON.stringify(result, null, 2),
|
|
|
+ }
|
|
|
+ }).pipe(Effect.runPromise),
|
|
|
}
|
|
|
- },
|
|
|
-})
|
|
|
+ }),
|
|
|
+)
|