golang.org/x/tools/gopls@v0.15.3/internal/cmd/execute.go (about) 1 // Copyright 2023 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 cmd 6 7 import ( 8 "context" 9 "encoding/json" 10 "flag" 11 "fmt" 12 "log" 13 "os" 14 "strings" 15 16 "golang.org/x/tools/gopls/internal/protocol" 17 "golang.org/x/tools/gopls/internal/protocol/command" 18 "golang.org/x/tools/gopls/internal/server" 19 "golang.org/x/tools/gopls/internal/util/slices" 20 "golang.org/x/tools/internal/tool" 21 ) 22 23 // execute implements the LSP ExecuteCommand verb for gopls. 24 type execute struct { 25 EditFlags 26 app *Application 27 } 28 29 func (e *execute) Name() string { return "execute" } 30 func (e *execute) Parent() string { return e.app.Name() } 31 func (e *execute) Usage() string { return "[flags] command argument..." } 32 func (e *execute) ShortHelp() string { return "Execute a gopls custom LSP command" } 33 func (e *execute) DetailedHelp(f *flag.FlagSet) { 34 fmt.Fprint(f.Output(), ` 35 The execute command sends an LSP ExecuteCommand request to gopls, 36 with a set of optional JSON argument values. 37 Some commands return a result, also JSON. 38 39 Available commands are documented at: 40 41 https://github.com/golang/tools/blob/master/gopls/doc/commands.md 42 43 This interface is experimental and commands may change or disappear without notice. 44 45 Examples: 46 47 $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI": "file:///hello.go"}' 48 $ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}' 49 $ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}' 50 51 execute-flags: 52 `) 53 printFlagDefaults(f) 54 } 55 56 func (e *execute) Run(ctx context.Context, args ...string) error { 57 if len(args) == 0 { 58 return tool.CommandLineErrorf("execute requires a command name") 59 } 60 cmd := args[0] 61 if !slices.Contains(command.Commands, command.Command(strings.TrimPrefix(cmd, "gopls."))) { 62 return tool.CommandLineErrorf("unrecognized command: %s", cmd) 63 } 64 65 // A command may have multiple arguments, though the only one 66 // that currently does so is the "legacy" gopls.test, 67 // so we don't show an example of it. 68 var jsonArgs []json.RawMessage 69 for i, arg := range args[1:] { 70 var dummy any 71 if err := json.Unmarshal([]byte(arg), &dummy); err != nil { 72 return fmt.Errorf("argument %d is not valid JSON: %v", i+1, err) 73 } 74 jsonArgs = append(jsonArgs, json.RawMessage(arg)) 75 } 76 77 e.app.editFlags = &e.EditFlags // in case command performs an edit 78 79 cmdDone, onProgress := commandProgress() 80 conn, err := e.app.connect(ctx, onProgress) 81 if err != nil { 82 return err 83 } 84 defer conn.terminate(ctx) 85 86 res, err := conn.executeCommand(ctx, cmdDone, &protocol.Command{ 87 Command: cmd, 88 Arguments: jsonArgs, 89 }) 90 if err != nil { 91 return err 92 } 93 if res != nil { 94 data, err := json.MarshalIndent(res, "", "\t") 95 if err != nil { 96 log.Fatal(err) 97 } 98 fmt.Printf("%s\n", data) 99 } 100 return nil 101 } 102 103 // -- shared command helpers -- 104 105 const cmdProgressToken = "cmd-progress" 106 107 // TODO(adonovan): disentangle this from app.connect, and factor with 108 // conn.executeCommand used by codelens and execute. Seems like 109 // connection needs a way to register and unregister independent 110 // handlers, later than at connect time. 111 func commandProgress() (<-chan bool, func(p *protocol.ProgressParams)) { 112 cmdDone := make(chan bool, 1) 113 onProgress := func(p *protocol.ProgressParams) { 114 switch v := p.Value.(type) { 115 case *protocol.WorkDoneProgressReport: 116 // TODO(adonovan): how can we segregate command's stdout and 117 // stderr so that structure is preserved? 118 fmt.Fprintln(os.Stderr, v.Message) 119 120 case *protocol.WorkDoneProgressEnd: 121 if p.Token == cmdProgressToken { 122 // commandHandler.run sends message = canceled | failed | completed 123 cmdDone <- v.Message == server.CommandCompleted 124 } 125 } 126 } 127 return cmdDone, onProgress 128 } 129 130 func (conn *connection) executeCommand(ctx context.Context, done <-chan bool, cmd *protocol.Command) (any, error) { 131 res, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ 132 Command: cmd.Command, 133 Arguments: cmd.Arguments, 134 WorkDoneProgressParams: protocol.WorkDoneProgressParams{ 135 WorkDoneToken: cmdProgressToken, 136 }, 137 }) 138 if err != nil { 139 return nil, err 140 } 141 142 // Wait for it to finish (by watching for a progress token). 143 // 144 // In theory this is only necessary for the two async 145 // commands (RunGovulncheck and RunTests), but the tests 146 // fail for Test as well (why?), and there is no cost to 147 // waiting in all cases. TODO(adonovan): investigate. 148 if success := <-done; !success { 149 // TODO(adonovan): suppress this message; 150 // the command's stderr should suffice. 151 return nil, fmt.Errorf("command failed") 152 } 153 154 return res, nil 155 }