1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
|
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
}
|