| // Copyright 2024 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package cmd | |
| import ( | |
| "context" | |
| "flag" | |
| "fmt" | |
| "regexp" | |
| "slices" | |
| "strings" | |
| "golang.org/x/tools/gopls/internal/protocol" | |
| ) | |
| // codeaction implements the codeaction verb for gopls. | |
| type codeaction struct { | |
| EditFlags | |
| Kind string `flag:"kind" help:"comma-separated list of code action kinds to filter"` | |
| Title string `flag:"title" help:"regular expression to match title"` | |
| Exec bool `flag:"exec" help:"execute the first matching code action"` | |
| app *application | |
| } | |
| func (cmd *codeaction) Name() string { return "codeaction" } | |
| func (cmd *codeaction) Parent() string { return cmd.app.Name() } | |
| func (cmd *codeaction) Usage() string { return "[codeaction-flags] filename[:line[:col]]" } | |
| func (cmd *codeaction) ShortHelp() string { return "list or execute code actions" } | |
| func (cmd *codeaction) DetailedHelp(f *flag.FlagSet) { | |
| fmt.Fprintf(f.Output(), ` | |
| The codeaction command lists or executes code actions for the | |
| specified file or range of a file. Each code action contains | |
| either an edit to be directly applied to the file, or a command | |
| to be executed by the server, which may have an effect such as: | |
| - requesting that the client apply an edit; | |
| - changing the state of the server; or | |
| - requesting that the client open a document. | |
| The -kind and -title flags filter the list of actions. | |
| The -kind flag specifies a comma-separated list of LSP CodeAction kinds. | |
| Only actions of these kinds will be requested from the server. | |
| Valid kinds include: | |
| gopls.doc.features | |
| quickfix | |
| refactor | |
| refactor.extract | |
| refactor.extract.constant | |
| refactor.extract.function | |
| refactor.extract.method | |
| refactor.extract.toNewFile | |
| refactor.extract.variable | |
| refactor.inline | |
| refactor.inline.call | |
| refactor.inline.variable | |
| refactor.rewrite | |
| refactor.rewrite.changeQuote | |
| refactor.rewrite.fillStruct | |
| refactor.rewrite.fillSwitch | |
| refactor.rewrite.invertIf | |
| refactor.rewrite.joinLines | |
| refactor.rewrite.removeUnusedParam | |
| refactor.rewrite.splitLines | |
| source | |
| source.assembly | |
| source.doc | |
| source.fixAll | |
| source.freesymbols | |
| source.organizeImports | |
| source.test | |
| Kinds are hierarchical, so "refactor" includes "refactor.inline". | |
| (Note: actions of kind "source.test" are not returned unless explicitly | |
| requested.) | |
| The -title flag specifies a regular expression that must match the | |
| action's title. (Ideally kinds would be specific enough that this | |
| isn't necessary; we really need to subdivide refactor.rewrite; see | |
| gopls/internal/settings/codeactionkind.go.) | |
| The -exec flag causes the first matching code action to be executed. | |
| Without the flag, the matching actions are merely listed. | |
| It is not currently possible to execute more than one action, | |
| as that requires a way to detect and resolve conflicts. | |
| TODO(adonovan): support it when golang/go#67049 is resolved. | |
| If executing an action causes the server to send a patch to the | |
| client, the usual -write, -preserve, -diff, and -list flags govern how | |
| the client deals with the patch. | |
| Example: execute the first "quick fix" in the specified file and show the diff: | |
| $ gopls codeaction -kind=quickfix -exec -diff ./gopls/main.go | |
| codeaction-flags: | |
| `) | |
| printFlagDefaults(f) | |
| } | |
| func (cmd *codeaction) Run(ctx context.Context, args ...string) error { | |
| if len(args) < 1 { | |
| return commandLineErrorf("codeaction expects at least 1 argument") | |
| } | |
| cmd.app.editFlags = &cmd.EditFlags | |
| cli, _, err := cmd.app.connect(ctx) | |
| if err != nil { | |
| return err | |
| } | |
| defer cli.terminate(ctx) | |
| from := parseSpan(args[0]) | |
| uri := from.URI() | |
| file, err := cli.openFile(ctx, uri) | |
| if err != nil { | |
| return err | |
| } | |
| rng, err := file.spanRange(from) | |
| if err != nil { | |
| return err | |
| } | |
| titleRE, err := regexp.Compile(cmd.Title) | |
| if err != nil { | |
| return err | |
| } | |
| // Get diagnostics, as they may encode various lazy code actions. | |
| if err := diagnoseFiles(ctx, cli.server, []protocol.DocumentURI{uri}); err != nil { | |
| return err | |
| } | |
| file.diagnosticsMu.Lock() | |
| diagnostics := slices.Clone(file.diagnostics) | |
| file.diagnosticsMu.Unlock() | |
| // Request code actions of the desired kinds. | |
| var kinds []protocol.CodeActionKind | |
| if cmd.Kind != "" { | |
| for kind := range strings.SplitSeq(cmd.Kind, ",") { | |
| kinds = append(kinds, protocol.CodeActionKind(kind)) | |
| } | |
| } else { | |
| kinds = append(kinds, protocol.Empty) // => all | |
| } | |
| actions, err := cli.server.CodeAction(ctx, &protocol.CodeActionParams{ | |
| TextDocument: protocol.TextDocumentIdentifier{URI: uri}, | |
| Range: rng, | |
| Context: protocol.CodeActionContext{ | |
| Only: kinds, | |
| Diagnostics: diagnostics, | |
| }, | |
| }) | |
| if err != nil { | |
| return fmt.Errorf("%v: %v", from, err) | |
| } | |
| // Gather edits from matching code actions. | |
| var edits []protocol.TextEdit | |
| for _, act := range actions { | |
| if act.Disabled != nil { | |
| continue | |
| } | |
| if !titleRE.MatchString(act.Title) { | |
| continue | |
| } | |
| // If the provided span has a position (not just offsets), | |
| // and the action has diagnostics, the action must have a | |
| // diagnostic with the same range as it. | |
| if from.HasPosition() && len(act.Diagnostics) > 0 && | |
| !slices.ContainsFunc(act.Diagnostics, func(diag protocol.Diagnostic) bool { | |
| return diag.Range.Start == rng.Start | |
| }) { | |
| continue | |
| } | |
| if cmd.Exec { | |
| // -exec: run the first matching code action. | |
| if act.Command != nil { | |
| // This may cause the server to make | |
| // an ApplyEdit downcall to the client. | |
| if _, err := executeCommand(ctx, cli.server, act.Command); err != nil { | |
| return err | |
| } | |
| // The specification says that commands should | |
| // be executed _after_ edits are applied, not | |
| // instead of them, but we don't want to | |
| // duplicate edits. | |
| } else { | |
| // Partially apply CodeAction.Edit, a WorkspaceEdit. | |
| // (See also cli.applyWorkspaceEdit(a.Edit)). | |
| for _, c := range act.Edit.DocumentChanges { | |
| tde := c.TextDocumentEdit | |
| if tde != nil && tde.TextDocument.URI == uri { | |
| // TODO(adonovan): this logic will butcher an edit that spans files. | |
| // It will also ignore create/delete/rename operations. | |
| // Fix or document. Need a three-way merge. | |
| edits = append(edits, protocol.AsTextEdits(tde.Edits)...) | |
| } | |
| } | |
| return applyTextEdits(file.mapper, edits, cmd.app.editFlags) | |
| } | |
| return nil | |
| } else { | |
| // No -exec: list matching code actions. | |
| action := "edit" | |
| if act.Command != nil { | |
| action = "command" | |
| } | |
| fmt.Printf("%s\t%q [%s]\n", | |
| action, | |
| act.Title, | |
| act.Kind) | |
| } | |
| } | |
| if cmd.Exec { | |
| return fmt.Errorf("no matching code action at %s", from) | |
| } | |
| return nil | |
| } |