1+ package tools
2+
3+ import (
4+ "context"
5+ "encoding/json"
6+ "fmt"
7+ "strings"
8+
9+ "github.com/sst/opencode/internal/lsp"
10+ "github.com/sst/opencode/internal/lsp/protocol"
11+ "github.com/sst/opencode/internal/lsp/util"
12+ )
13+
14+ type CodeActionParams struct {
15+ FilePath string `json:"file_path"`
16+ Line int `json:"line"`
17+ Column int `json:"column"`
18+ EndLine int `json:"end_line,omitempty"`
19+ EndColumn int `json:"end_column,omitempty"`
20+ ActionID int `json:"action_id,omitempty"`
21+ LspName string `json:"lsp_name,omitempty"`
22+ }
23+
24+ type codeActionTool struct {
25+ lspClients map [string ]* lsp.Client
26+ }
27+
28+ const (
29+ CodeActionToolName = "codeAction"
30+ codeActionDescription = `Get available code actions at a specific position or range in a file.
31+ WHEN TO USE THIS TOOL:
32+ - Use when you need to find available fixes or refactorings for code issues
33+ - Helpful for resolving errors, warnings, or improving code quality
34+ - Great for discovering automated code transformations
35+
36+ HOW TO USE:
37+ - Provide the path to the file containing the code
38+ - Specify the line number (1-based) where the action should be applied
39+ - Specify the column number (1-based) where the action should be applied
40+ - Optionally specify end_line and end_column to define a range
41+ - Results show available code actions with their titles and kinds
42+
43+ TO EXECUTE A CODE ACTION:
44+ - After getting the list of available actions, call the tool again with the same parameters
45+ - Add action_id parameter with the number of the action you want to execute (e.g., 1 for the first action)
46+ - Add lsp_name parameter with the name of the LSP server that provided the action
47+
48+ FEATURES:
49+ - Finds quick fixes for errors and warnings
50+ - Discovers available refactorings
51+ - Shows code organization actions
52+ - Returns detailed information about each action
53+ - Can execute selected code actions
54+
55+ LIMITATIONS:
56+ - Requires a functioning LSP server for the file type
57+ - May not work for all code issues depending on LSP capabilities
58+ - Results depend on the accuracy of the LSP server
59+
60+ TIPS:
61+ - Use in conjunction with Diagnostics tool to find issues that can be fixed
62+ - First call without action_id to see available actions, then call again with action_id to execute
63+ `
64+ )
65+
66+ func NewCodeActionTool (lspClients map [string ]* lsp.Client ) BaseTool {
67+ return & codeActionTool {
68+ lspClients ,
69+ }
70+ }
71+
72+ func (b * codeActionTool ) Info () ToolInfo {
73+ return ToolInfo {
74+ Name : CodeActionToolName ,
75+ Description : codeActionDescription ,
76+ Parameters : map [string ]any {
77+ "file_path" : map [string ]any {
78+ "type" : "string" ,
79+ "description" : "The path to the file containing the code" ,
80+ },
81+ "line" : map [string ]any {
82+ "type" : "integer" ,
83+ "description" : "The line number (1-based) where the action should be applied" ,
84+ },
85+ "column" : map [string ]any {
86+ "type" : "integer" ,
87+ "description" : "The column number (1-based) where the action should be applied" ,
88+ },
89+ "end_line" : map [string ]any {
90+ "type" : "integer" ,
91+ "description" : "The ending line number (1-based) for a range (optional)" ,
92+ },
93+ "end_column" : map [string ]any {
94+ "type" : "integer" ,
95+ "description" : "The ending column number (1-based) for a range (optional)" ,
96+ },
97+ "action_id" : map [string ]any {
98+ "type" : "integer" ,
99+ "description" : "The ID of the code action to execute (optional)" ,
100+ },
101+ "lsp_name" : map [string ]any {
102+ "type" : "string" ,
103+ "description" : "The name of the LSP server that provided the action (optional)" ,
104+ },
105+ },
106+ Required : []string {"file_path" , "line" , "column" },
107+ }
108+ }
109+
110+ func (b * codeActionTool ) Run (ctx context.Context , call ToolCall ) (ToolResponse , error ) {
111+ var params CodeActionParams
112+ if err := json .Unmarshal ([]byte (call .Input ), & params ); err != nil {
113+ return NewTextErrorResponse (fmt .Sprintf ("error parsing parameters: %s" , err )), nil
114+ }
115+
116+ lsps := b .lspClients
117+
118+ if len (lsps ) == 0 {
119+ return NewTextResponse ("\n LSP clients are still initializing. Code actions will be available once they're ready.\n " ), nil
120+ }
121+
122+ // Ensure file is open in LSP
123+ notifyLspOpenFile (ctx , params .FilePath , lsps )
124+
125+ // Convert 1-based line/column to 0-based for LSP protocol
126+ line := max (0 , params .Line - 1 )
127+ column := max (0 , params .Column - 1 )
128+
129+ // Handle optional end line/column
130+ endLine := line
131+ endColumn := column
132+ if params .EndLine > 0 {
133+ endLine = max (0 , params .EndLine - 1 )
134+ }
135+ if params .EndColumn > 0 {
136+ endColumn = max (0 , params .EndColumn - 1 )
137+ }
138+
139+ // Check if we're executing a specific action
140+ if params .ActionID > 0 && params .LspName != "" {
141+ return executeCodeAction (ctx , params .FilePath , line , column , endLine , endColumn , params .ActionID , params .LspName , lsps )
142+ }
143+
144+ // Otherwise, just list available actions
145+ output := getCodeActions (ctx , params .FilePath , line , column , endLine , endColumn , lsps )
146+ return NewTextResponse (output ), nil
147+ }
148+
149+ func getCodeActions (ctx context.Context , filePath string , line , column , endLine , endColumn int , lsps map [string ]* lsp.Client ) string {
150+ var results []string
151+
152+ for lspName , client := range lsps {
153+ // Create code action params
154+ uri := fmt .Sprintf ("file://%s" , filePath )
155+ codeActionParams := protocol.CodeActionParams {
156+ TextDocument : protocol.TextDocumentIdentifier {
157+ URI : protocol .DocumentUri (uri ),
158+ },
159+ Range : protocol.Range {
160+ Start : protocol.Position {
161+ Line : uint32 (line ),
162+ Character : uint32 (column ),
163+ },
164+ End : protocol.Position {
165+ Line : uint32 (endLine ),
166+ Character : uint32 (endColumn ),
167+ },
168+ },
169+ Context : protocol.CodeActionContext {
170+ // Request all kinds of code actions
171+ Only : []protocol.CodeActionKind {
172+ protocol .QuickFix ,
173+ protocol .Refactor ,
174+ protocol .RefactorExtract ,
175+ protocol .RefactorInline ,
176+ protocol .RefactorRewrite ,
177+ protocol .Source ,
178+ protocol .SourceOrganizeImports ,
179+ protocol .SourceFixAll ,
180+ },
181+ },
182+ }
183+
184+ // Get code actions
185+ codeActions , err := client .CodeAction (ctx , codeActionParams )
186+ if err != nil {
187+ results = append (results , fmt .Sprintf ("Error from %s: %s" , lspName , err ))
188+ continue
189+ }
190+
191+ if len (codeActions ) == 0 {
192+ results = append (results , fmt .Sprintf ("No code actions found by %s" , lspName ))
193+ continue
194+ }
195+
196+ // Format the code actions
197+ results = append (results , fmt .Sprintf ("Code actions found by %s:" , lspName ))
198+ for i , action := range codeActions {
199+ actionInfo := formatCodeAction (action , i + 1 )
200+ results = append (results , actionInfo )
201+ }
202+ }
203+
204+ if len (results ) == 0 {
205+ return "No code actions found at the specified position."
206+ }
207+
208+ return strings .Join (results , "\n " )
209+ }
210+
211+ func formatCodeAction (action protocol.Or_Result_textDocument_codeAction_Item0_Elem , index int ) string {
212+ switch v := action .Value .(type ) {
213+ case protocol.CodeAction :
214+ kind := "Unknown"
215+ if v .Kind != "" {
216+ kind = string (v .Kind )
217+ }
218+
219+ var details []string
220+
221+ // Add edit information if available
222+ if v .Edit != nil {
223+ numChanges := 0
224+ if v .Edit .Changes != nil {
225+ numChanges = len (v .Edit .Changes )
226+ }
227+ if v .Edit .DocumentChanges != nil {
228+ numChanges = len (v .Edit .DocumentChanges )
229+ }
230+ details = append (details , fmt .Sprintf ("Edits: %d changes" , numChanges ))
231+ }
232+
233+ // Add command information if available
234+ if v .Command != nil {
235+ details = append (details , fmt .Sprintf ("Command: %s" , v .Command .Title ))
236+ }
237+
238+ // Add diagnostics information if available
239+ if v .Diagnostics != nil && len (v .Diagnostics ) > 0 {
240+ details = append (details , fmt .Sprintf ("Fixes: %d diagnostics" , len (v .Diagnostics )))
241+ }
242+
243+ detailsStr := ""
244+ if len (details ) > 0 {
245+ detailsStr = " (" + strings .Join (details , ", " ) + ")"
246+ }
247+
248+ return fmt .Sprintf (" %d. %s [%s]%s" , index , v .Title , kind , detailsStr )
249+
250+ case protocol.Command :
251+ return fmt .Sprintf (" %d. %s [Command]" , index , v .Title )
252+ }
253+
254+ return fmt .Sprintf (" %d. Unknown code action type" , index )
255+ }
256+
257+ func executeCodeAction (ctx context.Context , filePath string , line , column , endLine , endColumn , actionID int , lspName string , lsps map [string ]* lsp.Client ) (ToolResponse , error ) {
258+ client , ok := lsps [lspName ]
259+ if ! ok {
260+ return NewTextErrorResponse (fmt .Sprintf ("LSP server '%s' not found" , lspName )), nil
261+ }
262+
263+ // Create code action params
264+ uri := fmt .Sprintf ("file://%s" , filePath )
265+ codeActionParams := protocol.CodeActionParams {
266+ TextDocument : protocol.TextDocumentIdentifier {
267+ URI : protocol .DocumentUri (uri ),
268+ },
269+ Range : protocol.Range {
270+ Start : protocol.Position {
271+ Line : uint32 (line ),
272+ Character : uint32 (column ),
273+ },
274+ End : protocol.Position {
275+ Line : uint32 (endLine ),
276+ Character : uint32 (endColumn ),
277+ },
278+ },
279+ Context : protocol.CodeActionContext {
280+ // Request all kinds of code actions
281+ Only : []protocol.CodeActionKind {
282+ protocol .QuickFix ,
283+ protocol .Refactor ,
284+ protocol .RefactorExtract ,
285+ protocol .RefactorInline ,
286+ protocol .RefactorRewrite ,
287+ protocol .Source ,
288+ protocol .SourceOrganizeImports ,
289+ protocol .SourceFixAll ,
290+ },
291+ },
292+ }
293+
294+ // Get code actions
295+ codeActions , err := client .CodeAction (ctx , codeActionParams )
296+ if err != nil {
297+ return NewTextErrorResponse (fmt .Sprintf ("Error getting code actions: %s" , err )), nil
298+ }
299+
300+ if len (codeActions ) == 0 {
301+ return NewTextErrorResponse ("No code actions found" ), nil
302+ }
303+
304+ // Check if the requested action ID is valid
305+ if actionID < 1 || actionID > len (codeActions ) {
306+ return NewTextErrorResponse (fmt .Sprintf ("Invalid action ID: %d. Available actions: 1-%d" , actionID , len (codeActions ))), nil
307+ }
308+
309+ // Get the selected action (adjust for 0-based index)
310+ selectedAction := codeActions [actionID - 1 ]
311+
312+ // Execute the action based on its type
313+ switch v := selectedAction .Value .(type ) {
314+ case protocol.CodeAction :
315+ // Apply workspace edit if available
316+ if v .Edit != nil {
317+ err := util .ApplyWorkspaceEdit (* v .Edit )
318+ if err != nil {
319+ return NewTextErrorResponse (fmt .Sprintf ("Error applying edit: %s" , err )), nil
320+ }
321+ }
322+
323+ // Execute command if available
324+ if v .Command != nil {
325+ _ , err := client .ExecuteCommand (ctx , protocol.ExecuteCommandParams {
326+ Command : v .Command .Command ,
327+ Arguments : v .Command .Arguments ,
328+ })
329+ if err != nil {
330+ return NewTextErrorResponse (fmt .Sprintf ("Error executing command: %s" , err )), nil
331+ }
332+ }
333+
334+ return NewTextResponse (fmt .Sprintf ("Successfully executed code action: %s" , v .Title )), nil
335+
336+ case protocol.Command :
337+ // Execute the command
338+ _ , err := client .ExecuteCommand (ctx , protocol.ExecuteCommandParams {
339+ Command : v .Command ,
340+ Arguments : v .Arguments ,
341+ })
342+ if err != nil {
343+ return NewTextErrorResponse (fmt .Sprintf ("Error executing command: %s" , err )), nil
344+ }
345+
346+ return NewTextResponse (fmt .Sprintf ("Successfully executed command: %s" , v .Title )), nil
347+ }
348+
349+ return NewTextErrorResponse ("Unknown code action type" ), nil
350+ }
0 commit comments