--- /dev/null
+// Copyright 2020 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 source
+
+import (
+ "context"
+ "fmt"
+ "go/ast"
+ "go/token"
+ "go/types"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/internal/lsp/analysis/fillstruct"
+ "golang.org/x/tools/internal/lsp/analysis/undeclaredname"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/span"
+ errors "golang.org/x/xerrors"
+)
+
+type Command struct {
+ Title string
+ Name string
+
+ // Async controls whether the command executes asynchronously.
+ Async bool
+
+ // appliesFn is an optional field to indicate whether or not a command can
+ // be applied to the given inputs. If it returns false, we should not
+ // suggest this command for these inputs.
+ appliesFn AppliesFunc
+
+ // suggestedFixFn is an optional field to generate the edits that the
+ // command produces for the given inputs.
+ suggestedFixFn SuggestedFixFunc
+}
+
+// CommandPrefix is the prefix of all command names gopls uses externally.
+const CommandPrefix = "gopls."
+
+// ID adds the CommandPrefix to the command name, in order to avoid
+// collisions with other language servers.
+func (c Command) ID() string {
+ return CommandPrefix + c.Name
+}
+
+type AppliesFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) bool
+
+// SuggestedFixFunc is a function used to get the suggested fixes for a given
+// gopls command, some of which are provided by go/analysis.Analyzers. Some of
+// the analyzers in internal/lsp/analysis are not efficient enough to include
+// suggested fixes with their diagnostics, so we have to compute them
+// separately. Such analyzers should provide a function with a signature of
+// SuggestedFixFunc.
+type SuggestedFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
+
+// Commands are the commands currently supported by gopls.
+var Commands = []*Command{
+ CommandGenerate,
+ CommandFillStruct,
+ CommandRegenerateCgo,
+ CommandTest,
+ CommandTidy,
+ CommandUndeclaredName,
+ CommandAddDependency,
+ CommandUpgradeDependency,
+ CommandRemoveDependency,
+ CommandVendor,
+ CommandExtractVariable,
+ CommandExtractFunction,
+ CommandToggleDetails,
+ CommandGenerateGoplsMod,
+}
+
+var (
+ // CommandTest runs `go test` for a specific test function.
+ CommandTest = &Command{
+ Name: "test",
+ Title: "Run test(s)",
+ Async: true,
+ }
+
+ // CommandGenerate runs `go generate` for a given directory.
+ CommandGenerate = &Command{
+ Name: "generate",
+ Title: "Run go generate",
+ }
+
+ // CommandTidy runs `go mod tidy` for a module.
+ CommandTidy = &Command{
+ Name: "tidy",
+ Title: "Run go mod tidy",
+ }
+
+ // CommandVendor runs `go mod vendor` for a module.
+ CommandVendor = &Command{
+ Name: "vendor",
+ Title: "Run go mod vendor",
+ }
+
+ // CommandAddDependency adds a dependency.
+ CommandAddDependency = &Command{
+ Name: "add_dependency",
+ Title: "Add dependency",
+ }
+
+ // CommandUpgradeDependency upgrades a dependency.
+ CommandUpgradeDependency = &Command{
+ Name: "upgrade_dependency",
+ Title: "Upgrade dependency",
+ }
+
+ // CommandRemoveDependency removes a dependency.
+ CommandRemoveDependency = &Command{
+ Name: "remove_dependency",
+ Title: "Remove dependency",
+ }
+
+ // CommandRegenerateCgo regenerates cgo definitions.
+ CommandRegenerateCgo = &Command{
+ Name: "regenerate_cgo",
+ Title: "Regenerate cgo",
+ }
+
+ // CommandToggleDetails controls calculation of gc annotations.
+ CommandToggleDetails = &Command{
+ Name: "gc_details",
+ Title: "Toggle gc_details",
+ }
+
+ // CommandFillStruct is a gopls command to fill a struct with default
+ // values.
+ CommandFillStruct = &Command{
+ Name: "fill_struct",
+ Title: "Fill struct",
+ suggestedFixFn: fillstruct.SuggestedFix,
+ }
+
+ // CommandUndeclaredName adds a variable declaration for an undeclared
+ // name.
+ CommandUndeclaredName = &Command{
+ Name: "undeclared_name",
+ Title: "Undeclared name",
+ suggestedFixFn: undeclaredname.SuggestedFix,
+ }
+
+ // CommandExtractVariable extracts an expression to a variable.
+ CommandExtractVariable = &Command{
+ Name: "extract_variable",
+ Title: "Extract to variable",
+ suggestedFixFn: extractVariable,
+ appliesFn: func(_ *token.FileSet, rng span.Range, _ []byte, file *ast.File, _ *types.Package, _ *types.Info) bool {
+ _, _, ok, _ := canExtractVariable(rng, file)
+ return ok
+ },
+ }
+
+ // CommandExtractFunction extracts statements to a function.
+ CommandExtractFunction = &Command{
+ Name: "extract_function",
+ Title: "Extract to function",
+ suggestedFixFn: extractFunction,
+ appliesFn: func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) bool {
+ _, ok, _ := canExtractFunction(fset, rng, src, file, info)
+ return ok
+ },
+ }
+
+ // CommandGenerateGoplsMod (re)generates the gopls.mod file.
+ CommandGenerateGoplsMod = &Command{
+ Name: "generate_gopls_mod",
+ Title: "Generate gopls.mod",
+ }
+)
+
+// Applies reports whether the command c implements a suggested fix that is
+// relevant to the given rng.
+func (c *Command) Applies(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) bool {
+ // If there is no applies function, assume that the command applies.
+ if c.appliesFn == nil {
+ return true
+ }
+ fset, rng, src, file, _, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
+ if err != nil {
+ return false
+ }
+ return c.appliesFn(fset, rng, src, file, pkg, info)
+}
+
+// IsSuggestedFix reports whether the given command is intended to work as a
+// suggested fix. Suggested fix commands are intended to return edits which are
+// then applied to the workspace.
+func (c *Command) IsSuggestedFix() bool {
+ return c.suggestedFixFn != nil
+}
+
+// SuggestedFix applies the command's suggested fix to the given file and
+// range, returning the resulting edits.
+func (c *Command) SuggestedFix(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
+ if c.suggestedFixFn == nil {
+ return nil, fmt.Errorf("no suggested fix function for %s", c.Name)
+ }
+ fset, rng, src, file, m, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
+ if err != nil {
+ return nil, err
+ }
+ fix, err := c.suggestedFixFn(fset, rng, src, file, pkg, info)
+ if err != nil {
+ return nil, err
+ }
+ if fix == nil {
+ return nil, nil
+ }
+
+ var edits []protocol.TextDocumentEdit
+ for _, edit := range fix.TextEdits {
+ rng := span.NewRange(fset, edit.Pos, edit.End)
+ spn, err := rng.Span()
+ if err != nil {
+ return nil, err
+ }
+ clRng, err := m.Range(spn)
+ if err != nil {
+ return nil, err
+ }
+ edits = append(edits, protocol.TextDocumentEdit{
+ TextDocument: protocol.VersionedTextDocumentIdentifier{
+ Version: fh.Version(),
+ TextDocumentIdentifier: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(fh.URI()),
+ },
+ },
+ Edits: []protocol.TextEdit{
+ {
+ Range: clRng,
+ NewText: string(edit.NewText),
+ },
+ },
+ })
+ }
+ return edits, nil
+}
+
+// getAllSuggestedFixInputs is a helper function to collect all possible needed
+// inputs for an AppliesFunc or SuggestedFixFunc.
+func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, span.Range, []byte, *ast.File, *protocol.ColumnMapper, *types.Package, *types.Info, error) {
+ pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
+ if err != nil {
+ return nil, span.Range{}, nil, nil, nil, nil, nil, errors.Errorf("getting file for Identifier: %w", err)
+ }
+ spn, err := pgf.Mapper.RangeSpan(pRng)
+ if err != nil {
+ return nil, span.Range{}, nil, nil, nil, nil, nil, err
+ }
+ rng, err := spn.Range(pgf.Mapper.Converter)
+ if err != nil {
+ return nil, span.Range{}, nil, nil, nil, nil, nil, err
+ }
+ src, err := fh.Read()
+ if err != nil {
+ return nil, span.Range{}, nil, nil, nil, nil, nil, err
+ }
+ return snapshot.FileSet(), rng, src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
+}