зеркало из https://github.com/golang/tools.git
227 строки
6.5 KiB
Go
227 строки
6.5 KiB
Go
// 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"
|
|
"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:
|
|
|
|
gopls.doc.features
|
|
quickfix
|
|
refactor
|
|
refactor.extract
|
|
refactor.extract.function
|
|
refactor.extract.method
|
|
refactor.extract.toNewFile
|
|
refactor.extract.variable
|
|
refactor.inline
|
|
refactor.inline.call
|
|
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 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))
|
|
}
|
|
} else {
|
|
kinds = append(kinds, protocol.Empty) // => all
|
|
}
|
|
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
|
|
}
|