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.
16 "golang.org/x/tools/internal/event"
17 "golang.org/x/tools/internal/gocommand"
18 "golang.org/x/tools/internal/lsp/cache"
19 "golang.org/x/tools/internal/lsp/protocol"
20 "golang.org/x/tools/internal/lsp/source"
21 "golang.org/x/tools/internal/span"
22 "golang.org/x/tools/internal/xcontext"
23 errors "golang.org/x/xerrors"
26 func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
27 var command *source.Command
28 for _, c := range source.Commands {
29 if c.ID() == params.Command {
35 return nil, fmt.Errorf("no known command")
38 for _, name := range s.session.Options().SupportedCommands {
39 if command.ID() == name {
45 return nil, fmt.Errorf("%s is not a supported command", command.ID())
47 // Some commands require that all files are saved to disk. If we detect
48 // unsaved files, warn the user instead of running the commands.
50 for _, overlay := range s.session.Overlays() {
57 switch params.Command {
58 case source.CommandTest.ID(),
59 source.CommandGenerate.ID(),
60 source.CommandToggleDetails.ID(),
61 source.CommandAddDependency.ID(),
62 source.CommandUpgradeDependency.ID(),
63 source.CommandRemoveDependency.ID(),
64 source.CommandVendor.ID():
65 // TODO(PJW): for Toggle, not an error if it is being disabled
66 err := errors.New("all files must be saved first")
67 s.showCommandError(ctx, command.Title, err)
71 ctx, cancel := context.WithCancel(xcontext.Detach(ctx))
74 // Don't show progress for suggested fixes. They should be quick.
75 if !command.IsSuggestedFix() {
76 // Start progress prior to spinning off a goroutine specifically so that
77 // clients are aware of the work item before the command completes. This
78 // matters for regtests, where having a continuous thread of work is
79 // convenient for assertions.
80 work = s.progress.start(ctx, command.Title, "Running...", params.WorkDoneToken, cancel)
85 s.runCommand(ctx, work, command, params.Arguments)
90 return nil, s.runCommand(ctx, work, command, params.Arguments)
93 func (s *Server) runSuggestedFixCommand(ctx context.Context, command *source.Command, args []json.RawMessage) error {
94 var uri protocol.DocumentURI
95 var rng protocol.Range
96 if err := source.UnmarshalArgs(args, &uri, &rng); err != nil {
99 snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go)
104 edits, err := command.SuggestedFix(ctx, snapshot, fh, rng)
108 r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{
109 Edit: protocol.WorkspaceEdit{
110 DocumentChanges: edits,
117 return errors.New(r.FailureReason)
122 func (s *Server) showCommandError(ctx context.Context, title string, err error) {
123 // Command error messages should not be cancelable.
124 ctx = xcontext.Detach(ctx)
125 if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
126 Type: protocol.Error,
127 Message: fmt.Sprintf("%s failed: %v", title, err),
129 event.Error(ctx, title+": failed to show message", err)
133 func (s *Server) runCommand(ctx context.Context, work *workDone, command *source.Command, args []json.RawMessage) (err error) {
136 case errors.Is(err, context.Canceled):
137 work.end(command.Title + ": canceled")
139 event.Error(ctx, fmt.Sprintf("%s: command error", command.Title), err)
140 work.end(command.Title + ": failed")
141 // Show a message when work completes with error, because the progress end
142 // message is typically dismissed immediately by LSP clients.
143 s.showCommandError(ctx, command.Title, err)
145 work.end(command.ID() + ": completed")
148 // If the command has a suggested fix function available, use it and apply
149 // the edits to the workspace.
150 if command.IsSuggestedFix() {
151 return s.runSuggestedFixCommand(ctx, command, args)
154 case source.CommandTest:
155 var uri protocol.DocumentURI
156 var tests, benchmarks []string
157 if err := source.UnmarshalArgs(args, &uri, &tests, &benchmarks); err != nil {
160 snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
165 return s.runTests(ctx, snapshot, uri, work, tests, benchmarks)
166 case source.CommandGenerate:
167 var uri protocol.DocumentURI
169 if err := source.UnmarshalArgs(args, &uri, &recursive); err != nil {
172 snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind)
177 return s.runGoGenerate(ctx, snapshot, uri.SpanURI(), recursive, work)
178 case source.CommandRegenerateCgo:
179 var uri protocol.DocumentURI
180 if err := source.UnmarshalArgs(args, &uri); err != nil {
183 mod := source.FileModification{
185 Action: source.InvalidateMetadata,
187 return s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo)
188 case source.CommandTidy, source.CommandVendor:
189 var uri protocol.DocumentURI
190 if err := source.UnmarshalArgs(args, &uri); err != nil {
193 // The flow for `go mod tidy` and `go mod vendor` is almost identical,
194 // so we combine them into one case for convenience.
196 if command == source.CommandVendor {
199 return s.directGoModCommand(ctx, uri, "mod", a)
200 case source.CommandAddDependency, source.CommandUpgradeDependency, source.CommandRemoveDependency:
201 var uri protocol.DocumentURI
202 var goCmdArgs []string
204 if err := source.UnmarshalArgs(args, &uri, &addRequire, &goCmdArgs); err != nil {
208 // Using go get to create a new dependency results in an
209 // `// indirect` comment we may not want. The only way to avoid it
210 // is to add the require as direct first. Then we can use go get to
211 // update go.sum and tidy up.
212 if err := s.directGoModCommand(ctx, uri, "mod", append([]string{"edit", "-require"}, goCmdArgs...)...); err != nil {
216 return s.directGoModCommand(ctx, uri, "get", append([]string{"-d"}, goCmdArgs...)...)
217 case source.CommandToggleDetails:
219 if err := source.UnmarshalArgs(args, &fileURI); err != nil {
222 pkgDir := span.URIFromPath(filepath.Dir(fileURI.Filename()))
223 s.gcOptimizationDetailsMu.Lock()
224 if _, ok := s.gcOptimizationDetails[pkgDir]; ok {
225 delete(s.gcOptimizationDetails, pkgDir)
227 s.gcOptimizationDetails[pkgDir] = struct{}{}
229 s.gcOptimizationDetailsMu.Unlock()
230 // need to recompute diagnostics.
231 // so find the snapshot
232 sv, err := s.session.ViewOf(fileURI)
236 snapshot, release := sv.Snapshot(ctx)
238 s.diagnoseSnapshot(snapshot, nil)
239 case source.CommandGenerateGoplsMod:
242 views := s.session.Views()
244 return fmt.Errorf("cannot resolve view: have %d views", len(views))
248 var uri protocol.DocumentURI
249 if err := source.UnmarshalArgs(args, &uri); err != nil {
253 v, err = s.session.ViewOf(uri.SpanURI())
258 snapshot, release := v.Snapshot(ctx)
260 modFile, err := cache.BuildGoplsMod(ctx, v.Folder(), snapshot)
262 return errors.Errorf("getting workspace mod file: %w", err)
264 content, err := modFile.Format()
266 return errors.Errorf("formatting mod file: %w", err)
268 filename := filepath.Join(v.Folder().Filename(), "gopls.mod")
269 if err := ioutil.WriteFile(filename, content, 0644); err != nil {
270 return errors.Errorf("writing mod file: %w", err)
273 return fmt.Errorf("unsupported command: %s", command.ID())
278 func (s *Server) directGoModCommand(ctx context.Context, uri protocol.DocumentURI, verb string, args ...string) error {
279 view, err := s.session.ViewOf(uri.SpanURI())
283 snapshot, release := view.Snapshot(ctx)
285 _, err = snapshot.RunGoCommandDirect(ctx, source.UpdateUserModFile, &gocommand.Invocation{
288 WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
293 func (s *Server) runTests(ctx context.Context, snapshot source.Snapshot, uri protocol.DocumentURI, work *workDone, tests, benchmarks []string) error {
294 pkgs, err := snapshot.PackagesForFile(ctx, uri.SpanURI(), source.TypecheckWorkspace)
299 return fmt.Errorf("package could not be found for file: %s", uri.SpanURI().Filename())
301 pkgPath := pkgs[0].PkgPath()
304 buf := &bytes.Buffer{}
305 ew := &eventWriter{ctx: ctx, operation: "test"}
306 out := io.MultiWriter(ew, workDoneWriter{work}, buf)
308 // Run `go test -run Func` on each test.
310 for _, funcName := range tests {
311 inv := &gocommand.Invocation{
313 Args: []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)},
314 WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
316 if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
317 if errors.Is(err, context.Canceled) {
324 // Run `go test -run=^$ -bench Func` on each test.
325 var failedBenchmarks int
326 for _, funcName := range benchmarks {
327 inv := &gocommand.Invocation{
329 Args: []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)},
330 WorkingDir: filepath.Dir(uri.SpanURI().Filename()),
332 if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil {
333 if errors.Is(err, context.Canceled) {
341 if len(tests) > 0 && len(benchmarks) > 0 {
342 title = "tests and benchmarks"
343 } else if len(tests) > 0 {
345 } else if len(benchmarks) > 0 {
348 return errors.New("No functions were provided")
350 message := fmt.Sprintf("all %s passed", title)
351 if failedTests > 0 && failedBenchmarks > 0 {
352 message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks))
353 } else if failedTests > 0 {
354 message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests))
355 } else if failedBenchmarks > 0 {
356 message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks))
358 messageType := protocol.Info
359 if failedTests > 0 || failedBenchmarks > 0 {
360 messageType = protocol.Error
361 message += "\n" + buf.String()
364 return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
370 func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, dir span.URI, recursive bool, work *workDone) error {
371 ctx, cancel := context.WithCancel(ctx)
374 er := &eventWriter{ctx: ctx, operation: "generate"}
381 inv := &gocommand.Invocation{
383 Args: []string{"-x", pattern},
384 WorkingDir: dir.Filename(),
386 stderr := io.MultiWriter(er, workDoneWriter{work})
387 if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil {