package tools import ( "context" "encoding/json" "fmt" "strings" "github.com/sst/opencode/internal/lsp" "github.com/sst/opencode/internal/lsp/protocol" "github.com/sst/opencode/internal/lsp/util" ) type CodeActionParams struct { FilePath string `json:"file_path"` Line int `json:"line"` Column int `json:"column"` EndLine int `json:"end_line,omitempty"` EndColumn int `json:"end_column,omitempty"` ActionID int `json:"action_id,omitempty"` LspName string `json:"lsp_name,omitempty"` } type codeActionTool struct { lspClients map[string]*lsp.Client } const ( CodeActionToolName = "codeAction" codeActionDescription = `Get available code actions at a specific position or range in a file. WHEN TO USE THIS TOOL: - Use when you need to find available fixes or refactorings for code issues - Helpful for resolving errors, warnings, or improving code quality - Great for discovering automated code transformations HOW TO USE: - Provide the path to the file containing the code - Specify the line number (1-based) where the action should be applied - Specify the column number (1-based) where the action should be applied - Optionally specify end_line and end_column to define a range - Results show available code actions with their titles and kinds TO EXECUTE A CODE ACTION: - After getting the list of available actions, call the tool again with the same parameters - Add action_id parameter with the number of the action you want to execute (e.g., 1 for the first action) - Add lsp_name parameter with the name of the LSP server that provided the action FEATURES: - Finds quick fixes for errors and warnings - Discovers available refactorings - Shows code organization actions - Returns detailed information about each action - Can execute selected code actions LIMITATIONS: - Requires a functioning LSP server for the file type - May not work for all code issues depending on LSP capabilities - Results depend on the accuracy of the LSP server TIPS: - Use in conjunction with Diagnostics tool to find issues that can be fixed - First call without action_id to see available actions, then call again with action_id to execute ` ) func NewCodeActionTool(lspClients map[string]*lsp.Client) BaseTool { return &codeActionTool{ lspClients, } } func (b *codeActionTool) Info() ToolInfo { return ToolInfo{ Name: CodeActionToolName, Description: codeActionDescription, Parameters: map[string]any{ "file_path": map[string]any{ "type": "string", "description": "The path to the file containing the code", }, "line": map[string]any{ "type": "integer", "description": "The line number (1-based) where the action should be applied", }, "column": map[string]any{ "type": "integer", "description": "The column number (1-based) where the action should be applied", }, "end_line": map[string]any{ "type": "integer", "description": "The ending line number (1-based) for a range (optional)", }, "end_column": map[string]any{ "type": "integer", "description": "The ending column number (1-based) for a range (optional)", }, "action_id": map[string]any{ "type": "integer", "description": "The ID of the code action to execute (optional)", }, "lsp_name": map[string]any{ "type": "string", "description": "The name of the LSP server that provided the action (optional)", }, }, Required: []string{"file_path", "line", "column"}, } } func (b *codeActionTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { var params CodeActionParams if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil } lsps := b.lspClients if len(lsps) == 0 { return NewTextResponse("\nLSP clients are still initializing. Code actions will be available once they're ready.\n"), nil } // Ensure file is open in LSP notifyLspOpenFile(ctx, params.FilePath, lsps) // Convert 1-based line/column to 0-based for LSP protocol line := max(0, params.Line-1) column := max(0, params.Column-1) // Handle optional end line/column endLine := line endColumn := column if params.EndLine > 0 { endLine = max(0, params.EndLine-1) } if params.EndColumn > 0 { endColumn = max(0, params.EndColumn-1) } // Check if we're executing a specific action if params.ActionID > 0 && params.LspName != "" { return executeCodeAction(ctx, params.FilePath, line, column, endLine, endColumn, params.ActionID, params.LspName, lsps) } // Otherwise, just list available actions output := getCodeActions(ctx, params.FilePath, line, column, endLine, endColumn, lsps) return NewTextResponse(output), nil } func getCodeActions(ctx context.Context, filePath string, line, column, endLine, endColumn int, lsps map[string]*lsp.Client) string { var results []string for lspName, client := range lsps { // Create code action params uri := fmt.Sprintf("file://%s", filePath) codeActionParams := protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.DocumentUri(uri), }, Range: protocol.Range{ Start: protocol.Position{ Line: uint32(line), Character: uint32(column), }, End: protocol.Position{ Line: uint32(endLine), Character: uint32(endColumn), }, }, Context: protocol.CodeActionContext{ // Request all kinds of code actions Only: []protocol.CodeActionKind{ protocol.QuickFix, protocol.Refactor, protocol.RefactorExtract, protocol.RefactorInline, protocol.RefactorRewrite, protocol.Source, protocol.SourceOrganizeImports, protocol.SourceFixAll, }, }, } // Get code actions codeActions, err := client.CodeAction(ctx, codeActionParams) if err != nil { results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) continue } if len(codeActions) == 0 { results = append(results, fmt.Sprintf("No code actions found by %s", lspName)) continue } // Format the code actions results = append(results, fmt.Sprintf("Code actions found by %s:", lspName)) for i, action := range codeActions { actionInfo := formatCodeAction(action, i+1) results = append(results, actionInfo) } } if len(results) == 0 { return "No code actions found at the specified position." } return strings.Join(results, "\n") } func formatCodeAction(action protocol.Or_Result_textDocument_codeAction_Item0_Elem, index int) string { switch v := action.Value.(type) { case protocol.CodeAction: kind := "Unknown" if v.Kind != "" { kind = string(v.Kind) } var details []string // Add edit information if available if v.Edit != nil { numChanges := 0 if v.Edit.Changes != nil { numChanges = len(v.Edit.Changes) } if v.Edit.DocumentChanges != nil { numChanges = len(v.Edit.DocumentChanges) } details = append(details, fmt.Sprintf("Edits: %d changes", numChanges)) } // Add command information if available if v.Command != nil { details = append(details, fmt.Sprintf("Command: %s", v.Command.Title)) } // Add diagnostics information if available if v.Diagnostics != nil && len(v.Diagnostics) > 0 { details = append(details, fmt.Sprintf("Fixes: %d diagnostics", len(v.Diagnostics))) } detailsStr := "" if len(details) > 0 { detailsStr = " (" + strings.Join(details, ", ") + ")" } return fmt.Sprintf(" %d. %s [%s]%s", index, v.Title, kind, detailsStr) case protocol.Command: return fmt.Sprintf(" %d. %s [Command]", index, v.Title) } return fmt.Sprintf(" %d. Unknown code action type", index) } func executeCodeAction(ctx context.Context, filePath string, line, column, endLine, endColumn, actionID int, lspName string, lsps map[string]*lsp.Client) (ToolResponse, error) { client, ok := lsps[lspName] if !ok { return NewTextErrorResponse(fmt.Sprintf("LSP server '%s' not found", lspName)), nil } // Create code action params uri := fmt.Sprintf("file://%s", filePath) codeActionParams := protocol.CodeActionParams{ TextDocument: protocol.TextDocumentIdentifier{ URI: protocol.DocumentUri(uri), }, Range: protocol.Range{ Start: protocol.Position{ Line: uint32(line), Character: uint32(column), }, End: protocol.Position{ Line: uint32(endLine), Character: uint32(endColumn), }, }, Context: protocol.CodeActionContext{ // Request all kinds of code actions Only: []protocol.CodeActionKind{ protocol.QuickFix, protocol.Refactor, protocol.RefactorExtract, protocol.RefactorInline, protocol.RefactorRewrite, protocol.Source, protocol.SourceOrganizeImports, protocol.SourceFixAll, }, }, } // Get code actions codeActions, err := client.CodeAction(ctx, codeActionParams) if err != nil { return NewTextErrorResponse(fmt.Sprintf("Error getting code actions: %s", err)), nil } if len(codeActions) == 0 { return NewTextErrorResponse("No code actions found"), nil } // Check if the requested action ID is valid if actionID < 1 || actionID > len(codeActions) { return NewTextErrorResponse(fmt.Sprintf("Invalid action ID: %d. Available actions: 1-%d", actionID, len(codeActions))), nil } // Get the selected action (adjust for 0-based index) selectedAction := codeActions[actionID-1] // Execute the action based on its type switch v := selectedAction.Value.(type) { case protocol.CodeAction: // Apply workspace edit if available if v.Edit != nil { err := util.ApplyWorkspaceEdit(*v.Edit) if err != nil { return NewTextErrorResponse(fmt.Sprintf("Error applying edit: %s", err)), nil } } // Execute command if available if v.Command != nil { _, err := client.ExecuteCommand(ctx, protocol.ExecuteCommandParams{ Command: v.Command.Command, Arguments: v.Command.Arguments, }) if err != nil { return NewTextErrorResponse(fmt.Sprintf("Error executing command: %s", err)), nil } } return NewTextResponse(fmt.Sprintf("Successfully executed code action: %s", v.Title)), nil case protocol.Command: // Execute the command _, err := client.ExecuteCommand(ctx, protocol.ExecuteCommandParams{ Command: v.Command, Arguments: v.Arguments, }) if err != nil { return NewTextErrorResponse(fmt.Sprintf("Error executing command: %s", err)), nil } return NewTextResponse(fmt.Sprintf("Successfully executed command: %s", v.Title)), nil } return NewTextErrorResponse("Unknown code action type"), nil }