98e91e508c
- Changed lsp_diagnostics error handling to throw errors instead of returning strings - Line 211: Changed from `return output` to `throw new Error(output)` - Makes errors display as proper error blocks in TUI Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
262 lines
8.7 KiB
TypeScript
262 lines
8.7 KiB
TypeScript
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
|
|
import {
|
|
DEFAULT_MAX_REFERENCES,
|
|
DEFAULT_MAX_SYMBOLS,
|
|
DEFAULT_MAX_DIAGNOSTICS,
|
|
} from "./constants"
|
|
import {
|
|
withLspClient,
|
|
formatLocation,
|
|
formatDocumentSymbol,
|
|
formatSymbolInfo,
|
|
formatDiagnostic,
|
|
filterDiagnosticsBySeverity,
|
|
formatPrepareRenameResult,
|
|
applyWorkspaceEdit,
|
|
formatApplyResult,
|
|
} from "./utils"
|
|
import type {
|
|
Location,
|
|
LocationLink,
|
|
DocumentSymbol,
|
|
SymbolInfo,
|
|
Diagnostic,
|
|
PrepareRenameResult,
|
|
PrepareRenameDefaultBehavior,
|
|
WorkspaceEdit,
|
|
} from "./types"
|
|
|
|
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_symbols: ToolDefinition = tool({
|
|
description: "Get symbols from file (document) or search across workspace. Use scope='document' for file outline, scope='workspace' for project-wide symbol search.",
|
|
args: {
|
|
filePath: tool.schema.string().describe("File path for LSP context"),
|
|
scope: tool.schema.enum(["document", "workspace"]).default("document").describe("'document' for file symbols, 'workspace' for project-wide search"),
|
|
query: tool.schema.string().optional().describe("Symbol name to search (required for workspace scope)"),
|
|
limit: tool.schema.number().optional().describe("Max results (default 50)"),
|
|
},
|
|
execute: async (args, context) => {
|
|
try {
|
|
const scope = args.scope ?? "document"
|
|
|
|
if (scope === "workspace") {
|
|
if (!args.query) {
|
|
return "Error: 'query' is required for workspace scope"
|
|
}
|
|
|
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
return (await client.workspaceSymbols(args.query!)) as SymbolInfo[] | null
|
|
})
|
|
|
|
if (!result || result.length === 0) {
|
|
return "No symbols found"
|
|
}
|
|
|
|
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}):`)
|
|
}
|
|
return lines.join("\n")
|
|
} else {
|
|
const result = await withLspClient(args.filePath, async (client) => {
|
|
return (await client.documentSymbols(args.filePath)) as DocumentSymbol[] | SymbolInfo[] | null
|
|
})
|
|
|
|
if (!result || result.length === 0) {
|
|
return "No symbols found"
|
|
}
|
|
|
|
const total = result.length
|
|
const limit = Math.min(args.limit ?? DEFAULT_MAX_SYMBOLS, DEFAULT_MAX_SYMBOLS)
|
|
const truncated = total > limit
|
|
const limited = truncated ? result.slice(0, limit) : result
|
|
|
|
const lines: string[] = []
|
|
if (truncated) {
|
|
lines.push(`Found ${total} symbols (showing first ${limit}):`)
|
|
}
|
|
|
|
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) {
|
|
return `Error: ${e instanceof Error ? e.message : String(e)}`
|
|
}
|
|
},
|
|
})
|
|
|
|
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)}`
|
|
throw new Error(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
|
|
}
|
|
},
|
|
})
|