import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { pathToFileURL } from "node:url"; import type { LspManager } from "./manager.js"; // Rich query tools: hover, definition, references, symbols, rename. Thin // sendRequest wrappers through the manager's first-matching-client semantics. const PositionInput = { path: Type.String({ description: "File path (absolute or relative to the workspace)." }), line: Type.Number({ description: "Zero-based line." }), character: Type.Number({ description: "Zero-based character." }), }; interface LocationLike { uri?: string; range?: { start: { line: number; character: number } }; } function renderLocations(locations: LocationLike[]): string { const lines = locations.map((l) => { const file = l.uri?.startsWith("file://") ? decodeURIComponent(new URL(l.uri).pathname) : (l.uri ?? "?"); return `${file}:${(l.range?.start.line ?? 0) + 1}:${(l.range?.start.character ?? 0) + 1}`; }); return lines.length ? lines.join("\n") : "No results."; } function pathOf(p: string, cwd: string): string { return p.startsWith("/") ? p : `${cwd}/${p}`; } export function createDefinitionTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_definition", label: "LSP: Definition", description: "Find the definition of the symbol at a position.", promptSnippet: "Find symbol definitions via LSP", promptGuidelines: [ "Use lsp_definition instead of grep when the user asks where a symbol is defined — it resolves cross-file locations accurately.", "Positions for lsp_definition are zero-based (line and character).", ], parameters: Type.Object(PositionInput), async execute(_id, params, _signal, _onUpdate, ctx) { const file = pathOf(params.path, ctx.cwd); const locations = await getManager().request(file, "textDocument/definition", { textDocument: { uri: pathToFileURL(file).href }, position: { line: params.line, character: params.character }, }); return { content: [{ type: "text", text: renderLocations(locations ?? []) }], details: { locations: locations ?? [] }, }; }, }); } export function createReferencesTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_references", label: "LSP: References", description: "Find all references to the symbol at a position (including the declaration).", promptSnippet: "Find symbol references via LSP", promptGuidelines: [ "Use lsp_references when the user asks where a symbol is used across the codebase — it is more accurate than grep for cross-file references.", "Positions for lsp_references are zero-based (line and character).", ], parameters: Type.Object(PositionInput), async execute(_id, params, _signal, _onUpdate, ctx) { const file = pathOf(params.path, ctx.cwd); const locations = await getManager().request(file, "textDocument/references", { textDocument: { uri: pathToFileURL(file).href }, position: { line: params.line, character: params.character }, context: { includeDeclaration: true }, }); return { content: [{ type: "text", text: renderLocations(locations ?? []) }], details: { locations: locations ?? [] }, }; }, }); } export function createHoverTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_hover", label: "LSP: Hover", description: "Get hover documentation for the symbol at a position.", promptSnippet: "Get hover docs via LSP", promptGuidelines: [ "Use lsp_hover to get documentation or signatures for a symbol when the user asks 'what does this do'.", "Positions for lsp_hover are zero-based (line and character).", ], parameters: Type.Object(PositionInput), async execute(_id, params, _signal, _onUpdate, ctx) { const file = pathOf(params.path, ctx.cwd); const hover = await getManager().request(file, "textDocument/hover", { textDocument: { uri: pathToFileURL(file).href }, position: { line: params.line, character: params.character }, }); const contents = hover?.contents; const value = typeof contents === "string" ? contents : (contents?.value ?? (contents ? JSON.stringify(contents) : "No hover info.")); return { content: [{ type: "text", text: String(value) }], details: { hover } }; }, }); } export function createImplementationTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_implementation", label: "LSP: Implementation", description: "Find implementations of the symbol at a position.", promptSnippet: "Find symbol implementations via LSP", promptGuidelines: [ "Use lsp_implementation when the user asks where a symbol (interface/abstract member) is implemented.", "Positions for lsp_implementation are zero-based (line and character).", ], parameters: Type.Object(PositionInput), async execute(_id, params, _signal, _onUpdate, ctx) { const file = pathOf(params.path, ctx.cwd); return getManager() .implementation({ file, line: params.line, character: params.character }) .then((locations) => ({ content: [{ type: "text", text: renderLocations((locations ?? []) as LocationLike[]) }], details: { locations: locations ?? [] }, })); }, }); } export function createWorkspaceSymbolTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_workspace_symbol", label: "LSP: Workspace Symbol", description: "Search workspace symbols by query (classes, functions, methods, ...).", promptSnippet: "Search workspace symbols via LSP", promptGuidelines: [ "Use lsp_workspace_symbol to find symbols across the whole workspace by name query (up to 10 results).", ], parameters: Type.Object({ query: Type.String({ description: "Symbol name query." }) }), async execute(_id, params, _signal, _onUpdate, ctx) { const symbols = (await getManager().workspaceSymbol(params.query)) ?? []; const text = symbols.length ? symbols.map((s) => `${s.name} (kind ${s.kind})`).join("\n") : "No workspace symbols found."; return { content: [{ type: "text", text }], details: { symbols } }; }, }); } export function createCallHierarchyTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_call_hierarchy", label: "LSP: Call Hierarchy", description: "Prepare call hierarchy for the symbol at a position, or list incoming/outgoing calls.", promptSnippet: "Explore call hierarchy via LSP", promptGuidelines: [ "Use lsp_call_hierarchy to show callers (incoming) or callees (outgoing) of the symbol at a position.", "Positions for lsp_call_hierarchy are zero-based (line and character).", ], parameters: Type.Object({ ...PositionInput, direction: Type.Optional( Type.String({ description: "prepare | incoming | outgoing. Defaults to prepare." }), ), }), async execute(_id, params, _signal, _onUpdate, ctx) { const manager = getManager(); const file = pathOf(params.path, ctx.cwd); const input = { file, line: params.line, character: params.character }; const direction = params.direction ?? "prepare"; const items = direction === "incoming" ? ((await manager.incomingCalls(input)) ?? []) : direction === "outgoing" ? ((await manager.outgoingCalls(input)) ?? []) : ((await manager.prepareCallHierarchy(input)) ?? []); const names = ( items as Array<{ name?: string; from?: { name?: string }; to?: { name?: string } }> ).map((item) => item.name ?? item.from?.name ?? item.to?.name ?? "?"); const text = names.length ? names.join("\n") : `No ${direction} call hierarchy items.`; return { content: [{ type: "text", text }], details: { direction, items } }; }, }); } export function createSymbolsTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_symbols", label: "LSP: Symbols", description: "List symbols declared in a file.", promptSnippet: "List file symbols via LSP", promptGuidelines: [ "Use lsp_symbols to list the symbols declared in a file when the user asks for its structure or overview.", ], parameters: Type.Object({ path: Type.String({ description: "File path." }) }), async execute(_id, params, _signal, _onUpdate, ctx) { const file = pathOf(params.path, ctx.cwd); const symbols = await getManager().request>( file, "textDocument/documentSymbol", { textDocument: { uri: pathToFileURL(file).href } }, ); const names = (symbols ?? []).map((s) => s.name); return { content: [{ type: "text", text: names.length ? names.join("\n") : "No symbols." }], details: { symbols: names }, }; }, }); } export function createRenameTool(getManager: () => LspManager): ToolDefinition { return defineTool({ name: "lsp_rename", label: "LSP: Rename", description: "Compute a workspace rename of the symbol at a position. Preview only — returns edits, never writes.", promptSnippet: "Compute LSP rename edits", promptGuidelines: [ "Use lsp_rename to preview a workspace rename — it returns edits and never writes; apply the edits with your file tools.", "Positions for lsp_rename are zero-based (line and character).", ], parameters: Type.Object({ ...PositionInput, newName: Type.String({ description: "New symbol name." }), }), async execute(_id, params, _signal, _onUpdate, ctx) { const file = pathOf(params.path, ctx.cwd); const edit = await getManager().request(file, "textDocument/rename", { textDocument: { uri: pathToFileURL(file).href }, position: { line: params.line, character: params.character }, newName: params.newName, }); const changes = (edit?.changes ?? {}) as Record; const count = Object.values(changes).reduce((n, arr) => n + arr.length, 0); return { content: [ { type: "text", text: `${params.newName}: ${count} edit(s) across ${Object.keys(changes).length} file(s).`, }, ], details: { edit }, }; }, }); }