2025-12-27 17:56:40 +08:00
import { tool , type ToolDefinition } from "@opencode-ai/plugin/tool"
2025-12-05 22:51:46 +09:00
import {
2026-01-16 17:11:34 +09:00
DEFAULT_MAX_REFERENCES ,
DEFAULT_MAX_SYMBOLS ,
2025-12-05 22:51:46 +09:00
DEFAULT_MAX_DIAGNOSTICS ,
} from "./constants"
2025-12-04 21:54:02 +09:00
import {
withLspClient ,
2026-01-16 17:11:34 +09:00
formatLocation ,
formatDocumentSymbol ,
formatSymbolInfo ,
2025-12-04 21:54:02 +09:00
formatDiagnostic ,
filterDiagnosticsBySeverity ,
2025-12-04 22:40:05 +09:00
formatPrepareRenameResult ,
applyWorkspaceEdit ,
formatApplyResult ,
2025-12-04 21:54:02 +09:00
} from "./utils"
2025-12-04 22:40:05 +09:00
import type {
2026-01-16 17:11:34 +09:00
Location ,
LocationLink ,
DocumentSymbol ,
SymbolInfo ,
2025-12-04 22:40:05 +09:00
Diagnostic ,
PrepareRenameResult ,
PrepareRenameDefaultBehavior ,
WorkspaceEdit ,
} from "./types"
2026-01-16 17:11:34 +09:00
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 ) } `
}
} ,
} )
2025-12-04 21:54:02 +09:00
2025-12-27 17:56:40 +08:00
export const lsp_diagnostics : ToolDefinition = tool ( {
2025-12-17 00:16:21 +09:00
description : "Get errors, warnings, hints from language server BEFORE running build." ,
2025-12-04 21:54:02 +09:00
args : {
2025-12-17 00:16:21 +09:00
filePath : tool.schema.string ( ) ,
2025-12-04 21:54:02 +09:00
severity : tool.schema
. enum ( [ "error" , "warning" , "information" , "hint" , "all" ] )
. optional ( )
. describe ( "Filter by severity level" ) ,
} ,
2025-12-04 22:40:05 +09:00
execute : async ( args , context ) = > {
2025-12-04 21:54:02 +09:00
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 )
2025-12-04 22:40:05 +09:00
if ( diagnostics . length === 0 ) {
const output = "No diagnostics found"
return output
}
2025-12-04 21:54:02 +09:00
2025-12-05 22:51:46 +09:00
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" )
2025-12-04 22:40:05 +09:00
return output
2025-12-04 21:54:02 +09:00
} catch ( e ) {
2025-12-04 22:40:05 +09:00
const output = ` Error: ${ e instanceof Error ? e.message : String ( e ) } `
2026-01-17 18:14:50 +09:00
throw new Error ( output )
2025-12-04 21:54:02 +09:00
}
} ,
} )
2025-12-27 17:56:40 +08:00
export const lsp_prepare_rename : ToolDefinition = tool ( {
2025-12-17 00:16:21 +09:00
description : "Check if rename is valid. Use BEFORE lsp_rename." ,
2025-12-04 22:40:05 +09:00
args : {
2025-12-17 00:16:21 +09:00
filePath : tool.schema.string ( ) ,
line : tool.schema.number ( ) . min ( 1 ) . describe ( "1-based" ) ,
character : tool.schema.number ( ) . min ( 0 ) . describe ( "0-based" ) ,
2025-12-04 22:40:05 +09:00
} ,
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
}
} ,
} )
2025-12-27 17:56:40 +08:00
export const lsp_rename : ToolDefinition = tool ( {
2025-12-17 00:16:21 +09:00
description : "Rename symbol across entire workspace. APPLIES changes to all files." ,
2025-12-04 22:40:05 +09:00
args : {
2025-12-17 00:16:21 +09:00
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" ) ,
2025-12-04 22:40:05 +09:00
} ,
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
}
} ,
} )