internal/lsp: eliminate funcs from commands, and refactor

appliesFn and suggestedFixFn were blocking eliminating the
source.Command dynamic configuration. Remove them, and along the way
refactor command dispatch to align better with the new
internal/lsp/command package.

This involved refactoring the internal/lsp/command.go as follows:
 - create a new commandHandler type, which will eventually implement
   command.Interface.
 - create a commandDeps struct to hold command dependencies.
 - move command functionality into methods on commandHandler.

Of these, there are likely to be at least a couple points of controvery:

I decided to store the ctx on the commandHandler, because I preferred it
to threading a context through command.Interface when it isn't needed.
We should revisit this in a later CL.

I opted for a sparse commandDeps struct, rather than either explicit
resolution of dependencies where necessary, or something more abstract
like a proper dependency resolution pattern. It saved enough boilerplate
that I deemed it worthwhile, but didn't want to commit to something more
sophisticated.

Actually switching to the internal/lsp/command package will happen in a
later CL.

Change-Id: I71502fc68f51f1b296bc529ee2885f7547145e92
Reviewed-on: https://go-review.googlesource.com/c/tools/+/289970
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
This commit is contained in:
Rob Findley 2021-02-04 19:35:05 -05:00 committed by Robert Findley
parent fd2f9f3bd1
commit a30116df7a
6 changed files with 410 additions and 412 deletions

View File

@ -18,6 +18,7 @@ import (
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
@ -433,14 +434,23 @@ func extractionFixes(ctx context.Context, snapshot source.Snapshot, pkg source.P
if err != nil {
return nil, err
}
_, pgf, err := source.GetParsedFile(ctx, snapshot, fh, source.NarrowestPackage)
if err != nil {
return nil, errors.Errorf("getting file for Identifier: %w", err)
}
srng, err := pgf.Mapper.RangeToSpanRange(rng)
if err != nil {
return nil, err
}
var commands []*source.Command
if _, ok, _ := source.CanExtractFunction(snapshot.FileSet(), srng, pgf.Src, pgf.File); ok {
commands = append(commands, source.CommandExtractFunction)
}
if _, _, ok, _ := source.CanExtractVariable(srng, pgf.File); ok {
commands = append(commands, source.CommandExtractVariable)
}
var actions []protocol.CodeAction
for _, command := range []*source.Command{
source.CommandExtractFunction,
source.CommandExtractVariable,
} {
if !command.Applies(ctx, snapshot, fh, rng) {
continue
}
for _, command := range commands {
actions = append(actions, protocol.CodeAction{
Title: command.Title,
Kind: protocol.RefactorExtract,

View File

@ -26,348 +26,314 @@ import (
)
func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
var command *source.Command
for _, c := range source.Commands {
if c.ID() == params.Command {
command = c
break
}
}
if command == nil {
return nil, fmt.Errorf("no known command")
}
var match bool
var found bool
for _, name := range s.session.Options().SupportedCommands {
if command.ID() == name {
match = true
if name == params.Command {
found = true
break
}
}
if !match {
return nil, fmt.Errorf("%s is not a supported command", command.ID())
}
ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
var work *workDone
// Don't show progress for suggested fixes. They should be quick.
if !command.IsSuggestedFix() {
// Start progress prior to spinning off a goroutine specifically so that
// clients are aware of the work item before the command completes. This
// matters for regtests, where having a continuous thread of work is
// convenient for assertions.
work = s.progress.start(ctx, command.Title, "Running...", params.WorkDoneToken, cancel)
if !found {
return nil, fmt.Errorf("%s is not a supported command", params.Command)
}
run := func() {
defer cancel()
err := s.runCommand(ctx, work, command, params.Arguments)
switch {
case errors.Is(err, context.Canceled):
work.end(command.Title + ": canceled")
case err != nil:
event.Error(ctx, fmt.Sprintf("%s: command error", command.Title), err)
work.end(command.Title + ": failed")
// Show a message when work completes with error, because the progress end
// message is typically dismissed immediately by LSP clients.
s.showCommandError(ctx, command.Title, err)
default:
work.end(command.ID() + ": completed")
}
cmd := &commandHandler{
ctx: ctx,
s: s,
params: params,
}
if command.Async {
go run()
} else {
run()
}
// Errors running the command are displayed to the user above, so don't
// return them.
return nil, nil
return cmd.dispatch()
}
func (s *Server) runSuggestedFixCommand(ctx context.Context, command *source.Command, args []json.RawMessage) error {
var uri protocol.DocumentURI
var rng protocol.Range
if err := source.UnmarshalArgs(args, &uri, &rng); err != nil {
return err
}
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go)
defer release()
if !ok {
return err
}
edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
if err != nil {
return err
}
r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
DocumentChanges: edits,
},
})
if err != nil {
return err
}
if !r.Applied {
return errors.New(r.FailureReason)
}
return nil
}
func (s *Server) showCommandError(ctx context.Context, title string, err error) {
// Command error messages should not be cancelable.
ctx = xcontext.Detach(ctx)
if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Error,
Message: fmt.Sprintf("%s failed: %v", title, err),
}); err != nil {
event.Error(ctx, title+": failed to show message", err)
}
type commandHandler struct {
// ctx is temporarily held so that we may implement the command.Interface interface.
ctx context.Context
s *Server
params *protocol.ExecuteCommandParams
}
// commandConfig configures common command set-up and execution.
type commandConfig struct {
async bool
requireSave bool // whether all files must be saved for the command to work
progress string // title to use for progress reporting. If empty, no progress will be reported.
forURI protocol.DocumentURI // URI to resolve to a snapshot. If unset, snapshot will be nil.
}
func (s *Server) prepareAndRun(ctx context.Context, cfg commandConfig, run func(source.Snapshot) error) error {
// commandDeps is evaluated from a commandConfig. Note that not all fields may
// be populated, depending on which configuration is set. See comments in-line
// for details.
type commandDeps struct {
snapshot source.Snapshot // present if cfg.forURI was set
fh source.VersionedFileHandle // present if cfg.forURI was set
work *workDone // present cfg.progress was set
}
type commandFunc func(context.Context, commandDeps) error
func (c *commandHandler) run(cfg commandConfig, run commandFunc) (err error) {
if cfg.requireSave {
for _, overlay := range s.session.Overlays() {
for _, overlay := range c.s.session.Overlays() {
if !overlay.Saved() {
return errors.New("All files must be saved first")
}
}
}
var snapshot source.Snapshot
var deps commandDeps
if cfg.forURI != "" {
snap, _, ok, release, err := s.beginFileRequest(ctx, cfg.forURI, source.UnknownKind)
var ok bool
var release func()
deps.snapshot, deps.fh, ok, release, err = c.s.beginFileRequest(c.ctx, cfg.forURI, source.UnknownKind)
defer release()
if !ok {
return err
}
snapshot = snap
}
return run(snapshot)
ctx, cancel := context.WithCancel(xcontext.Detach(c.ctx))
if cfg.progress != "" {
deps.work = c.s.progress.start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel)
}
runcmd := func() error {
defer cancel()
err := run(ctx, deps)
switch {
case errors.Is(err, context.Canceled):
deps.work.end("canceled")
case err != nil:
event.Error(ctx, "command error", err)
deps.work.end("failed")
default:
deps.work.end("completed")
}
return err
}
if cfg.async {
go runcmd()
return nil
}
return runcmd()
}
func (s *Server) runCommand(ctx context.Context, work *workDone, command *source.Command, args []json.RawMessage) (err error) {
// If the command has a suggested fix function available, use it and apply
// the edits to the workspace.
if command.IsSuggestedFix() {
return s.runSuggestedFixCommand(ctx, command, args)
}
switch command {
case source.CommandTest:
func (c *commandHandler) dispatch() (interface{}, error) {
switch c.params.Command {
case source.CommandFillStruct.ID(), source.CommandUndeclaredName.ID(),
source.CommandExtractVariable.ID(), source.CommandExtractFunction.ID():
var uri protocol.DocumentURI
var rng protocol.Range
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &rng); err != nil {
return nil, err
}
err := c.ApplyFix(uri, rng)
return nil, err
case source.CommandTest.ID():
var uri protocol.DocumentURI
var tests, benchmarks []string
if err := source.UnmarshalArgs(args, &uri, &tests, &benchmarks); err != nil {
return err
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &tests, &benchmarks); err != nil {
return nil, err
}
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(snapshot source.Snapshot) error {
return s.runTests(ctx, snapshot, uri, work, tests, benchmarks)
})
case source.CommandGenerate:
err := c.RunTests(uri, tests, benchmarks)
return nil, err
case source.CommandGenerate.ID():
var uri protocol.DocumentURI
var recursive bool
if err := source.UnmarshalArgs(args, &uri, &recursive); err != nil {
return err
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &recursive); err != nil {
return nil, err
}
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(snapshot source.Snapshot) error {
return s.runGoGenerate(ctx, snapshot, uri.SpanURI(), recursive, work)
})
case source.CommandRegenerateCgo:
err := c.Generate(uri, recursive)
return nil, err
case source.CommandRegenerateCgo.ID():
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(args, &uri); err != nil {
if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
return nil, err
}
return nil, c.RegenerateCgo(uri)
case source.CommandTidy.ID():
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
return nil, err
}
return nil, c.Tidy(uri)
case source.CommandVendor.ID():
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
return nil, err
}
return nil, c.Vendor(uri)
case source.CommandUpdateGoSum.ID():
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
return nil, err
}
return nil, c.UpdateGoSum(uri)
case source.CommandCheckUpgrades.ID():
var uri protocol.DocumentURI
var modules []string
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &modules); err != nil {
return nil, err
}
return nil, c.CheckUpgrades(uri, modules)
case source.CommandAddDependency.ID(), source.CommandUpgradeDependency.ID():
var uri protocol.DocumentURI
var goCmdArgs []string
var addRequire bool
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &addRequire, &goCmdArgs); err != nil {
return nil, err
}
return nil, c.GoGetModule(uri, addRequire, goCmdArgs)
case source.CommandRemoveDependency.ID():
var uri protocol.DocumentURI
var modulePath string
var onlyDiagnostic bool
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &onlyDiagnostic, &modulePath); err != nil {
return nil, err
}
return nil, c.RemoveDependency(modulePath, uri, onlyDiagnostic)
case source.CommandGoGetPackage.ID():
var uri protocol.DocumentURI
var pkg string
var addRequire bool
if err := source.UnmarshalArgs(c.params.Arguments, &uri, &addRequire, &pkg); err != nil {
return nil, err
}
return nil, c.GoGetPackage(uri, addRequire, pkg)
case source.CommandToggleDetails.ID():
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(c.params.Arguments, &uri); err != nil {
return nil, err
}
return nil, c.GCDetails(uri)
case source.CommandGenerateGoplsMod.ID():
return nil, c.GenerateGoplsMod()
}
return nil, fmt.Errorf("unsupported command: %s", c.params.Command)
}
func (c *commandHandler) ApplyFix(uri protocol.DocumentURI, rng protocol.Range) error {
return c.run(commandConfig{
// Note: no progress here. Applying fixes should be quick.
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
edits, err := source.ApplyFix(ctx, c.params.Command, deps.snapshot, deps.fh, rng)
if err != nil {
return err
}
r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
DocumentChanges: edits,
},
})
if err != nil {
return err
}
if !r.Applied {
return errors.New(r.FailureReason)
}
return nil
})
}
func (c *commandHandler) RegenerateCgo(uri protocol.DocumentURI) error {
return c.run(commandConfig{
progress: source.CommandRegenerateCgo.Title,
}, func(ctx context.Context, deps commandDeps) error {
mod := source.FileModification{
URI: uri.SpanURI(),
Action: source.InvalidateMetadata,
}
return s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
case source.CommandTidy, source.CommandVendor:
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(args, &uri); err != nil {
return err
}
// The flow for `go mod tidy` and `go mod vendor` is almost identical,
// so we combine them into one case for convenience.
action := "tidy"
if command == source.CommandVendor {
action = "vendor"
}
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(snapshot source.Snapshot) error {
return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{action})
})
case source.CommandUpdateGoSum:
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(args, &uri); err != nil {
return err
}
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(snapshot source.Snapshot) error {
return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
})
case source.CommandCheckUpgrades:
var uri protocol.DocumentURI
var modules []string
if err := source.UnmarshalArgs(args, &uri, &modules); err != nil {
return err
}
snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
defer release()
if !ok {
return err
}
upgrades, err := s.getUpgrades(ctx, snapshot, uri.SpanURI(), modules)
if err != nil {
return err
}
snapshot.View().RegisterModuleUpgrades(upgrades)
// Re-diagnose the snapshot to publish the new module diagnostics.
s.diagnoseSnapshot(snapshot, nil, false)
return nil
case source.CommandAddDependency, source.CommandUpgradeDependency:
var uri protocol.DocumentURI
var goCmdArgs []string
var addRequire bool
if err := source.UnmarshalArgs(args, &uri, &addRequire, &goCmdArgs); err != nil {
return err
}
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(snapshot source.Snapshot) error {
return s.runGoGetModule(ctx, snapshot, uri.SpanURI(), addRequire, goCmdArgs)
})
case source.CommandRemoveDependency:
var uri protocol.DocumentURI
var modulePath string
var onlyDiagnostic bool
if err := source.UnmarshalArgs(args, &uri, &onlyDiagnostic, &modulePath); err != nil {
return err
}
return s.removeDependency(ctx, modulePath, uri, onlyDiagnostic)
case source.CommandGoGetPackage:
var uri protocol.DocumentURI
var pkg string
var addRequire bool
if err := source.UnmarshalArgs(args, &uri, &addRequire, &pkg); err != nil {
return err
}
return s.prepareAndRun(ctx, commandConfig{
forURI: uri,
}, func(snapshot source.Snapshot) error {
return s.runGoGetPackage(ctx, snapshot, uri.SpanURI(), addRequire, pkg)
})
case source.CommandToggleDetails:
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(args, &uri); err != nil {
return err
}
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(snapshot source.Snapshot) error {
pkgDir := span.URIFromPath(filepath.Dir(uri.SpanURI().Filename()))
s.gcOptimizationDetailsMu.Lock()
if _, ok := s.gcOptimizationDetails[pkgDir]; ok {
delete(s.gcOptimizationDetails, pkgDir)
s.clearDiagnosticSource(gcDetailsSource)
} else {
s.gcOptimizationDetails[pkgDir] = struct{}{}
}
s.gcOptimizationDetailsMu.Unlock()
s.diagnoseSnapshot(snapshot, nil, false)
return nil
})
case source.CommandGenerateGoplsMod:
var v source.View
if len(args) == 0 {
views := s.session.Views()
if len(views) != 1 {
return fmt.Errorf("cannot resolve view: have %d views", len(views))
}
v = views[0]
} else {
var uri protocol.DocumentURI
if err := source.UnmarshalArgs(args, &uri); err != nil {
return err
}
var err error
v, err = s.session.ViewOf(uri.SpanURI())
if err != nil {
return err
}
}
snapshot, release := v.Snapshot(ctx)
defer release()
modFile, err := cache.BuildGoplsMod(ctx, v.Folder(), snapshot)
if err != nil {
return errors.Errorf("getting workspace mod file: %w", err)
}
content, err := modFile.Format()
if err != nil {
return errors.Errorf("formatting mod file: %w", err)
}
filename := filepath.Join(v.Folder().Filename(), "gopls.mod")
if err := ioutil.WriteFile(filename, content, 0644); err != nil {
return errors.Errorf("writing mod file: %w", err)
}
default:
return fmt.Errorf("unsupported command: %s", command.ID())
}
return nil
return c.s.didModifyFiles(c.ctx, []source.FileModification{mod}, FromRegenerateCgo)
})
}
func (s *Server) removeDependency(ctx context.Context, modulePath string, uri protocol.DocumentURI, onlyDiagnostic bool) error {
return s.prepareAndRun(ctx, commandConfig{
requireSave: true,
forURI: uri,
}, func(source.Snapshot) error {
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
defer release()
if !ok {
func (c *commandHandler) CheckUpgrades(uri protocol.DocumentURI, modules []string) error {
return c.run(commandConfig{
forURI: uri,
progress: source.CommandCheckUpgrades.Title,
}, func(ctx context.Context, deps commandDeps) error {
upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, uri.SpanURI(), modules)
if err != nil {
return err
}
deps.snapshot.View().RegisterModuleUpgrades(upgrades)
// Re-diagnose the snapshot to publish the new module diagnostics.
c.s.diagnoseSnapshot(deps.snapshot, nil, false)
return nil
})
}
func (c *commandHandler) GoGetModule(uri protocol.DocumentURI, addRequire bool, goCmdArgs []string) error {
return c.run(commandConfig{
requireSave: true,
progress: "Running go get",
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
return runGoGetModule(ctx, deps.snapshot, uri.SpanURI(), addRequire, goCmdArgs)
})
}
// TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command.
func (c *commandHandler) UpdateGoSum(uri protocol.DocumentURI) error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandUpdateGoSum.Title,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
})
}
func (c *commandHandler) Tidy(uri protocol.DocumentURI) error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandTidy.Title,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
})
}
func (c *commandHandler) Vendor(uri protocol.DocumentURI) error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandVendor.Title,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"vendor"})
})
}
func (c *commandHandler) RemoveDependency(modulePath string, uri protocol.DocumentURI, onlyDiagnostic bool) error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandRemoveDependency.Title,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
// If the module is tidied apart from the one unused diagnostic, we can
// run `go get module@none`, and then run `go mod tidy`. Otherwise, we
// must make textual edits.
// TODO(rstambler): In Go 1.17+, we will be able to use the go command
// without checking if the module is tidy.
if onlyDiagnostic {
if err := s.runGoGetModule(ctx, snapshot, uri.SpanURI(), false, []string{modulePath + "@none"}); err != nil {
if err := runGoGetModule(ctx, deps.snapshot, uri.SpanURI(), false, []string{modulePath + "@none"}); err != nil {
return err
}
return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
return runSimpleGoCommand(ctx, deps.snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{"tidy"})
}
pm, err := snapshot.ParseMod(ctx, fh)
pm, err := deps.snapshot.ParseMod(ctx, deps.fh)
if err != nil {
return err
}
edits, err := dropDependency(snapshot, pm, modulePath)
edits, err := dropDependency(deps.snapshot, pm, modulePath)
if err != nil {
return err
}
response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
Edit: protocol.WorkspaceEdit{
DocumentChanges: []protocol.TextDocumentEdit{{
TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
Version: fh.Version(),
Version: deps.fh.Version(),
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
URI: protocol.URIFromSpanURI(fh.URI()),
URI: protocol.URIFromSpanURI(deps.fh.URI()),
},
},
Edits: edits,
@ -409,7 +375,29 @@ func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePat
return source.ToProtocolEdits(pm.Mapper, diff)
}
func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI, work *workDone, tests, benchmarks []string) error {
func (c *commandHandler) RunTests(uri protocol.DocumentURI, tests, benchmarks []string) error {
return c.run(commandConfig{
async: true,
progress: source.CommandTest.Title,
requireSave: true,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
if err := c.runTests(ctx, deps.snapshot, deps.work, uri, tests, benchmarks); err != nil {
if err := c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Error,
Message: fmt.Sprintf("Running tests failed: %v", err),
}); err != nil {
event.Error(ctx, "running tests: failed to show message", err)
}
}
// Since we're running asynchronously, any error returned here would be
// ignored.
return nil
})
}
func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *workDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
// TODO: fix the error reporting when this runs async.
pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
if err != nil {
return err
@ -478,49 +466,57 @@ func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri pro
message += "\n" + buf.String()
}
return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
Type: protocol.Info,
Message: message,
})
}
func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, dir span.URI, recursive bool, work *workDone) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
func (c *commandHandler) Generate(uri protocol.DocumentURI, recursive bool) error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandGenerate.Title,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
er := &eventWriter{ctx: ctx, operation: "generate"}
er := &eventWriter{ctx: ctx, operation: "generate"}
pattern := "."
if recursive {
pattern = "./..."
}
inv := &gocommand.Invocation{
Verb: "generate",
Args: []string{"-x", pattern},
WorkingDir: dir.Filename(),
}
stderr := io.MultiWriter(er, workDoneWriter{work})
if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
return err
}
return nil
}
func (s *Server) runGoGetPackage(ctx context.Context, snapshot source.Snapshot, uri span.URI, addRequire bool, pkg string) error {
stdout, err := snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
Verb: "list",
Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", pkg},
WorkingDir: filepath.Dir(uri.Filename()),
pattern := "."
if recursive {
pattern = "./..."
}
inv := &gocommand.Invocation{
Verb: "generate",
Args: []string{"-x", pattern},
WorkingDir: uri.SpanURI().Filename(),
}
stderr := io.MultiWriter(er, workDoneWriter{deps.work})
if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
return err
}
return nil
})
if err != nil {
return err
}
ver := strings.TrimSpace(stdout.String())
return s.runGoGetModule(ctx, snapshot, uri, addRequire, []string{ver})
}
func (s *Server) runGoGetModule(ctx context.Context, snapshot source.Snapshot, uri span.URI, addRequire bool, args []string) error {
func (c *commandHandler) GoGetPackage(puri protocol.DocumentURI, addRequire bool, pkg string) error {
return c.run(commandConfig{
forURI: puri,
progress: source.CommandGoGetPackage.Title,
}, func(ctx context.Context, deps commandDeps) error {
uri := puri.SpanURI()
stdout, err := deps.snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
Verb: "list",
Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", pkg},
WorkingDir: filepath.Dir(uri.Filename()),
})
if err != nil {
return err
}
ver := strings.TrimSpace(stdout.String())
return runGoGetModule(ctx, deps.snapshot, uri, addRequire, []string{ver})
})
}
func runGoGetModule(ctx context.Context, snapshot source.Snapshot, uri span.URI, addRequire bool, args []string) error {
if addRequire {
// Using go get to create a new dependency results in an
// `// indirect` comment we may not want. The only way to avoid it
@ -565,3 +561,51 @@ func (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri
}
return upgrades, nil
}
func (c *commandHandler) GCDetails(uri protocol.DocumentURI) error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandToggleDetails.Title,
forURI: uri,
}, func(ctx context.Context, deps commandDeps) error {
pkgDir := span.URIFromPath(filepath.Dir(uri.SpanURI().Filename()))
c.s.gcOptimizationDetailsMu.Lock()
if _, ok := c.s.gcOptimizationDetails[pkgDir]; ok {
delete(c.s.gcOptimizationDetails, pkgDir)
c.s.clearDiagnosticSource(gcDetailsSource)
} else {
c.s.gcOptimizationDetails[pkgDir] = struct{}{}
}
c.s.gcOptimizationDetailsMu.Unlock()
c.s.diagnoseSnapshot(deps.snapshot, nil, false)
return nil
})
}
func (c *commandHandler) GenerateGoplsMod() error {
return c.run(commandConfig{
requireSave: true,
progress: source.CommandGenerateGoplsMod.Title,
}, func(ctx context.Context, deps commandDeps) error {
views := c.s.session.Views()
if len(views) != 1 {
return fmt.Errorf("cannot resolve view: have %d views", len(views))
}
v := views[0]
snapshot, release := v.Snapshot(ctx)
defer release()
modFile, err := cache.BuildGoplsMod(ctx, snapshot.View().Folder(), snapshot)
if err != nil {
return errors.Errorf("getting workspace mod file: %w", err)
}
content, err := modFile.Format()
if err != nil {
return errors.Errorf("formatting mod file: %w", err)
}
filename := filepath.Join(snapshot.View().Folder().Filename(), "gopls.mod")
if err := ioutil.WriteFile(filename, content, 0644); err != nil {
return errors.Errorf("writing mod file: %w", err)
}
return nil
})
}

View File

@ -533,7 +533,7 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string,
}
var res map[span.URI]string
if cmd := action.Command; cmd != nil {
edits, err := commandToEdits(r.ctx, snapshot, fh, rng, action.Command.Command)
edits, err := source.ApplyFix(r.ctx, cmd.Command, snapshot, fh, rng)
if err != nil {
t.Fatalf("error converting command %q to edits: %v", action.Command.Command, err)
}
@ -557,27 +557,6 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string,
}
}
func commandToEdits(ctx context.Context, snapshot source.Snapshot, fh source.VersionedFileHandle, rng protocol.Range, cmd string) ([]protocol.TextDocumentEdit, error) {
var command *source.Command
for _, c := range source.Commands {
if c.ID() == cmd {
command = c
break
}
}
if command == nil {
return nil, fmt.Errorf("no known command for %s", cmd)
}
if !command.Applies(ctx, snapshot, fh, rng) {
return nil, fmt.Errorf("cannot apply %v", command.ID())
}
edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
if err != nil {
return nil, fmt.Errorf("error calling command.SuggestedFix: %v", err)
}
return edits, nil
}
func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span) {
uri := start.URI()
view, err := r.server.session.ViewOf(uri)
@ -618,7 +597,7 @@ func (r *runner) FunctionExtraction(t *testing.T, start span.Span, end span.Span
if len(actions) == 0 || len(actions) > 1 {
t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions))
}
edits, err := commandToEdits(r.ctx, snapshot, fh, rng, actions[0].Command.Command)
edits, err := source.ApplyFix(r.ctx, actions[0].Command.Command, snapshot, fh, rng)
if err != nil {
t.Fatal(err)
}

View File

@ -85,6 +85,14 @@ func (m *ColumnMapper) RangeSpan(r Range) (span.Span, error) {
return span.New(m.URI, start, end).WithAll(m.Converter)
}
func (m *ColumnMapper) RangeToSpanRange(r Range) (span.Range, error) {
spn, err := m.RangeSpan(r)
if err != nil {
return span.Range{}, err
}
return spn.Range(m.Converter)
}
func (m *ColumnMapper) PointSpan(p Position) (span.Span, error) {
start, err := m.Point(p)
if err != nil {

View File

@ -25,15 +25,6 @@ type Command struct {
// 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.
@ -45,8 +36,6 @@ 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
@ -153,39 +142,27 @@ var (
// CommandFillStruct is a gopls command to fill a struct with default
// values.
CommandFillStruct = &Command{
Name: "fill_struct",
Title: "Fill struct",
suggestedFixFn: fillstruct.SuggestedFix,
Name: "fill_struct",
Title: "Fill struct",
}
// CommandUndeclaredName adds a variable declaration for an undeclared
// name.
CommandUndeclaredName = &Command{
Name: "undeclared_name",
Title: "Undeclared name",
suggestedFixFn: undeclaredname.SuggestedFix,
Name: "undeclared_name",
Title: "Undeclared name",
}
// 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
},
Name: "extract_variable",
Title: "Extract to variable",
}
// 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
},
Name: "extract_function",
Title: "Extract to function",
}
// CommandGenerateGoplsMod (re)generates the gopls.mod file.
@ -195,38 +172,26 @@ var (
}
)
// 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)
// suggestedFixes maps a suggested fix command id to its handler.
var suggestedFixes = map[string]SuggestedFixFunc{
CommandFillStruct.ID(): fillstruct.SuggestedFix,
CommandUndeclaredName.ID(): undeclaredname.SuggestedFix,
CommandExtractVariable.ID(): extractVariable,
CommandExtractFunction.ID(): extractFunction,
}
// 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
// ApplyFix 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)
func ApplyFix(ctx context.Context, cmdid string, snapshot Snapshot, fh VersionedFileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) {
handler, ok := suggestedFixes[cmdid]
if !ok {
return nil, fmt.Errorf("no suggested fix function for %s", cmdid)
}
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)
fix, err := handler(fset, rng, src, file, pkg, info)
if err != nil {
return nil, err
}
@ -270,17 +235,9 @@ func getAllSuggestedFixInputs(ctx context.Context, snapshot Snapshot, fh FileHan
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)
rng, err := pgf.Mapper.RangeToSpanRange(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
return snapshot.FileSet(), rng, pgf.Src, pgf.File, pgf.Mapper, pkg.GetTypes(), pkg.GetTypesInfo(), nil
}

View File

@ -22,7 +22,7 @@ import (
)
func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Package, info *types.Info) (*analysis.SuggestedFix, error) {
expr, path, ok, err := canExtractVariable(rng, file)
expr, path, ok, err := CanExtractVariable(rng, file)
if !ok {
return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", fset.Position(rng.Start), err)
}
@ -90,9 +90,9 @@ func extractVariable(fset *token.FileSet, rng span.Range, src []byte, file *ast.
}, nil
}
// canExtractVariable reports whether the code in the given range can be
// CanExtractVariable reports whether the code in the given range can be
// extracted to a variable.
func canExtractVariable(rng span.Range, file *ast.File) (ast.Expr, []ast.Node, bool, error) {
func CanExtractVariable(rng span.Range, file *ast.File) (ast.Expr, []ast.Node, bool, error) {
if rng.Start == rng.End {
return nil, nil, false, fmt.Errorf("start and end are equal")
}
@ -180,7 +180,7 @@ type returnVariable struct {
// of the function and insert this call as well as the extracted function into
// their proper locations.
func extractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) {
p, ok, err := canExtractFunction(fset, rng, src, file, info)
p, ok, err := CanExtractFunction(fset, rng, src, file)
if !ok {
return nil, fmt.Errorf("extractFunction: cannot extract %s: %v",
fset.Position(rng.Start), err)
@ -792,9 +792,9 @@ type fnExtractParams struct {
start ast.Node
}
// canExtractFunction reports whether the code in the given range can be
// CanExtractFunction reports whether the code in the given range can be
// extracted to a function.
func canExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File, _ *types.Info) (*fnExtractParams, bool, error) {
func CanExtractFunction(fset *token.FileSet, rng span.Range, src []byte, file *ast.File) (*fnExtractParams, bool, error) {
if rng.Start == rng.End {
return nil, false, fmt.Errorf("start and end are equal")
}