-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
gopls/internal/cmd: add 'execute' command
The execute command runs an arbitrary ExecuteCommand operation. Plus tests. Also, fix two existing bugs: - update the list of async commands ("run_tests" not "test")... though "test" appears to behave asynchronously too. - properly regexp-quote -run=TestFoo argument. Fixes golang/go#64428 Change-Id: I918857414ba911383b2c925a191e6fe3cb868994 Reviewed-on: https://go-review.googlesource.com/c/tools/+/546419 Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
- Loading branch information
Showing
15 changed files
with
365 additions
and
95 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
// 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"}' | ||
$ gopls execute gopls.run_govulncheck '{"URI": "file:///go.mod"}' | ||
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.