lsp_code_action.go 10 KB

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