| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cmd |
| |
| import ( |
| "context" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "log" |
| "os" |
| "strings" |
| |
| "golang.org/x/tools/gopls/internal/lsp/command" |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| "golang.org/x/tools/gopls/internal/server" |
| "golang.org/x/tools/gopls/internal/util/slices" |
| "golang.org/x/tools/internal/tool" |
| ) |
| |
| // execute implements the LSP ExecuteCommand verb for gopls. |
| type execute struct { |
| EditFlags |
| app *Application |
| } |
| |
| func (e *execute) Name() string { return "execute" } |
| func (e *execute) Parent() string { return e.app.Name() } |
| func (e *execute) Usage() string { return "[flags] command argument..." } |
| func (e *execute) ShortHelp() string { return "Execute a gopls custom LSP command" } |
| func (e *execute) DetailedHelp(f *flag.FlagSet) { |
| fmt.Fprint(f.Output(), ` |
| The execute command sends an LSP ExecuteCommand request to gopls, |
| with a set of optional JSON argument values. |
| Some commands return a result, also JSON. |
| |
| Available commands are documented at: |
| |
| https://github.com/golang/tools/blob/master/gopls/doc/commands.md |
| |
| This interface is experimental and commands may change or disappear without notice. |
| |
| Examples: |
| |
| $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI", "file:///hello.go"}' |
| $ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}' |
| $ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}' |
| |
| execute-flags: |
| `) |
| printFlagDefaults(f) |
| } |
| |
| func (e *execute) Run(ctx context.Context, args ...string) error { |
| if len(args) == 0 { |
| return tool.CommandLineErrorf("execute requires a command name") |
| } |
| cmd := args[0] |
| if !slices.Contains(command.Commands, command.Command(strings.TrimPrefix(cmd, "gopls."))) { |
| return tool.CommandLineErrorf("unrecognized command: %s", cmd) |
| } |
| |
| // A command may have multiple arguments, though the only one |
| // that currently does so is the "legacy" gopls.test, |
| // so we don't show an example of it. |
| var jsonArgs []json.RawMessage |
| for i, arg := range args[1:] { |
| var dummy any |
| if err := json.Unmarshal([]byte(arg), &dummy); err != nil { |
| return fmt.Errorf("argument %d is not valid JSON: %v", i+1, err) |
| } |
| jsonArgs = append(jsonArgs, json.RawMessage(arg)) |
| } |
| |
| e.app.editFlags = &e.EditFlags // in case command performs an edit |
| |
| cmdDone, onProgress := commandProgress() |
| conn, err := e.app.connect(ctx, onProgress) |
| if err != nil { |
| return err |
| } |
| defer conn.terminate(ctx) |
| |
| res, err := conn.executeCommand(ctx, cmdDone, &protocol.Command{ |
| Command: cmd, |
| Arguments: jsonArgs, |
| }) |
| if err != nil { |
| return err |
| } |
| if res != nil { |
| data, err := json.MarshalIndent(res, "", "\t") |
| if err != nil { |
| log.Fatal(err) |
| } |
| fmt.Printf("%s\n", data) |
| } |
| return nil |
| } |
| |
| // -- shared command helpers -- |
| |
| const cmdProgressToken = "cmd-progress" |
| |
| // TODO(adonovan): disentangle this from app.connect, and factor with |
| // conn.executeCommand used by codelens and execute. Seems like |
| // connection needs a way to register and unregister independent |
| // handlers, later than at connect time. |
| func commandProgress() (<-chan bool, func(p *protocol.ProgressParams)) { |
| cmdDone := make(chan bool, 1) |
| onProgress := func(p *protocol.ProgressParams) { |
| switch v := p.Value.(type) { |
| case *protocol.WorkDoneProgressReport: |
| // TODO(adonovan): how can we segregate command's stdout and |
| // stderr so that structure is preserved? |
| fmt.Fprintln(os.Stderr, v.Message) |
| |
| case *protocol.WorkDoneProgressEnd: |
| if p.Token == cmdProgressToken { |
| // commandHandler.run sends message = canceled | failed | completed |
| cmdDone <- v.Message == server.CommandCompleted |
| } |
| } |
| } |
| return cmdDone, onProgress |
| } |
| |
| func (conn *connection) executeCommand(ctx context.Context, done <-chan bool, cmd *protocol.Command) (any, error) { |
| res, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ |
| Command: cmd.Command, |
| Arguments: cmd.Arguments, |
| WorkDoneProgressParams: protocol.WorkDoneProgressParams{ |
| WorkDoneToken: cmdProgressToken, |
| }, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Wait for it to finish (by watching for a progress token). |
| // |
| // In theory this is only necessary for the two async |
| // commands (RunGovulncheck and RunTests), but the tests |
| // fail for Test as well (why?), and there is no cost to |
| // waiting in all cases. TODO(adonovan): investigate. |
| if success := <-done; !success { |
| // TODO(adonovan): suppress this message; |
| // the command's stderr should suffice. |
| return nil, fmt.Errorf("command failed") |
| } |
| |
| return res, nil |
| } |