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 5package lsp 6 7import ( 8 "context" 9 "fmt" 10 "io" 11 "path" 12 "strings" 13 14 "golang.org/x/tools/internal/event" 15 "golang.org/x/tools/internal/lsp/debug/tag" 16 "golang.org/x/tools/internal/lsp/protocol" 17 "golang.org/x/tools/internal/lsp/source" 18 "golang.org/x/tools/internal/span" 19 "golang.org/x/tools/internal/xcontext" 20 errors "golang.org/x/xerrors" 21) 22 23func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { 24 var command *source.Command 25 for _, c := range source.Commands { 26 if c.Name == params.Command { 27 command = c 28 break 29 } 30 } 31 if command == nil { 32 return nil, fmt.Errorf("no known command") 33 } 34 var match bool 35 for _, name := range s.session.Options().SupportedCommands { 36 if command.Name == name { 37 match = true 38 break 39 } 40 } 41 if !match { 42 return nil, fmt.Errorf("%s is not a supported command", command.Name) 43 } 44 // Some commands require that all files are saved to disk. If we detect 45 // unsaved files, warn the user instead of running the commands. 46 unsaved := false 47 for _, overlay := range s.session.Overlays() { 48 if !overlay.Saved() { 49 unsaved = true 50 break 51 } 52 } 53 if unsaved { 54 switch params.Command { 55 case source.CommandTest.Name, source.CommandGenerate.Name, source.CommandToggleDetails.Name: 56 // TODO(PJW): for Toggle, not an error if it is being disabled 57 return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 58 Type: protocol.Error, 59 Message: fmt.Sprintf("cannot run command %s: unsaved files in the view", params.Command), 60 }) 61 } 62 } 63 // If the command has a suggested fix function available, use it and apply 64 // the edits to the workspace. 65 if command.IsSuggestedFix() { 66 var uri protocol.DocumentURI 67 var rng protocol.Range 68 if err := source.UnmarshalArgs(params.Arguments, &uri, &rng); err != nil { 69 return nil, err 70 } 71 snapshot, fh, ok, release, err := s.beginFileRequest(ctx, uri, source.Go) 72 defer release() 73 if !ok { 74 return nil, err 75 } 76 edits, err := command.SuggestedFix(ctx, snapshot, fh, rng) 77 if err != nil { 78 return nil, err 79 } 80 r, err := s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ 81 Edit: protocol.WorkspaceEdit{ 82 DocumentChanges: edits, 83 }, 84 }) 85 if err != nil { 86 return nil, err 87 } 88 if !r.Applied { 89 return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 90 Type: protocol.Error, 91 Message: fmt.Sprintf("%s failed: %v", params.Command, r.FailureReason), 92 }) 93 } 94 return nil, nil 95 } 96 // Default commands that don't have suggested fix functions. 97 switch command { 98 case source.CommandTest: 99 var uri protocol.DocumentURI 100 var flag string 101 var funcName string 102 if err := source.UnmarshalArgs(params.Arguments, &uri, &flag, &funcName); err != nil { 103 return nil, err 104 } 105 snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind) 106 defer release() 107 if !ok { 108 return nil, err 109 } 110 go s.runTest(ctx, snapshot, []string{flag, funcName}) 111 case source.CommandGenerate: 112 var uri protocol.DocumentURI 113 var recursive bool 114 if err := source.UnmarshalArgs(params.Arguments, &uri, &recursive); err != nil { 115 return nil, err 116 } 117 snapshot, _, ok, release, err := s.beginFileRequest(ctx, uri, source.UnknownKind) 118 defer release() 119 if !ok { 120 return nil, err 121 } 122 go s.runGoGenerate(xcontext.Detach(ctx), snapshot, uri.SpanURI(), recursive) 123 case source.CommandRegenerateCgo: 124 var uri protocol.DocumentURI 125 if err := source.UnmarshalArgs(params.Arguments, &uri); err != nil { 126 return nil, err 127 } 128 mod := source.FileModification{ 129 URI: uri.SpanURI(), 130 Action: source.InvalidateMetadata, 131 } 132 err := s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo) 133 return nil, err 134 case source.CommandTidy, source.CommandVendor: 135 var uri protocol.DocumentURI 136 if err := source.UnmarshalArgs(params.Arguments, &uri); err != nil { 137 return nil, err 138 } 139 // The flow for `go mod tidy` and `go mod vendor` is almost identical, 140 // so we combine them into one case for convenience. 141 a := "tidy" 142 if command == source.CommandVendor { 143 a = "vendor" 144 } 145 err := s.directGoModCommand(ctx, uri, "mod", []string{a}...) 146 return nil, err 147 case source.CommandUpgradeDependency: 148 var uri protocol.DocumentURI 149 var goCmdArgs []string 150 if err := source.UnmarshalArgs(params.Arguments, &uri, &goCmdArgs); err != nil { 151 return nil, err 152 } 153 err := s.directGoModCommand(ctx, uri, "get", goCmdArgs...) 154 return nil, err 155 case source.CommandToggleDetails: 156 var fileURI span.URI 157 if err := source.UnmarshalArgs(params.Arguments, &fileURI); err != nil { 158 return nil, err 159 } 160 pkgDir := span.URIFromPath(path.Dir(fileURI.Filename())) 161 s.gcOptimizationDetailsMu.Lock() 162 if _, ok := s.gcOptimizatonDetails[pkgDir]; ok { 163 delete(s.gcOptimizatonDetails, pkgDir) 164 } else { 165 s.gcOptimizatonDetails[pkgDir] = struct{}{} 166 } 167 s.gcOptimizationDetailsMu.Unlock() 168 event.Log(ctx, fmt.Sprintf("gc_details %s now %v %v", pkgDir, s.gcOptimizatonDetails[pkgDir], 169 s.gcOptimizatonDetails)) 170 // need to recompute diagnostics. 171 // so find the snapshot 172 sv, err := s.session.ViewOf(fileURI) 173 if err != nil { 174 return nil, err 175 } 176 snapshot, release := sv.Snapshot() 177 defer release() 178 s.diagnoseSnapshot(snapshot) 179 return nil, nil 180 default: 181 return nil, fmt.Errorf("unknown command: %s", params.Command) 182 } 183 return nil, nil 184} 185 186func (s *Server) directGoModCommand(ctx context.Context, uri protocol.DocumentURI, verb string, args ...string) error { 187 view, err := s.session.ViewOf(uri.SpanURI()) 188 if err != nil { 189 return err 190 } 191 snapshot, release := view.Snapshot() 192 defer release() 193 return snapshot.RunGoCommandDirect(ctx, verb, args) 194} 195 196func (s *Server) runTest(ctx context.Context, snapshot source.Snapshot, args []string) error { 197 ctx, cancel := context.WithCancel(ctx) 198 defer cancel() 199 200 ew := &eventWriter{ctx: ctx, operation: "test"} 201 msg := fmt.Sprintf("running `go test %s`", strings.Join(args, " ")) 202 wc := s.newProgressWriter(ctx, "test", msg, msg, cancel) 203 defer wc.Close() 204 205 messageType := protocol.Info 206 message := "test passed" 207 stderr := io.MultiWriter(ew, wc) 208 209 if err := snapshot.RunGoCommandPiped(ctx, "test", args, ew, stderr); err != nil { 210 if errors.Is(err, context.Canceled) { 211 return err 212 } 213 messageType = protocol.Error 214 message = "test failed" 215 } 216 return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 217 Type: messageType, 218 Message: message, 219 }) 220} 221 222// GenerateWorkDoneTitle is the title used in progress reporting for go 223// generate commands. It is exported for testing purposes. 224const GenerateWorkDoneTitle = "generate" 225 226func (s *Server) runGoGenerate(ctx context.Context, snapshot source.Snapshot, uri span.URI, recursive bool) error { 227 ctx, cancel := context.WithCancel(ctx) 228 defer cancel() 229 230 er := &eventWriter{ctx: ctx, operation: "generate"} 231 wc := s.newProgressWriter(ctx, GenerateWorkDoneTitle, "running go generate", "started go generate, check logs for progress", cancel) 232 defer wc.Close() 233 args := []string{"-x"} 234 if recursive { 235 args = append(args, "./...") 236 } 237 238 stderr := io.MultiWriter(er, wc) 239 240 if err := snapshot.RunGoCommandPiped(ctx, "generate", args, er, stderr); err != nil { 241 if errors.Is(err, context.Canceled) { 242 return nil 243 } 244 event.Error(ctx, "generate: command error", err, tag.Directory.Of(uri.Filename())) 245 return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ 246 Type: protocol.Error, 247 Message: "go generate exited with an error, check gopls logs", 248 }) 249 } 250 return nil 251} 252