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.
18 "golang.org/x/mod/modfile"
19 "golang.org/x/tools/internal/event"
20 "golang.org/x/tools/internal/gocommand"
21 "golang.org/x/tools/internal/lsp/cache"
22 "golang.org/x/tools/internal/lsp/command"
23 "golang.org/x/tools/internal/lsp/protocol"
24 "golang.org/x/tools/internal/lsp/source"
25 "golang.org/x/tools/internal/span"
26 "golang.org/x/tools/internal/xcontext"
27 errors "golang.org/x/xerrors"
30 func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
32 for _, name := range s.session.Options().SupportedCommands {
33 if name == params.Command {
39 return nil, fmt.Errorf("%s is not a supported command", params.Command)
42 handler := &commandHandler{
46 return command.Dispatch(ctx, params, handler)
49 type commandHandler struct {
51 params *protocol.ExecuteCommandParams
54 // commandConfig configures common command set-up and execution.
55 type commandConfig struct {
56 async bool // whether to run the command asynchronously. Async commands cannot return results.
57 requireSave bool // whether all files must be saved for the command to work
58 progress string // title to use for progress reporting. If empty, no progress will be reported.
59 forURI protocol.DocumentURI // URI to resolve to a snapshot. If unset, snapshot will be nil.
62 // commandDeps is evaluated from a commandConfig. Note that not all fields may
63 // be populated, depending on which configuration is set. See comments in-line
65 type commandDeps struct {
66 snapshot source.Snapshot // present if cfg.forURI was set
67 fh source.VersionedFileHandle // present if cfg.forURI was set
68 work *workDone // present cfg.progress was set
71 type commandFunc func(context.Context, commandDeps) error
73 func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run commandFunc) (err error) {
75 for _, overlay := range c.s.session.Overlays() {
77 return errors.New("All files must be saved first")
85 deps.snapshot, deps.fh, ok, release, err = c.s.beginFileRequest(ctx, cfg.forURI, source.UnknownKind)
91 ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
92 if cfg.progress != "" {
93 deps.work = c.s.progress.start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel)
95 runcmd := func() error {
99 case errors.Is(err, context.Canceled):
100 deps.work.end("canceled")
102 event.Error(ctx, "command error", err)
103 deps.work.end("failed")
105 deps.work.end("completed")
116 func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) error {
117 return c.run(ctx, commandConfig{
118 // Note: no progress here. Applying fixes should be quick.
120 }, func(ctx context.Context, deps commandDeps) error {
121 edits, err := source.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range)
125 r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
126 Edit: protocol.WorkspaceEdit{
127 DocumentChanges: edits,
134 return errors.New(r.FailureReason)
140 func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error {
141 return c.run(ctx, commandConfig{
142 progress: "Regenerating Cgo",
143 }, func(ctx context.Context, deps commandDeps) error {
144 mod := source.FileModification{
145 URI: args.URI.SpanURI(),
146 Action: source.InvalidateMetadata,
148 return c.s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
152 func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUpgradesArgs) error {
153 return c.run(ctx, commandConfig{
155 progress: "Checking for upgrades",
156 }, func(ctx context.Context, deps commandDeps) error {
157 upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, args.URI.SpanURI(), args.Modules)
161 deps.snapshot.View().RegisterModuleUpgrades(upgrades)
162 // Re-diagnose the snapshot to publish the new module diagnostics.
163 c.s.diagnoseSnapshot(deps.snapshot, nil, false)
168 func (c *commandHandler) AddDependency(ctx context.Context, args command.DependencyArgs) error {
169 return c.GoGetModule(ctx, args)
172 func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.DependencyArgs) error {
173 return c.GoGetModule(ctx, args)
176 func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error {
177 return c.run(ctx, commandConfig{
178 progress: "Running go get",
180 }, func(ctx context.Context, deps commandDeps) error {
181 return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
182 return runGoGetModule(invoke, args.AddRequire, args.GoCmdArgs)
187 // TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command.
188 func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArgs) error {
189 return c.run(ctx, commandConfig{
190 progress: "Updating go.sum",
191 }, func(ctx context.Context, deps commandDeps) error {
192 for _, uri := range args.URIs {
193 snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
198 if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
199 _, err := invoke("list", "all")
209 func (c *commandHandler) Tidy(ctx context.Context, args command.URIArgs) error {
210 return c.run(ctx, commandConfig{
212 progress: "Running go mod tidy",
213 }, func(ctx context.Context, deps commandDeps) error {
214 for _, uri := range args.URIs {
215 snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind)
220 if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
221 _, err := invoke("mod", "tidy")
231 func (c *commandHandler) Vendor(ctx context.Context, args command.URIArg) error {
232 return c.run(ctx, commandConfig{
234 progress: "Running go mod vendor",
236 }, func(ctx context.Context, deps commandDeps) error {
237 _, err := deps.snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{
239 Args: []string{"vendor"},
240 WorkingDir: filepath.Dir(args.URI.SpanURI().Filename()),
246 func (c *commandHandler) RemoveDependency(ctx context.Context, args command.RemoveDependencyArgs) error {
247 return c.run(ctx, commandConfig{
248 progress: "Removing dependency",
250 }, func(ctx context.Context, deps commandDeps) error {
251 // If the module is tidied apart from the one unused diagnostic, we can
252 // run `go get module@none`, and then run `go mod tidy`. Otherwise, we
253 // must make textual edits.
254 // TODO(rstambler): In Go 1.17+, we will be able to use the go command
255 // without checking if the module is tidy.
256 if args.OnlyDiagnostic {
257 return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
258 if err := runGoGetModule(invoke, false, []string{args.ModulePath + "@none"}); err != nil {
261 _, err := invoke("mod", "tidy")
265 pm, err := deps.snapshot.ParseMod(ctx, deps.fh)
269 edits, err := dropDependency(deps.snapshot, pm, args.ModulePath)
273 response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
274 Edit: protocol.WorkspaceEdit{
275 DocumentChanges: []protocol.TextDocumentEdit{{
276 TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
277 Version: deps.fh.Version(),
278 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
279 URI: protocol.URIFromSpanURI(deps.fh.URI()),
289 if !response.Applied {
290 return fmt.Errorf("edits not applied because of %s", response.FailureReason)
296 // dropDependency returns the edits to remove the given require from the go.mod
298 func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePath string) ([]protocol.TextEdit, error) {
299 // We need a private copy of the parsed go.mod file, since we're going to
301 copied, err := modfile.Parse("", pm.Mapper.Content, nil)
305 if err := copied.DropRequire(modulePath); err != nil {
309 newContent, err := copied.Format()
313 // Calculate the edits to be made due to the change.
314 diff, err := snapshot.View().Options().ComputeEdits(pm.URI, string(pm.Mapper.Content), string(newContent))
318 return source.ToProtocolEdits(pm.Mapper, diff)
321 func (c *commandHandler) Test(ctx context.Context, uri protocol.DocumentURI, tests, benchmarks []string) error {
322 return c.RunTests(ctx, command.RunTestsArgs{
325 Benchmarks: benchmarks,
329 func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error {
330 return c.run(ctx, commandConfig{
332 progress: "Running go test",
335 }, func(ctx context.Context, deps commandDeps) error {
336 if err := c.runTests(ctx, deps.snapshot, deps.work, args.URI, args.Tests, args.Benchmarks); err != nil {
337 if err := c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
338 Type: protocol.Error,
339 Message: fmt.Sprintf("Running tests failed: %v", err),
341 event.Error(ctx, "running tests: failed to show message", err)
344 // Since we're running asynchronously, any error returned here would be
350 func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *workDone, uri protocol.DocumentURI, tests, benchmarks []string) error {
351 // TODO: fix the error reporting when this runs async.
352 pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
357 return fmt.Errorf("package could not be found for file: %s", uri.SpanURI().Filename())
359 pkgPath := pkgs[0].ForTest()
362 buf := &bytes.Buffer{}
363 ew := &eventWriter{ctx: ctx, operation: "test"}
364 out := io.MultiWriter(ew, workDoneWriter{work}, buf)
366 // Run `go test -run Func` on each test.
368 for _, funcName := range tests {
369 inv := &gocommand.Invocation{
371 Args: []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)},
372 WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
374 if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
375 if errors.Is(err, context.Canceled) {
382 // Run `go test -run=^$ -bench Func` on each test.
383 var failedBenchmarks int
384 for _, funcName := range benchmarks {
385 inv := &gocommand.Invocation{
387 Args: []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)},
388 WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
390 if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
391 if errors.Is(err, context.Canceled) {
399 if len(tests) > 0 && len(benchmarks) > 0 {
400 title = "tests and benchmarks"
401 } else if len(tests) > 0 {
403 } else if len(benchmarks) > 0 {
406 return errors.New("No functions were provided")
408 message := fmt.Sprintf("all %s passed", title)
409 if failedTests > 0 && failedBenchmarks > 0 {
410 message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks))
411 } else if failedTests > 0 {
412 message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests))
413 } else if failedBenchmarks > 0 {
414 message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks))
416 if failedTests > 0 || failedBenchmarks > 0 {
417 message += "\n" + buf.String()
420 return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
426 func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs) error {
427 title := "Running go generate ."
429 title = "Running go generate ./..."
431 return c.run(ctx, commandConfig{
435 }, func(ctx context.Context, deps commandDeps) error {
436 er := &eventWriter{ctx: ctx, operation: "generate"}
442 inv := &gocommand.Invocation{
444 Args: []string{"-x", pattern},
445 WorkingDir: args.Dir.SpanURI().Filename(),
447 stderr := io.MultiWriter(er, workDoneWriter{deps.work})
448 if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
455 func (c *commandHandler) GoGetPackage(ctx context.Context, args command.GoGetPackageArgs) error {
456 return c.run(ctx, commandConfig{
458 progress: "Running go get",
459 }, func(ctx context.Context, deps commandDeps) error {
460 // Run on a throwaway go.mod, otherwise it'll write to the real one.
461 stdout, err := deps.snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
463 Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", args.Pkg},
464 WorkingDir: filepath.Dir(args.URI.SpanURI().Filename()),
469 ver := strings.TrimSpace(stdout.String())
470 return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error {
472 if err := addModuleRequire(invoke, []string{ver}); err != nil {
476 _, err := invoke(append([]string{"get", "-d"}, args.Pkg)...)
482 func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Snapshot, uri span.URI, run func(invoke func(...string) (*bytes.Buffer, error)) error) error {
483 tmpModfile, newModBytes, newSumBytes, err := snapshot.RunGoCommands(ctx, true, filepath.Dir(uri.Filename()), run)
490 modURI := snapshot.GoModForFile(uri)
491 sumURI := span.URIFromPath(strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum")
492 modEdits, err := applyFileEdits(ctx, snapshot, modURI, newModBytes)
496 sumEdits, err := applyFileEdits(ctx, snapshot, sumURI, newSumBytes)
500 changes := append(sumEdits, modEdits...)
501 if len(changes) == 0 {
504 response, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
505 Edit: protocol.WorkspaceEdit{
506 DocumentChanges: changes,
512 if !response.Applied {
513 return fmt.Errorf("edits not applied because of %s", response.FailureReason)
518 func applyFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, newContent []byte) ([]protocol.TextDocumentEdit, error) {
519 fh, err := snapshot.GetVersionedFile(ctx, uri)
523 oldContent, err := fh.Read()
524 if err != nil && !os.IsNotExist(err) {
527 if bytes.Equal(oldContent, newContent) {
531 // Sending a workspace edit to a closed file causes VS Code to open the
532 // file and leave it unsaved. We would rather apply the changes directly,
533 // especially to go.sum, which should be mostly invisible to the user.
534 if !snapshot.IsOpen(uri) {
535 err := ioutil.WriteFile(uri.Filename(), newContent, 0666)
539 m := &protocol.ColumnMapper{
541 Converter: span.NewContentConverter(fh.URI().Filename(), oldContent),
544 diff, err := snapshot.View().Options().ComputeEdits(uri, string(oldContent), string(newContent))
548 edits, err := source.ToProtocolEdits(m, diff)
552 return []protocol.TextDocumentEdit{{
553 TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{
554 Version: fh.Version(),
555 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
556 URI: protocol.URIFromSpanURI(uri),
563 func runGoGetModule(invoke func(...string) (*bytes.Buffer, error), addRequire bool, args []string) error {
565 if err := addModuleRequire(invoke, args); err != nil {
569 _, err := invoke(append([]string{"get", "-d"}, args...)...)
573 func addModuleRequire(invoke func(...string) (*bytes.Buffer, error), args []string) error {
574 // Using go get to create a new dependency results in an
575 // `// indirect` comment we may not want. The only way to avoid it
576 // is to add the require as direct first. Then we can use go get to
577 // update go.sum and tidy up.
578 _, err := invoke(append([]string{"mod", "edit", "-require"}, args...)...)
582 func (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri span.URI, modules []string) (map[string]string, error) {
583 stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{
585 Args: append([]string{"-m", "-u", "-json"}, modules...),
586 WorkingDir: filepath.Dir(uri.Filename()),
592 upgrades := map[string]string{}
593 for dec := json.NewDecoder(stdout); dec.More(); {
594 mod := &gocommand.ModuleJSON{}
595 if err := dec.Decode(mod); err != nil {
598 if mod.Update == nil {
601 upgrades[mod.Path] = mod.Update.Version
606 func (c *commandHandler) GCDetails(ctx context.Context, uri protocol.DocumentURI) error {
607 return c.ToggleGCDetails(ctx, command.URIArg{URI: uri})
610 func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIArg) error {
611 return c.run(ctx, commandConfig{
613 progress: "Toggling GC Details",
615 }, func(ctx context.Context, deps commandDeps) error {
616 pkg, err := deps.snapshot.PackageForFile(ctx, deps.fh.URI(), source.TypecheckWorkspace, source.NarrowestPackage)
620 c.s.gcOptimizationDetailsMu.Lock()
621 if _, ok := c.s.gcOptimizationDetails[pkg.ID()]; ok {
622 delete(c.s.gcOptimizationDetails, pkg.ID())
623 c.s.clearDiagnosticSource(gcDetailsSource)
625 c.s.gcOptimizationDetails[pkg.ID()] = struct{}{}
627 c.s.gcOptimizationDetailsMu.Unlock()
628 c.s.diagnoseSnapshot(deps.snapshot, nil, false)
633 func (c *commandHandler) GenerateGoplsMod(ctx context.Context, args command.URIArg) error {
634 // TODO: go back to using URI
635 return c.run(ctx, commandConfig{
637 progress: "Generating gopls.mod",
638 }, func(ctx context.Context, deps commandDeps) error {
639 views := c.s.session.Views()
641 return fmt.Errorf("cannot resolve view: have %d views", len(views))
644 snapshot, release := v.Snapshot(ctx)
646 modFile, err := cache.BuildGoplsMod(ctx, snapshot.View().Folder(), snapshot)
648 return errors.Errorf("getting workspace mod file: %w", err)
650 content, err := modFile.Format()
652 return errors.Errorf("formatting mod file: %w", err)
654 filename := filepath.Join(snapshot.View().Folder().Filename(), "gopls.mod")
655 if err := ioutil.WriteFile(filename, content, 0644); err != nil {
656 return errors.Errorf("writing mod file: %w", err)
662 func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URIArg) (command.ListKnownPackagesResult, error) {
663 var result command.ListKnownPackagesResult
664 err := c.run(ctx, commandConfig{
665 progress: "Listing packages", // optional, causes a progress report during command execution
666 forURI: args.URI, // optional, populates deps.snapshot and deps.fh
667 }, func(ctx context.Context, deps commandDeps) error {
668 // Marwan: add implementation here. deps.snapshot and deps.fh are available for use.
669 result.Packages = []string{}
675 func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) (command.AddImportResult, error) {
676 var result command.AddImportResult
677 err := c.run(ctx, commandConfig{
678 progress: "Adding import",
680 }, func(ctx context.Context, deps commandDeps) error {