| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- 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
- }
|