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

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package lsp
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"github.com/jd-ly/tools/internal/event"
    18  	"github.com/jd-ly/tools/internal/gocommand"
    19  	"github.com/jd-ly/tools/internal/lsp/cache"
    20  	"github.com/jd-ly/tools/internal/lsp/protocol"
    21  	"github.com/jd-ly/tools/internal/lsp/source"
    22  	"github.com/jd-ly/tools/internal/span"
    23  	"github.com/jd-ly/tools/internal/xcontext"
    24  	errors "golang.org/x/xerrors"
    25  )
    26  
    27  func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
    28  	var command *source.Command
    29  	for _, c := range source.Commands {
    30  		if c.ID() == params.Command {
    31  			command = c
    32  			break
    33  		}
    34  	}
    35  	if command == nil {
    36  		return nil, fmt.Errorf("no known command")
    37  	}
    38  	var match bool
    39  	for _, name := range s.session.Options().SupportedCommands {
    40  		if command.ID() == name {
    41  			match = true
    42  			break
    43  		}
    44  	}
    45  	if !match {
    46  		return nil, fmt.Errorf("%s is not a supported command", command.ID())
    47  	}
    48  	// Some commands require that all files are saved to disk. If we detect
    49  	// unsaved files, warn the user instead of running the commands.
    50  	unsaved := false
    51  	for _, overlay := range s.session.Overlays() {
    52  		if !overlay.Saved() {
    53  			unsaved = true
    54  			break
    55  		}
    56  	}
    57  	if unsaved {
    58  		switch params.Command {
    59  		case source.CommandTest.ID(),
    60  			source.CommandGenerate.ID(),
    61  			source.CommandToggleDetails.ID(),
    62  			source.CommandAddDependency.ID(),
    63  			source.CommandUpgradeDependency.ID(),
    64  			source.CommandRemoveDependency.ID(),
    65  			source.CommandVendor.ID():
    66  			// TODO(PJW): for Toggle, not an error if it is being disabled
    67  			err := errors.New("All files must be saved first")
    68  			s.showCommandError(ctx, command.Title, err)
    69  			return nil, nil
    70  		}
    71  	}
    72  	ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
    73  
    74  	var work *workDone
    75  	// Don't show progress for suggested fixes. They should be quick.
    76  	if !command.IsSuggestedFix() {
    77  		// Start progress prior to spinning off a goroutine specifically so that
    78  		// clients are aware of the work item before the command completes. This
    79  		// matters for regtests, where having a continuous thread of work is
    80  		// convenient for assertions.
    81  		work = s.progress.start(ctx, command.Title, "Running...", params.WorkDoneToken, cancel)
    82  	}
    83  
    84  	run := func() {
    85  		defer cancel()
    86  		err := s.runCommand(ctx, work, command, params.Arguments)
    87  		switch {
    88  		case errors.Is(err, context.Canceled):
    89  			work.end(command.Title + ": canceled")
    90  		case err != nil:
    91  			event.Error(ctx, fmt.Sprintf("%s: command error", command.Title), err)
    92  			work.end(command.Title + ": failed")
    93  			// Show a message when work completes with error, because the progress end
    94  			// message is typically dismissed immediately by LSP clients.
    95  			s.showCommandError(ctx, command.Title, err)
    96  		default:
    97  			work.end(command.ID() + ": completed")
    98  		}
    99  	}
   100  	if command.Async {
   101  		go run()
   102  	} else {
   103  		run()
   104  	}
   105  	// Errors running the command are displayed to the user above, so don't
   106  	// return them.
   107  	return nil, nil
   108  }
   109  
   110  func (s *Server) runSuggestedFixCommand(ctx context.Context, command *source.Command, args []json.RawMessage) error {
   111  	var uri protocol.DocumentURI
   112  	var rng protocol.Range
   113  	if err := source.UnmarshalArgs(args, &uri, &rng); err != nil {
   114  		return err
   115  	}
   116  	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go)
   117  	defer release()
   118  	if !ok {
   119  		return err
   120  	}
   121  	edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
   122  	if err != nil {
   123  		return err
   124  	}
   125  	r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
   126  		Edit: protocol.WorkspaceEdit{
   127  			DocumentChanges: edits,
   128  		},
   129  	})
   130  	if err != nil {
   131  		return err
   132  	}
   133  	if !r.Applied {
   134  		return errors.New(r.FailureReason)
   135  	}
   136  	return nil
   137  }
   138  
   139  func (s *Server) showCommandError(ctx context.Context, title string, err error) {
   140  	// Command error messages should not be cancelable.
   141  	ctx = xcontext.Detach(ctx)
   142  	if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
   143  		Type:    protocol.Error,
   144  		Message: fmt.Sprintf("%s failed: %v", title, err),
   145  	}); err != nil {
   146  		event.Error(ctx, title+": failed to show message", err)
   147  	}
   148  }
   149  
   150  func (s *Server) runCommand(ctx context.Context, work *workDone, command *source.Command, args []json.RawMessage) (err error) {
   151  	// If the command has a suggested fix function available, use it and apply
   152  	// the edits to the workspace.
   153  	if command.IsSuggestedFix() {
   154  		return s.runSuggestedFixCommand(ctx, command, args)
   155  	}
   156  	switch command {
   157  	case source.CommandTest:
   158  		var uri protocol.DocumentURI
   159  		var tests, benchmarks []string
   160  		if err := source.UnmarshalArgs(args, &uri, &tests, &benchmarks); err != nil {
   161  			return err
   162  		}
   163  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
   164  		defer release()
   165  		if !ok {
   166  			return err
   167  		}
   168  		return s.runTests(ctx, snapshot, uri, work, tests, benchmarks)
   169  	case source.CommandGenerate:
   170  		var uri protocol.DocumentURI
   171  		var recursive bool
   172  		if err := source.UnmarshalArgs(args, &uri, &recursive); err != nil {
   173  			return err
   174  		}
   175  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
   176  		defer release()
   177  		if !ok {
   178  			return err
   179  		}
   180  		return s.runGoGenerate(ctx, snapshot, uri.SpanURI(), recursive, work)
   181  	case source.CommandRegenerateCgo:
   182  		var uri protocol.DocumentURI
   183  		if err := source.UnmarshalArgs(args, &uri); err != nil {
   184  			return err
   185  		}
   186  		mod := source.FileModification{
   187  			URI:    uri.SpanURI(),
   188  			Action: source.InvalidateMetadata,
   189  		}
   190  		return s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
   191  	case source.CommandTidy, source.CommandVendor:
   192  		var uri protocol.DocumentURI
   193  		if err := source.UnmarshalArgs(args, &uri); err != nil {
   194  			return err
   195  		}
   196  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
   197  		defer release()
   198  		if !ok {
   199  			return err
   200  		}
   201  		// The flow for `go mod tidy` and `go mod vendor` is almost identical,
   202  		// so we combine them into one case for convenience.
   203  		action := "tidy"
   204  		if command == source.CommandVendor {
   205  			action = "vendor"
   206  		}
   207  		return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "mod", []string{action})
   208  	case source.CommandUpdateGoSum:
   209  		var uri protocol.DocumentURI
   210  		if err := source.UnmarshalArgs(args, &uri); err != nil {
   211  			return err
   212  		}
   213  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
   214  		defer release()
   215  		if !ok {
   216  			return err
   217  		}
   218  		return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri.SpanURI(), "list", []string{"all"})
   219  	case source.CommandAddDependency, source.CommandUpgradeDependency, source.CommandRemoveDependency:
   220  		var uri protocol.DocumentURI
   221  		var goCmdArgs []string
   222  		var addRequire bool
   223  		if err := source.UnmarshalArgs(args, &uri, &addRequire, &goCmdArgs); err != nil {
   224  			return err
   225  		}
   226  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
   227  		defer release()
   228  		if !ok {
   229  			return err
   230  		}
   231  		return s.runGoGetModule(ctx, snapshot, uri.SpanURI(), addRequire, goCmdArgs)
   232  	case source.CommandGoGetPackage:
   233  		var uri protocol.DocumentURI
   234  		var pkg string
   235  		if err := source.UnmarshalArgs(args, &uri, &pkg); err != nil {
   236  			return err
   237  		}
   238  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
   239  		defer release()
   240  		if !ok {
   241  			return err
   242  		}
   243  		return s.runGoGetPackage(ctx, snapshot, uri.SpanURI(), pkg)
   244  
   245  	case source.CommandToggleDetails:
   246  		var fileURI protocol.DocumentURI
   247  		if err := source.UnmarshalArgs(args, &fileURI); err != nil {
   248  			return err
   249  		}
   250  		pkgDir := span.URIFromPath(filepath.Dir(fileURI.SpanURI().Filename()))
   251  		s.gcOptimizationDetailsMu.Lock()
   252  		if _, ok := s.gcOptimizationDetails[pkgDir]; ok {
   253  			delete(s.gcOptimizationDetails, pkgDir)
   254  			s.clearDiagnosticSource(gcDetailsSource)
   255  		} else {
   256  			s.gcOptimizationDetails[pkgDir] = struct{}{}
   257  		}
   258  		s.gcOptimizationDetailsMu.Unlock()
   259  		// need to recompute diagnostics.
   260  		// so find the snapshot
   261  		snapshot, _, ok, release, err := s.beginFileRequest(ctx, fileURI, source.UnknownKind)
   262  		defer release()
   263  		if !ok {
   264  			return err
   265  		}
   266  		s.diagnoseSnapshot(snapshot, nil, false)
   267  	case source.CommandGenerateGoplsMod:
   268  		var v source.View
   269  		if len(args) == 0 {
   270  			views := s.session.Views()
   271  			if len(views) != 1 {
   272  				return fmt.Errorf("cannot resolve view: have %d views", len(views))
   273  			}
   274  			v = views[0]
   275  		} else {
   276  			var uri protocol.DocumentURI
   277  			if err := source.UnmarshalArgs(args, &uri); err != nil {
   278  				return err
   279  			}
   280  			var err error
   281  			v, err = s.session.ViewOf(uri.SpanURI())
   282  			if err != nil {
   283  				return err
   284  			}
   285  		}
   286  		snapshot, release := v.Snapshot(ctx)
   287  		defer release()
   288  		modFile, err := cache.BuildGoplsMod(ctx, v.Folder(), snapshot)
   289  		if err != nil {
   290  			return errors.Errorf("getting workspace mod file: %w", err)
   291  		}
   292  		content, err := modFile.Format()
   293  		if err != nil {
   294  			return errors.Errorf("formatting mod file: %w", err)
   295  		}
   296  		filename := filepath.Join(v.Folder().Filename(), "gopls.mod")
   297  		if err := ioutil.WriteFile(filename, content, 0644); err != nil {
   298  			return errors.Errorf("writing mod file: %w", err)
   299  		}
   300  	default:
   301  		return fmt.Errorf("unsupported command: %s", command.ID())
   302  	}
   303  	return nil
   304  }
   305  
   306  func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI, work *workDone, tests, benchmarks []string) error {
   307  	pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	if len(pkgs) == 0 {
   312  		return fmt.Errorf("package could not be found for file: %s", uri.SpanURI().Filename())
   313  	}
   314  	pkgPath := pkgs[0].PkgPath()
   315  
   316  	// create output
   317  	buf := &bytes.Buffer{}
   318  	ew := &eventWriter{ctx: ctx, operation: "test"}
   319  	out := io.MultiWriter(ew, workDoneWriter{work}, buf)
   320  
   321  	// Run `go test -run Func` on each test.
   322  	var failedTests int
   323  	for _, funcName := range tests {
   324  		inv := &gocommand.Invocation{
   325  			Verb:       "test",
   326  			Args:       []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)},
   327  			WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
   328  		}
   329  		if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
   330  			if errors.Is(err, context.Canceled) {
   331  				return err
   332  			}
   333  			failedTests++
   334  		}
   335  	}
   336  
   337  	// Run `go test -run=^$ -bench Func` on each test.
   338  	var failedBenchmarks int
   339  	for _, funcName := range benchmarks {
   340  		inv := &gocommand.Invocation{
   341  			Verb:       "test",
   342  			Args:       []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)},
   343  			WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
   344  		}
   345  		if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
   346  			if errors.Is(err, context.Canceled) {
   347  				return err
   348  			}
   349  			failedBenchmarks++
   350  		}
   351  	}
   352  
   353  	var title string
   354  	if len(tests) > 0 && len(benchmarks) > 0 {
   355  		title = "tests and benchmarks"
   356  	} else if len(tests) > 0 {
   357  		title = "tests"
   358  	} else if len(benchmarks) > 0 {
   359  		title = "benchmarks"
   360  	} else {
   361  		return errors.New("No functions were provided")
   362  	}
   363  	message := fmt.Sprintf("all %s passed", title)
   364  	if failedTests > 0 && failedBenchmarks > 0 {
   365  		message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks))
   366  	} else if failedTests > 0 {
   367  		message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests))
   368  	} else if failedBenchmarks > 0 {
   369  		message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks))
   370  	}
   371  	if failedTests > 0 || failedBenchmarks > 0 {
   372  		message += "\n" + buf.String()
   373  	}
   374  
   375  	return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
   376  		Type:    protocol.Info,
   377  		Message: message,
   378  	})
   379  }
   380  
   381  func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, dir span.URI, recursive bool, work *workDone) error {
   382  	ctx, cancel := context.WithCancel(ctx)
   383  	defer cancel()
   384  
   385  	er := &eventWriter{ctx: ctx, operation: "generate"}
   386  
   387  	pattern := "."
   388  	if recursive {
   389  		pattern = "./..."
   390  	}
   391  
   392  	inv := &gocommand.Invocation{
   393  		Verb:       "generate",
   394  		Args:       []string{"-x", pattern},
   395  		WorkingDir: dir.Filename(),
   396  	}
   397  	stderr := io.MultiWriter(er, workDoneWriter{work})
   398  	if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {
   399  		return err
   400  	}
   401  	return nil
   402  }
   403  
   404  func (s *Server) runGoGetPackage(ctx context.Context, snapshot source.Snapshot, uri span.URI, pkg string) error {
   405  	stdout, err := snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{
   406  		Verb:       "list",
   407  		Args:       []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", pkg},
   408  		WorkingDir: filepath.Dir(uri.Filename()),
   409  	})
   410  	if err != nil {
   411  		return err
   412  	}
   413  	ver := strings.TrimSpace(stdout.String())
   414  	return s.runGoGetModule(ctx, snapshot, uri, true, []string{ver})
   415  }
   416  
   417  func (s *Server) runGoGetModule(ctx context.Context, snapshot source.Snapshot, uri span.URI, addRequire bool, args []string) error {
   418  	if addRequire {
   419  		// Using go get to create a new dependency results in an
   420  		// `// indirect` comment we may not want. The only way to avoid it
   421  		// is to add the require as direct first. Then we can use go get to
   422  		// update go.sum and tidy up.
   423  		if err := runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile, uri, "mod", append([]string{"edit", "-require"}, args...)); err != nil {
   424  			return err
   425  		}
   426  	}
   427  	return runSimpleGoCommand(ctx, snapshot, source.UpdateUserModFile|source.AllowNetwork, uri, "get", append([]string{"-d"}, args...))
   428  }
   429  
   430  func runSimpleGoCommand(ctx context.Context, snapshot source.Snapshot, mode source.InvocationFlags, uri span.URI, verb string, args []string) error {
   431  	_, err := snapshot.RunGoCommandDirect(ctx, mode, &gocommand.Invocation{
   432  		Verb:       verb,
   433  		Args:       args,
   434  		WorkingDir: filepath.Dir(uri.Filename()),
   435  	})
   436  	return err
   437  }