Files
oh-my-opencode-free-fork/src/tools/lsp/tools.ts
2025-12-27 18:56:40 +09:00

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
}
},
})