github.com/jd-ly/tools@v0.5.7/internal/lsp/source/command.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package source
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/token"
    12  	"go/types"
    13  
    14  	"github.com/jd-ly/tools/go/analysis"
    15  	"github.com/jd-ly/tools/internal/lsp/analysis/fillstruct"
    16  	"github.com/jd-ly/tools/internal/lsp/analysis/undeclaredname"
    17  	"github.com/jd-ly/tools/internal/lsp/protocol"
    18  	"github.com/jd-ly/tools/internal/span"
    19  	errors "golang.org/x/xerrors"
    20  )
    21  
    22  type Command struct {
    23  	Title string
    24  	Name  string
    25  
    26  	// Async controls whether the command executes asynchronously.
    27  	Async bool
    28  
    29  	// appliesFn is an optional field to indicate whether or not a command can
    30  	// be applied to the given inputs. If it returns false, we should not
    31  	// suggest this command for these inputs.
    32  	appliesFn AppliesFunc
    33  
    34  	// suggestedFixFn is an optional field to generate the edits that the
    35  	// command produces for the given inputs.
    36  	suggestedFixFn SuggestedFixFunc
    37  }
    38  
    39  // CommandPrefix is the prefix of all command names gopls uses externally.
    40  const CommandPrefix = "gopls."
    41  
    42  // ID adds the CommandPrefix to the command name, in order to avoid
    43  // collisions with other language servers.
    44  func (c Command) ID() string {
    45  	return CommandPrefix + c.Name
    46  }
    47  
    48  type AppliesFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) bool
    49  
    50  // SuggestedFixFunc is a function used to get the suggested fixes for a given
    51  // gopls command, some of which are provided by go/analysis.Analyzers. Some of
    52  // the analyzers in internal/lsp/analysis are not efficient enough to include
    53  // suggested fixes with their diagnostics, so we have to compute them
    54  // separately. Such analyzers should provide a function with a signature of
    55  // SuggestedFixFunc.
    56  type SuggestedFixFunc func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error)
    57  
    58  // Commands are the commands currently supported by gopls.
    59  var Commands = []*Command{
    60  	CommandGenerate,
    61  	CommandFillStruct,
    62  	CommandRegenerateCgo,
    63  	CommandTest,
    64  	CommandTidy,
    65  	CommandUpdateGoSum,
    66  	CommandUndeclaredName,
    67  	CommandGoGetPackage,
    68  	CommandAddDependency,
    69  	CommandUpgradeDependency,
    70  	CommandRemoveDependency,
    71  	CommandVendor,
    72  	CommandExtractVariable,
    73  	CommandExtractFunction,
    74  	CommandToggleDetails,
    75  	CommandGenerateGoplsMod,
    76  }
    77  
    78  var (
    79  	// CommandTest runs `go test` for a specific test function.
    80  	CommandTest = &Command{
    81  		Name:  "test",
    82  		Title: "Run test(s)",
    83  		Async: true,
    84  	}
    85  
    86  	// CommandGenerate runs `go generate` for a given directory.
    87  	CommandGenerate = &Command{
    88  		Name:  "generate",
    89  		Title: "Run go generate",
    90  	}
    91  
    92  	// CommandTidy runs `go mod tidy` for a module.
    93  	CommandTidy = &Command{
    94  		Name:  "tidy",
    95  		Title: "Run go mod tidy",
    96  	}
    97  
    98  	// CommandVendor runs `go mod vendor` for a module.
    99  	CommandVendor = &Command{
   100  		Name:  "vendor",
   101  		Title: "Run go mod vendor",
   102  	}
   103  
   104  	// CommandGoGetPackage runs `go get` to fetch a package.
   105  	CommandGoGetPackage = &Command{
   106  		Name:  "go_get_package",
   107  		Title: "go get package",
   108  	}
   109  
   110  	// CommandUpdateGoSum updates the go.sum file for a module.
   111  	CommandUpdateGoSum = &Command{
   112  		Name:  "update_go_sum",
   113  		Title: "Update go.sum",
   114  	}
   115  
   116  	// CommandAddDependency adds a dependency.
   117  	CommandAddDependency = &Command{
   118  		Name:  "add_dependency",
   119  		Title: "Add dependency",
   120  	}
   121  
   122  	// CommandUpgradeDependency upgrades a dependency.
   123  	CommandUpgradeDependency = &Command{
   124  		Name:  "upgrade_dependency",
   125  		Title: "Upgrade dependency",
   126  	}
   127  
   128  	// CommandRemoveDependency removes a dependency.
   129  	CommandRemoveDependency = &Command{
   130  		Name:  "remove_dependency",
   131  		Title: "Remove dependency",
   132  	}
   133  
   134  	// CommandRegenerateCgo regenerates cgo definitions.
   135  	CommandRegenerateCgo = &Command{
   136  		Name:  "regenerate_cgo",
   137  		Title: "Regenerate cgo",
   138  	}
   139  
   140  	// CommandToggleDetails controls calculation of gc annotations.
   141  	CommandToggleDetails = &Command{
   142  		Name:  "gc_details",
   143  		Title: "Toggle gc_details",
   144  	}
   145  
   146  	// CommandFillStruct is a gopls command to fill a struct with default
   147  	// values.
   148  	CommandFillStruct = &Command{
   149  		Name:           "fill_struct",
   150  		Title:          "Fill struct",
   151  		suggestedFixFn: fillstruct.SuggestedFix,
   152  	}
   153  
   154  	// CommandUndeclaredName adds a variable declaration for an undeclared
   155  	// name.
   156  	CommandUndeclaredName = &Command{
   157  		Name:           "undeclared_name",
   158  		Title:          "Undeclared name",
   159  		suggestedFixFn: undeclaredname.SuggestedFix,
   160  	}
   161  
   162  	// CommandExtractVariable extracts an expression to a variable.
   163  	CommandExtractVariable = &Command{
   164  		Name:           "extract_variable",
   165  		Title:          "Extract to variable",
   166  		suggestedFixFn: extractVariable,
   167  		appliesFn: func(_ *token.FileSet, rng span.Range, _ []byte, file *ast.File, _ *types.Package, _ *types.Info) bool {
   168  			_, _, ok, _ := canExtractVariable(rng, file)
   169  			return ok
   170  		},
   171  	}
   172  
   173  	// CommandExtractFunction extracts statements to a function.
   174  	CommandExtractFunction = &Command{
   175  		Name:           "extract_function",
   176  		Title:          "Extract to function",
   177  		suggestedFixFn: extractFunction,
   178  		appliesFn: func(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) bool {
   179  			_, ok, _ := canExtractFunction(fset, rng, src, file, info)
   180  			return ok
   181  		},
   182  	}
   183  
   184  	// CommandGenerateGoplsMod (re)generates the gopls.mod file.
   185  	CommandGenerateGoplsMod = &Command{
   186  		Name:  "generate_gopls_mod",
   187  		Title: "Generate gopls.mod",
   188  	}
   189  )
   190  
   191  // Applies reports whether the command c implements a suggested fix that is
   192  // relevant to the given rng.
   193  func (c *Command) Applies(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) bool {
   194  	// If there is no applies function, assume that the command applies.
   195  	if c.appliesFn == nil {
   196  		return true
   197  	}
   198  	fset, rng, src, file, _, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
   199  	if err != nil {
   200  		return false
   201  	}
   202  	return c.appliesFn(fset, rng, src, file, pkg, info)
   203  }
   204  
   205  // IsSuggestedFix reports whether the given command is intended to work as a
   206  // suggested fix. Suggested fix commands are intended to return edits which are
   207  // then applied to the workspace.
   208  func (c *Command) IsSuggestedFix() bool {
   209  	return c.suggestedFixFn != nil
   210  }
   211  
   212  // SuggestedFix applies the command's suggested fix to the given file and
   213  // range, returning the resulting edits.
   214  func (c *Command) SuggestedFix(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
   215  	if c.suggestedFixFn == nil {
   216  		return nil, fmt.Errorf("no suggested fix function for %s", c.Name)
   217  	}
   218  	fset, rng, src, file, m, pkg, info, err := getAllSuggestedFixInputs(ctx, snapshot, fh, pRng)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	fix, err := c.suggestedFixFn(fset, rng, src, file, pkg, info)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	if fix == nil {
   227  		return nil, nil
   228  	}
   229  
   230  	var edits []protocol.TextDocumentEdit
   231  	for _, edit := range fix.TextEdits {
   232  		rng := span.NewRange(fset, edit.Pos, edit.End)
   233  		spn, err := rng.Span()
   234  		if err != nil {
   235  			return nil, err
   236  		}
   237  		clRng, err := m.Range(spn)
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  		edits = append(edits, protocol.TextDocumentEdit{
   242  			TextDocument: protocol.VersionedTextDocumentIdentifier{
   243  				Version: fh.Version(),
   244  				TextDocumentIdentifier: protocol.TextDocumentIdentifier{
   245  					URI: protocol.URIFromSpanURI(fh.URI()),
   246  				},
   247  			},
   248  			Edits: []protocol.TextEdit{
   249  				{
   250  					Range:   clRng,
   251  					NewText: string(edit.NewText),
   252  				},
   253  			},
   254  		})
   255  	}
   256  	return edits, nil
   257  }
   258  
   259  // getAllSuggestedFixInputs is a helper function to collect all possible needed
   260  // inputs for an AppliesFunc or SuggestedFixFunc.
   261  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) {
   262  	pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
   263  	if err != nil {
   264  		return nil, span.Range{}, nil, nil, nil, nil, nil, errors.Errorf("getting file for Identifier: %w", err)
   265  	}
   266  	spn, err := pgf.Mapper.RangeSpan(pRng)
   267  	if err != nil {
   268  		return nil, span.Range{}, nil, nil, nil, nil, nil, err
   269  	}
   270  	rng, err := spn.Range(pgf.Mapper.Converter)
   271  	if err != nil {
   272  		return nil, span.Range{}, nil, nil, nil, nil, nil, err
   273  	}
   274  	src, err := fh.Read()
   275  	if err != nil {
   276  		return nil, span.Range{}, nil, nil, nil, nil, nil, err
   277  	}
   278  	return snapshot.FileSet(), rng, src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
   279  }