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 }