refactor(tool): convert lsp tool internals to Effect (#21806)
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import { Effect } from "effect"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { LSP } from "../lsp"
|
import { LSP } from "../lsp"
|
||||||
import DESCRIPTION from "./lsp.txt"
|
import DESCRIPTION from "./lsp.txt"
|
||||||
import { Instance } from "../project/instance"
|
import { Instance } from "../project/instance"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { assertExternalDirectory } from "./external-directory"
|
import { assertExternalDirectoryEffect } from "./external-directory"
|
||||||
import { Filesystem } from "../util/filesystem"
|
import { AppFileSystem } from "../filesystem"
|
||||||
|
|
||||||
const operations = [
|
const operations = [
|
||||||
"goToDefinition",
|
"goToDefinition",
|
||||||
@@ -20,78 +21,70 @@ const operations = [
|
|||||||
"outgoingCalls",
|
"outgoingCalls",
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const LspTool = Tool.define("lsp", {
|
export const LspTool = Tool.defineEffect(
|
||||||
description: DESCRIPTION,
|
"lsp",
|
||||||
parameters: z.object({
|
Effect.gen(function* () {
|
||||||
operation: z.enum(operations).describe("The LSP operation to perform"),
|
const lsp = yield* LSP.Service
|
||||||
filePath: z.string().describe("The absolute or relative path to the file"),
|
const fs = yield* AppFileSystem.Service
|
||||||
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)
|
|
||||||
})()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
description: DESCRIPTION,
|
||||||
metadata: { result },
|
parameters: z.object({
|
||||||
output,
|
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),
|
||||||
}
|
}
|
||||||
},
|
}),
|
||||||
})
|
)
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export namespace ToolRegistry {
|
|||||||
const read = yield* ReadTool
|
const read = yield* ReadTool
|
||||||
const question = yield* QuestionTool
|
const question = yield* QuestionTool
|
||||||
const todo = yield* TodoWriteTool
|
const todo = yield* TodoWriteTool
|
||||||
|
const lsptool = yield* LspTool
|
||||||
|
|
||||||
const state = yield* InstanceState.make<State>(
|
const state = yield* InstanceState.make<State>(
|
||||||
Effect.fn("ToolRegistry.state")(function* (ctx) {
|
Effect.fn("ToolRegistry.state")(function* (ctx) {
|
||||||
@@ -164,7 +165,7 @@ export namespace ToolRegistry {
|
|||||||
skill: Tool.init(SkillTool),
|
skill: Tool.init(SkillTool),
|
||||||
patch: Tool.init(ApplyPatchTool),
|
patch: Tool.init(ApplyPatchTool),
|
||||||
question: Tool.init(question),
|
question: Tool.init(question),
|
||||||
lsp: Tool.init(LspTool),
|
lsp: Tool.init(lsptool),
|
||||||
plan: Tool.init(PlanExitTool),
|
plan: Tool.init(PlanExitTool),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user