406 lines
13 KiB
TypeScript
406 lines
13 KiB
TypeScript
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
|
|
}
|
|
},
|
|
})
|