diff --git a/internal/lsp/code_action.go b/internal/lsp/code_action.go index 9f3a1325eb..b7964336e9 100644 --- a/internal/lsp/code_action.go +++ b/internal/lsp/code_action.go @@ -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, diff --git a/internal/lsp/command.go b/internal/lsp/command.go index a4492cde8f..8b1b8e0c29 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -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 + }) +} diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index f3677282ea..2318b043f8 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -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) } diff --git a/internal/lsp/protocol/span.go b/internal/lsp/protocol/span.go index d6da886dd7..381e5f500c 100644 --- a/internal/lsp/protocol/span.go +++ b/internal/lsp/protocol/span.go @@ -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 { diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go index a4775e1690..1edf1542c2 100644 --- a/internal/lsp/source/command.go +++ b/internal/lsp/source/command.go @@ -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 } diff --git a/internal/lsp/source/extract.go b/internal/lsp/source/extract.go index e163554233..e7faaff3d8 100644 --- a/internal/lsp/source/extract.go +++ b/internal/lsp/source/extract.go @@ -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") }