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
|
// 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"
"strings"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/slices"
"golang.org/x/tools/internal/tool"
)
// 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 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:
quickfix
refactor
refactor.extract
refactor.inline
refactor.rewrite
source.organizeImports
source.fixAll
source.assembly
source.doc
source.freesymbols
goTest
gopls.doc.features
Kinds are hierarchical, so "refactor" includes "refactor.inline".
(Note: actions of kind "goTest" 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 tool.CommandLineErrorf("codeaction expects at least 1 argument")
}
cmd.app.editFlags = &cmd.EditFlags
conn, err := cmd.app.connect(ctx)
if err != nil {
return err
}
defer conn.terminate(ctx)
from := parseSpan(args[0])
uri := from.URI()
file, err := conn.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 := conn.diagnoseFiles(ctx, []protocol.DocumentURI{uri}); err != nil {
return err
}
diagnostics := []protocol.Diagnostic{} // LSP wants non-nil slice
conn.client.filesMu.Lock()
diagnostics = append(diagnostics, file.diagnostics...)
conn.client.filesMu.Unlock()
// Request code actions of the desired kinds.
var kinds []protocol.CodeActionKind
if cmd.Kind != "" {
for _, kind := range strings.Split(cmd.Kind, ",") {
kinds = append(kinds, protocol.CodeActionKind(kind))
}
}
actions, err := conn.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 := conn.executeCommand(ctx, 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 conn.Client.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
}
|