refactor(lsp): remove duplicate LSP tools already provided by OpenCode

Remove lsp_goto_definition, lsp_find_references, lsp_symbols as they
duplicate OpenCode's built-in LspGotoDefinition, LspFindReferences,
LspDocumentSymbols, and LspWorkspaceSymbols.

Keep oh-my-opencode-specific tools:
- lsp_diagnostics (OpenCode lacks pull diagnostics)
- lsp_servers (server management)
- lsp_prepare_rename / lsp_rename (OpenCode lacks rename)

Clean up associated dead code:
- Client methods: hover, definition, references, symbols, codeAction
- Utils: formatLocation, formatSymbolKind, formatDocumentSymbol, etc.
- Types: Location, LocationLink, SymbolInfo, DocumentSymbol, etc.
- Constants: SYMBOL_KIND_MAP, DEFAULT_MAX_REFERENCES/SYMBOLS

-418 lines removed.
This commit is contained in:
justsisyphus
2026-01-16 02:17:00 +09:00
parent 207a39b17a
commit 48167a6920
7 changed files with 3 additions and 418 deletions

View File

@@ -12,8 +12,6 @@ const TRUNCATABLE_TOOLS = [
"glob",
"Glob",
"safe_glob",
"lsp_find_references",
"lsp_symbols",
"lsp_diagnostics",
"ast_grep_search",
"interactive_bash",

View File

@@ -1,7 +1,4 @@
import {
lsp_goto_definition,
lsp_find_references,
lsp_symbols,
lsp_diagnostics,
lsp_servers,
lsp_prepare_rename,
@@ -56,9 +53,6 @@ export function createBackgroundTools(manager: BackgroundManager, client: Openco
}
export const builtinTools: Record<string, ToolDefinition> = {
lsp_goto_definition,
lsp_find_references,
lsp_symbols,
lsp_diagnostics,
lsp_servers,
lsp_prepare_rename,

View File

@@ -509,46 +509,6 @@ export class LSPClient {
await new Promise((r) => setTimeout(r, 1000))
}
async hover(filePath: string, line: number, character: number): Promise<unknown> {
const absPath = resolve(filePath)
await this.openFile(absPath)
return this.send("textDocument/hover", {
textDocument: { uri: pathToFileURL(absPath).href },
position: { line: line - 1, character },
})
}
async definition(filePath: string, line: number, character: number): Promise<unknown> {
const absPath = resolve(filePath)
await this.openFile(absPath)
return this.send("textDocument/definition", {
textDocument: { uri: pathToFileURL(absPath).href },
position: { line: line - 1, character },
})
}
async references(filePath: string, line: number, character: number, includeDeclaration = true): Promise<unknown> {
const absPath = resolve(filePath)
await this.openFile(absPath)
return this.send("textDocument/references", {
textDocument: { uri: pathToFileURL(absPath).href },
position: { line: line - 1, character },
context: { includeDeclaration },
})
}
async documentSymbols(filePath: string): Promise<unknown> {
const absPath = resolve(filePath)
await this.openFile(absPath)
return this.send("textDocument/documentSymbol", {
textDocument: { uri: pathToFileURL(absPath).href },
})
}
async workspaceSymbols(query: string): Promise<unknown> {
return this.send("workspace/symbol", { query })
}
async diagnostics(filePath: string): Promise<{ items: Diagnostic[] }> {
const absPath = resolve(filePath)
const uri = pathToFileURL(absPath).href
@@ -587,33 +547,6 @@ export class LSPClient {
})
}
async codeAction(
filePath: string,
startLine: number,
startChar: number,
endLine: number,
endChar: number,
only?: string[]
): Promise<unknown> {
const absPath = resolve(filePath)
await this.openFile(absPath)
return this.send("textDocument/codeAction", {
textDocument: { uri: pathToFileURL(absPath).href },
range: {
start: { line: startLine - 1, character: startChar },
end: { line: endLine - 1, character: endChar },
},
context: {
diagnostics: [],
only,
},
})
}
async codeActionResolve(codeAction: unknown): Promise<unknown> {
return this.send("codeAction/resolve", codeAction)
}
isAlive(): boolean {
return this.proc !== null && !this.processExited && this.proc.exitCode === null
}

View File

@@ -1,34 +1,5 @@
import type { LSPServerConfig } from "./types"
export const SYMBOL_KIND_MAP: Record<number, string> = {
1: "File",
2: "Module",
3: "Namespace",
4: "Package",
5: "Class",
6: "Method",
7: "Property",
8: "Field",
9: "Constructor",
10: "Enum",
11: "Interface",
12: "Function",
13: "Variable",
14: "Constant",
15: "String",
16: "Number",
17: "Boolean",
18: "Array",
19: "Object",
20: "Key",
21: "Null",
22: "EnumMember",
23: "Struct",
24: "Event",
25: "Operator",
26: "TypeParameter",
}
export const SEVERITY_MAP: Record<number, string> = {
1: "error",
2: "warning",
@@ -36,8 +7,6 @@ export const SEVERITY_MAP: Record<number, string> = {
4: "hint",
}
export const DEFAULT_MAX_REFERENCES = 200
export const DEFAULT_MAX_SYMBOLS = 200
export const DEFAULT_MAX_DIAGNOSTICS = 200
export const LSP_INSTALL_HINTS: Record<string, string> = {

View File

@@ -1,15 +1,10 @@
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,
formatLocation,
formatDocumentSymbol,
formatSymbolInfo,
formatDiagnostic,
filterDiagnosticsBySeverity,
formatPrepareRenameResult,
@@ -17,157 +12,14 @@ import {
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)}`
}
},
})
// NOTE: lsp_goto_definition, lsp_find_references, lsp_symbols are removed
// as they duplicate OpenCode's built-in LSP tools (LspGotoDefinition, LspFindReferences, LspDocumentSymbols, LspWorkspaceSymbols)
export const lsp_diagnostics: ToolDefinition = tool({
description: "Get errors, warnings, hints from language server BEFORE running build.",

View File

@@ -17,33 +17,6 @@ export interface Range {
end: Position
}
export interface Location {
uri: string
range: Range
}
export interface LocationLink {
targetUri: string
targetRange: Range
targetSelectionRange: Range
originSelectionRange?: Range
}
export interface SymbolInfo {
name: string
kind: number
location: Location
containerName?: string
}
export interface DocumentSymbol {
name: string
kind: number
range: Range
selectionRange: Range
children?: DocumentSymbol[]
}
export interface Diagnostic {
range: Range
severity?: number
@@ -52,14 +25,6 @@ export interface Diagnostic {
message: string
}
export interface HoverResult {
contents:
| { kind?: string; value: string }
| string
| Array<{ kind?: string; value: string } | string>
range?: Range
}
export interface TextDocumentIdentifier {
uri: string
}
@@ -111,31 +76,6 @@ export interface PrepareRenameDefaultBehavior {
defaultBehavior: boolean
}
export interface Command {
title: string
command: string
arguments?: unknown[]
}
export interface CodeActionContext {
diagnostics: Diagnostic[]
only?: string[]
triggerKind?: CodeActionTriggerKind
}
export type CodeActionTriggerKind = 1 | 2
export interface CodeAction {
title: string
kind?: string
diagnostics?: Diagnostic[]
isPreferred?: boolean
disabled?: { reason: string }
edit?: WorkspaceEdit
command?: Command
data?: unknown
}
export interface ServerLookupInfo {
id: string
command: string[]

View File

@@ -3,21 +3,14 @@ import { fileURLToPath } from "node:url"
import { existsSync, readFileSync, writeFileSync } from "fs"
import { LSPClient, lspManager } from "./client"
import { findServerForExtension } from "./config"
import { SYMBOL_KIND_MAP, SEVERITY_MAP } from "./constants"
import { SEVERITY_MAP } from "./constants"
import type {
HoverResult,
DocumentSymbol,
SymbolInfo,
Location,
LocationLink,
Diagnostic,
PrepareRenameResult,
PrepareRenameDefaultBehavior,
Range,
WorkspaceEdit,
TextEdit,
CodeAction,
Command,
ServerLookupResult,
} from "./types"
@@ -113,73 +106,11 @@ export async function withLspClient<T>(filePath: string, fn: (client: LSPClient)
}
}
export function formatHoverResult(result: HoverResult | null): string {
if (!result) return "No hover information available"
const contents = result.contents
if (typeof contents === "string") {
return contents
}
if (Array.isArray(contents)) {
return contents
.map((c) => (typeof c === "string" ? c : c.value))
.filter(Boolean)
.join("\n\n")
}
if (typeof contents === "object" && "value" in contents) {
return contents.value
}
return "No hover information available"
}
export function formatLocation(loc: Location | LocationLink): string {
if ("targetUri" in loc) {
const uri = uriToPath(loc.targetUri)
const line = loc.targetRange.start.line + 1
const char = loc.targetRange.start.character
return `${uri}:${line}:${char}`
}
const uri = uriToPath(loc.uri)
const line = loc.range.start.line + 1
const char = loc.range.start.character
return `${uri}:${line}:${char}`
}
export function formatSymbolKind(kind: number): string {
return SYMBOL_KIND_MAP[kind] || `Unknown(${kind})`
}
export function formatSeverity(severity: number | undefined): string {
if (!severity) return "unknown"
return SEVERITY_MAP[severity] || `unknown(${severity})`
}
export function formatDocumentSymbol(symbol: DocumentSymbol, indent = 0): string {
const prefix = " ".repeat(indent)
const kind = formatSymbolKind(symbol.kind)
const line = symbol.range.start.line + 1
let result = `${prefix}${symbol.name} (${kind}) - line ${line}`
if (symbol.children && symbol.children.length > 0) {
for (const child of symbol.children) {
result += "\n" + formatDocumentSymbol(child, indent + 1)
}
}
return result
}
export function formatSymbolInfo(symbol: SymbolInfo): string {
const kind = formatSymbolKind(symbol.kind)
const loc = formatLocation(symbol.location)
const container = symbol.containerName ? ` (in ${symbol.containerName})` : ""
return `${symbol.name} (${kind})${container} - ${loc}`
}
export function formatDiagnostic(diag: Diagnostic): string {
const severity = formatSeverity(diag.severity)
const line = diag.range.start.line + 1
@@ -292,38 +223,6 @@ export function formatWorkspaceEdit(edit: WorkspaceEdit | null): string {
return lines.join("\n")
}
export function formatCodeAction(action: CodeAction): string {
let result = `[${action.kind || "action"}] ${action.title}`
if (action.isPreferred) {
result += " ⭐"
}
if (action.disabled) {
result += ` (disabled: ${action.disabled.reason})`
}
return result
}
export function formatCodeActions(actions: (CodeAction | Command)[] | null): string {
if (!actions || actions.length === 0) return "No code actions available"
const lines: string[] = []
for (let i = 0; i < actions.length; i++) {
const action = actions[i]
if ("command" in action && typeof action.command === "string" && !("kind" in action)) {
lines.push(`${i + 1}. [command] ${(action as Command).title}`)
} else {
lines.push(`${i + 1}. ${formatCodeAction(action as CodeAction)}`)
}
}
return lines.join("\n")
}
export interface ApplyResult {
success: boolean
filesModified: string[]