Skip to content

Commit

Permalink
gopls/internal/cmd: add 'execute' command
Browse files Browse the repository at this point in the history
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
adonovan committed Dec 1, 2023
1 parent d146c60 commit c4f958a
Show file tree
Hide file tree
Showing 15 changed files with 365 additions and 95 deletions.
27 changes: 14 additions & 13 deletions gopls/doc/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Args:
}
```

### **update the given telemetry counters.**
### **Update the given telemetry counters**
Identifier: `gopls.add_telemetry_counters`

Gopls will prepend "fwd/" to all the counters updated using this command
Expand Down Expand Up @@ -84,7 +84,7 @@ Args:
}
```

### **performs a "change signature" refactoring.**
### **Perform a "change signature" refactoring**
Identifier: `gopls.change_signature`

This command is experimental, currently only supporting parameter removal.
Expand Down Expand Up @@ -183,7 +183,7 @@ Args:
}
```

### **go get a package**
### **'go get' a package**
Identifier: `gopls.go_get_package`

Runs `go get` to fetch a package.
Expand Down Expand Up @@ -258,13 +258,14 @@ Result:
}
```

### **checks for the right conditions, and then prompts**
### **Prompt user to enable telemetry**
Identifier: `gopls.maybe_prompt_for_telemetry`

the user to ask if they want to enable Go telemetry uploading. If the user
responds 'Yes', the telemetry mode is set to "on".
Checks for the right conditions, and then prompts the user
to ask if they want to enable Go telemetry uploading. If
the user responds 'Yes', the telemetry mode is set to "on".

### **fetch memory statistics**
### **Fetch memory statistics**
Identifier: `gopls.mem_stats`

Call runtime.GC multiple times and return memory statistics as reported by
Expand Down Expand Up @@ -334,10 +335,10 @@ Args:
}
```

### **run `go work [args...]`, and apply the resulting go.work**
### **Run `go work [args...]`, and apply the resulting go.work**
Identifier: `gopls.run_go_work_command`

edits to the current go.work file.
edits to the current go.work file

Args:

Expand All @@ -349,7 +350,7 @@ Args:
}
```

### **Run vulncheck.**
### **Run vulncheck**
Identifier: `gopls.run_govulncheck`

Run vulnerability check (`govulncheck`).
Expand Down Expand Up @@ -438,7 +439,7 @@ Result:
}
```

### **start capturing a profile of gopls' execution.**
### **Start capturing a profile of gopls' execution**
Identifier: `gopls.start_profile`

Start a new pprof profile. Before using the resulting file, profiling must
Expand All @@ -459,7 +460,7 @@ Result:
struct{}
```

### **stop an ongoing profile.**
### **Stop an ongoing profile**
Identifier: `gopls.stop_profile`

This command is intended for internal use only, by the gopls benchmark
Expand Down Expand Up @@ -567,7 +568,7 @@ Args:
}
```

### **fetch workspace statistics**
### **Fetch workspace statistics**
Identifier: `gopls.workspace_stats`

Query statistics about workspace builds, modules, packages, and files.
Expand Down
2 changes: 1 addition & 1 deletion gopls/doc/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ Runs `go generate` for a given directory.
Identifier: `regenerate_cgo`

Regenerates cgo definitions.
### **Run vulncheck.**
### **Run vulncheck**

Identifier: `run_govulncheck`

Expand Down
1 change: 1 addition & 0 deletions gopls/internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func (app *Application) featureCommands() []tool.Application {
&check{app: app},
&codelens{app: app},
&definition{app: app},
&execute{app: app},
&foldingRanges{app: app},
&format{app: app},
&highlight{app: app},
Expand Down
47 changes: 3 additions & 44 deletions gopls/internal/cmd/codelens.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"flag"
"fmt"

"golang.org/x/tools/gopls/internal/lsp/command"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/tool"
Expand Down Expand Up @@ -83,22 +82,7 @@ func (r *codelens) Run(ctx context.Context, args ...string) error {
}

// TODO(adonovan): cleanup: factor progress with stats subcommand.
const cmdProgressToken = "cmd-progress"
cmdDone := make(chan bool)
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.Println(v.Message)

case *protocol.WorkDoneProgressEnd:
if p.Token == cmdProgressToken {
// commandHandler.run sends message = canceled | failed | completed
cmdDone <- v.Message == "completed"
}
}
}
cmdDone, onProgress := commandProgress()

conn, err := r.app.connect(ctx, onProgress)
if err != nil {
Expand Down Expand Up @@ -139,33 +123,8 @@ func (r *codelens) Run(ctx context.Context, args ...string) error {

// -exec: run the first matching code lens.
if r.Exec {
// Start the command.
if _, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
Command: lens.Command.Command,
Arguments: lens.Command.Arguments,
WorkDoneProgressParams: protocol.WorkDoneProgressParams{
WorkDoneToken: cmdProgressToken,
},
}); err != nil {
return err
}

// Wait for it to finish, if it is asynchronous
// and honors progress tokens.
//
// TODO(adonovan): extract this list more
// robustly. from lsp.commandConfig.async.
switch lens.Command.Command {
case "gopls." + string(command.RunGovulncheck),
"gopls." + string(command.Test):
if ok := <-cmdDone; !ok {
// TODO(adonovan): suppress this message;
// the command's stderr should suffice.
return fmt.Errorf("command failed")
}
}

return nil
_, err := conn.executeCommand(ctx, cmdDone, lens.Command)
return err
}

// No -exec: list matching code lenses.
Expand Down
156 changes: 156 additions & 0 deletions gopls/internal/cmd/execute.go
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
}
4 changes: 3 additions & 1 deletion gopls/internal/cmd/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

package cmd

// This file defines the help, bug, version, api-json, licenses commands.

import (
"bytes"
"context"
Expand Down Expand Up @@ -216,7 +218,7 @@ type apiJSON struct {
func (j *apiJSON) Name() string { return "api-json" }
func (j *apiJSON) Parent() string { return j.app.Name() }
func (j *apiJSON) Usage() string { return "" }
func (j *apiJSON) ShortHelp() string { return "print json describing gopls API" }
func (j *apiJSON) ShortHelp() string { return "print JSON describing gopls API" }
func (j *apiJSON) DetailedHelp(f *flag.FlagSet) {
fmt.Fprint(f.Output(), ``)
printFlagDefaults(f)
Expand Down
Loading

0 comments on commit c4f958a

Please sign in to comment.