import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool" import { getAllServers } from "./config" import { DEFAULT_MAX_REFERENCES, DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_DIAGNOSTICS, } from "./constants" import { withLspClient, formatHoverResult, formatLocation, formatDocumentSymbol, formatSymbolInfo, formatDiagnostic, filterDiagnosticsBySeverity, formatPrepareRenameResult, formatCodeActions, applyWorkspaceEdit, formatApplyResult, } from "./utils" import type { HoverResult, Location, LocationLink, DocumentSymbol, SymbolInfo, Diagnostic, PrepareRenameResult, PrepareRenameDefaultBehavior, WorkspaceEdit, CodeAction, Command, } from "./types" export const lsp_hover: ToolDefinition = tool({ description: "Get type info, docs, and signature for a symbol at position.", args: { filePath: tool.schema.string(), line: tool.schema.number().min(1).describe("1-based"), character: tool.schema.number().min(0).describe("0-based"), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.hover(args.filePath, args.line, args.character)) as HoverResult | null }) const output = formatHoverResult(result) return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_goto_definition: ToolDefinition = tool({ description: "Jump to symbol definition. Find WHERE something is defined.", args: { filePath: tool.schema.string(), line: tool.schema.number().min(1).describe("1-based"), character: tool.schema.number().min(0).describe("0-based"), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.definition(args.filePath, args.line, args.character)) as | Location | Location[] | LocationLink[] | null }) if (!result) { const output = "No definition found" return output } const locations = Array.isArray(result) ? result : [result] if (locations.length === 0) { const output = "No definition found" return output } const output = locations.map(formatLocation).join("\n") return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_find_references: ToolDefinition = tool({ description: "Find ALL usages/references of a symbol across the entire workspace.", args: { filePath: tool.schema.string(), line: tool.schema.number().min(1).describe("1-based"), character: tool.schema.number().min(0).describe("0-based"), includeDeclaration: tool.schema.boolean().optional().describe("Include the declaration itself"), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.references(args.filePath, args.line, args.character, args.includeDeclaration ?? true)) as | Location[] | null }) if (!result || result.length === 0) { const output = "No references found" return output } const total = result.length const truncated = total > DEFAULT_MAX_REFERENCES const limited = truncated ? result.slice(0, DEFAULT_MAX_REFERENCES) : result const lines = limited.map(formatLocation) if (truncated) { lines.unshift(`Found ${total} references (showing first ${DEFAULT_MAX_REFERENCES}):`) } const output = lines.join("\n") return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_document_symbols: ToolDefinition = tool({ description: "Get hierarchical outline of all symbols in a file.", args: { filePath: tool.schema.string(), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.documentSymbols(args.filePath)) as DocumentSymbol[] | SymbolInfo[] | null }) if (!result || result.length === 0) { const output = "No symbols found" return output } const total = result.length const truncated = total > DEFAULT_MAX_SYMBOLS const limited = truncated ? result.slice(0, DEFAULT_MAX_SYMBOLS) : result const lines: string[] = [] if (truncated) { lines.push(`Found ${total} symbols (showing first ${DEFAULT_MAX_SYMBOLS}):`) } if ("range" in limited[0]) { lines.push(...(limited as DocumentSymbol[]).map((s) => formatDocumentSymbol(s))) } else { lines.push(...(limited as SymbolInfo[]).map(formatSymbolInfo)) } return lines.join("\n") } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_workspace_symbols: ToolDefinition = tool({ description: "Search symbols by name across ENTIRE workspace.", args: { filePath: tool.schema.string(), query: tool.schema.string().describe("Symbol name (fuzzy match)"), limit: tool.schema.number().optional().describe("Max results"), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.workspaceSymbols(args.query)) as SymbolInfo[] | null }) if (!result || result.length === 0) { const output = "No symbols found" return output } const total = result.length const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS) const truncated = total > limit const limited = result.slice(0, limit) const lines = limited.map(formatSymbolInfo) if (truncated) { lines.unshift(`Found ${total} symbols (showing first ${limit}):`) } const output = lines.join("\n") return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_diagnostics: ToolDefinition = tool({ description: "Get errors, warnings, hints from language server BEFORE running build.", args: { filePath: tool.schema.string(), severity: tool.schema .enum(["error", "warning", "information", "hint", "all"]) .optional() .describe("Filter by severity level"), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.diagnostics(args.filePath)) as { items?: Diagnostic[] } | Diagnostic[] | null }) let diagnostics: Diagnostic[] = [] if (result) { if (Array.isArray(result)) { diagnostics = result } else if (result.items) { diagnostics = result.items } } diagnostics = filterDiagnosticsBySeverity(diagnostics, args.severity) if (diagnostics.length === 0) { const output = "No diagnostics found" return output } const total = diagnostics.length const truncated = total > DEFAULT_MAX_DIAGNOSTICS const limited = truncated ? diagnostics.slice(0, DEFAULT_MAX_DIAGNOSTICS) : diagnostics const lines = limited.map(formatDiagnostic) if (truncated) { lines.unshift(`Found ${total} diagnostics (showing first ${DEFAULT_MAX_DIAGNOSTICS}):`) } const output = lines.join("\n") return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_servers: ToolDefinition = tool({ description: "List available LSP servers and installation status.", args: {}, execute: async (_args, context) => { try { const servers = getAllServers() const lines = servers.map((s) => { if (s.disabled) { return `${s.id} [disabled] - ${s.extensions.join(", ")}` } const status = s.installed ? "[installed]" : "[not installed]" return `${s.id} ${status} - ${s.extensions.join(", ")}` }) const output = lines.join("\n") return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_prepare_rename: ToolDefinition = tool({ description: "Check if rename is valid. Use BEFORE lsp_rename.", args: { filePath: tool.schema.string(), line: tool.schema.number().min(1).describe("1-based"), character: tool.schema.number().min(0).describe("0-based"), }, execute: async (args, context) => { try { const result = await withLspClient(args.filePath, async (client) => { return (await client.prepareRename(args.filePath, args.line, args.character)) as | PrepareRenameResult | PrepareRenameDefaultBehavior | null }) const output = formatPrepareRenameResult(result) return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_rename: ToolDefinition = tool({ description: "Rename symbol across entire workspace. APPLIES changes to all files.", args: { filePath: tool.schema.string(), line: tool.schema.number().min(1).describe("1-based"), character: tool.schema.number().min(0).describe("0-based"), newName: tool.schema.string().describe("New symbol name"), }, execute: async (args, context) => { try { const edit = await withLspClient(args.filePath, async (client) => { return (await client.rename(args.filePath, args.line, args.character, args.newName)) as WorkspaceEdit | null }) const result = applyWorkspaceEdit(edit) const output = formatApplyResult(result) return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_code_actions: ToolDefinition = tool({ description: "Get available quick fixes, refactorings, and source actions (organize imports, fix all).", args: { filePath: tool.schema.string(), startLine: tool.schema.number().min(1).describe("1-based"), startCharacter: tool.schema.number().min(0).describe("0-based"), endLine: tool.schema.number().min(1).describe("1-based"), endCharacter: tool.schema.number().min(0).describe("0-based"), kind: tool.schema .enum([ "quickfix", "refactor", "refactor.extract", "refactor.inline", "refactor.rewrite", "source", "source.organizeImports", "source.fixAll", ]) .optional() .describe("Filter by code action kind"), }, execute: async (args, context) => { try { const only = args.kind ? [args.kind] : undefined const result = await withLspClient(args.filePath, async (client) => { return (await client.codeAction( args.filePath, args.startLine, args.startCharacter, args.endLine, args.endCharacter, only )) as (CodeAction | Command)[] | null }) const output = formatCodeActions(result) return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, }) export const lsp_code_action_resolve: ToolDefinition = tool({ description: "Resolve and APPLY a code action from lsp_code_actions.", args: { filePath: tool.schema.string(), codeAction: tool.schema.string().describe("Code action JSON from lsp_code_actions"), }, execute: async (args, context) => { try { const codeAction = JSON.parse(args.codeAction) as CodeAction const resolved = await withLspClient(args.filePath, async (client) => { return (await client.codeActionResolve(codeAction)) as CodeAction | null }) if (!resolved) { const output = "Failed to resolve code action" return output } const lines: string[] = [] lines.push(`Action: ${resolved.title}`) if (resolved.kind) lines.push(`Kind: ${resolved.kind}`) if (resolved.edit) { const result = applyWorkspaceEdit(resolved.edit) lines.push(formatApplyResult(result)) } else { lines.push("No edit to apply") } if (resolved.command) { lines.push(`Command: ${resolved.command.title} (${resolved.command.command}) - not executed`) } const output = lines.join("\n") return output } catch (e) { const output = `Error: ${e instanceof Error ? e.message : String(e)}` return output } }, })