From 48167a69200966c16ec60d3591ee2caa719ace98 Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Fri, 16 Jan 2026 02:17:00 +0900 Subject: [PATCH] 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. --- src/hooks/tool-output-truncator.ts | 2 - src/tools/index.ts | 6 -- src/tools/lsp/client.ts | 67 ------------- src/tools/lsp/constants.ts | 31 ------ src/tools/lsp/tools.ts | 152 +---------------------------- src/tools/lsp/types.ts | 60 ------------ src/tools/lsp/utils.ts | 103 +------------------ 7 files changed, 3 insertions(+), 418 deletions(-) diff --git a/src/hooks/tool-output-truncator.ts b/src/hooks/tool-output-truncator.ts index 9fe7362d..c2837991 100644 --- a/src/hooks/tool-output-truncator.ts +++ b/src/hooks/tool-output-truncator.ts @@ -12,8 +12,6 @@ const TRUNCATABLE_TOOLS = [ "glob", "Glob", "safe_glob", - "lsp_find_references", - "lsp_symbols", "lsp_diagnostics", "ast_grep_search", "interactive_bash", diff --git a/src/tools/index.ts b/src/tools/index.ts index 405602bb..8b3ce735 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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 = { - lsp_goto_definition, - lsp_find_references, - lsp_symbols, lsp_diagnostics, lsp_servers, lsp_prepare_rename, diff --git a/src/tools/lsp/client.ts b/src/tools/lsp/client.ts index 449dce6f..329afb4a 100644 --- a/src/tools/lsp/client.ts +++ b/src/tools/lsp/client.ts @@ -509,46 +509,6 @@ export class LSPClient { await new Promise((r) => setTimeout(r, 1000)) } - async hover(filePath: string, line: number, character: number): Promise { - 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 { - 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 { - 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 { - const absPath = resolve(filePath) - await this.openFile(absPath) - return this.send("textDocument/documentSymbol", { - textDocument: { uri: pathToFileURL(absPath).href }, - }) - } - - async workspaceSymbols(query: string): Promise { - 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 { - 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 { - return this.send("codeAction/resolve", codeAction) - } - isAlive(): boolean { return this.proc !== null && !this.processExited && this.proc.exitCode === null } diff --git a/src/tools/lsp/constants.ts b/src/tools/lsp/constants.ts index d5aada38..9bc71835 100644 --- a/src/tools/lsp/constants.ts +++ b/src/tools/lsp/constants.ts @@ -1,34 +1,5 @@ import type { LSPServerConfig } from "./types" -export const SYMBOL_KIND_MAP: Record = { - 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 = { 1: "error", 2: "warning", @@ -36,8 +7,6 @@ export const SEVERITY_MAP: Record = { 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 = { diff --git a/src/tools/lsp/tools.ts b/src/tools/lsp/tools.ts index b0120c98..412f5db4 100644 --- a/src/tools/lsp/tools.ts +++ b/src/tools/lsp/tools.ts @@ -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.", diff --git a/src/tools/lsp/types.ts b/src/tools/lsp/types.ts index 895375d0..2b431193 100644 --- a/src/tools/lsp/types.ts +++ b/src/tools/lsp/types.ts @@ -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[] diff --git a/src/tools/lsp/utils.ts b/src/tools/lsp/utils.ts index 99956af1..810021f7 100644 --- a/src/tools/lsp/utils.ts +++ b/src/tools/lsp/utils.ts @@ -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(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[]