From c4f958a653396861b08b28c040959ad2853f674e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 30 Nov 2023 17:44:07 -0500 Subject: [PATCH] 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 LUCI-TryBot-Result: Go LUCI --- gopls/doc/commands.md | 27 ++-- gopls/doc/settings.md | 2 +- gopls/internal/cmd/cmd.go | 1 + gopls/internal/cmd/codelens.go | 47 +------ gopls/internal/cmd/execute.go | 156 ++++++++++++++++++++++++ gopls/internal/cmd/info.go | 4 +- gopls/internal/cmd/integration_test.go | 78 +++++++++++- gopls/internal/cmd/usage/api-json.hlp | 2 +- gopls/internal/cmd/usage/execute.hlp | 31 +++++ gopls/internal/cmd/usage/usage-v.hlp | 3 +- gopls/internal/cmd/usage/usage.hlp | 3 +- gopls/internal/lsp/command/interface.go | 36 ++++-- gopls/internal/server/command.go | 28 ++++- gopls/internal/settings/api_json.go | 26 ++-- gopls/internal/util/slices/slices.go | 16 +++ 15 files changed, 365 insertions(+), 95 deletions(-) create mode 100644 gopls/internal/cmd/execute.go create mode 100644 gopls/internal/cmd/usage/execute.hlp create mode 100644 gopls/internal/util/slices/slices.go diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 3404c91c7e5..6ddd1a56de7 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -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 @@ -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. @@ -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. @@ -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 @@ -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: @@ -349,7 +350,7 @@ Args: } ``` -### **Run vulncheck.** +### **Run vulncheck** Identifier: `gopls.run_govulncheck` Run vulnerability check (`govulncheck`). @@ -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 @@ -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 @@ -567,7 +568,7 @@ Args: } ``` -### **fetch workspace statistics** +### **Fetch workspace statistics** Identifier: `gopls.workspace_stats` Query statistics about workspace builds, modules, packages, and files. diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 47cd211d898..4ded2904c82 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -553,7 +553,7 @@ Runs `go generate` for a given directory. Identifier: `regenerate_cgo` Regenerates cgo definitions. -### **Run vulncheck.** +### **Run vulncheck** Identifier: `run_govulncheck` diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index fac27a5541a..0e53eaf1f2d 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go @@ -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}, diff --git a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go index 85eabc08bc6..f861eca1b5e 100644 --- a/gopls/internal/cmd/codelens.go +++ b/gopls/internal/cmd/codelens.go @@ -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" @@ -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 { @@ -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. diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go new file mode 100644 index 00000000000..405d87f09bd --- /dev/null +++ b/gopls/internal/cmd/execute.go @@ -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 +} diff --git a/gopls/internal/cmd/info.go b/gopls/internal/cmd/info.go index efaaed4300b..95e15fc18d7 100644 --- a/gopls/internal/cmd/info.go +++ b/gopls/internal/cmd/info.go @@ -4,6 +4,8 @@ package cmd +// This file defines the help, bug, version, api-json, licenses commands. + import ( "bytes" "context" @@ -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) diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 4d40b6f334d..db7609d161d 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -205,14 +205,14 @@ func TestFail(t *testing.T) { t.Fatal("fail") } { res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:3", "run test") res.checkExit(true) - res.checkStdout(`PASS: TestPass`) // from go test + res.checkStderr(`PASS: TestPass`) // from go test res.checkStderr("Info: all tests passed") // from gopls.test } // run the failing test { res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:4", "run test") res.checkExit(false) - res.checkStdout(`FAIL example.com/a`) + res.checkStderr(`FAIL example.com/a`) res.checkStderr("Info: 1 / 1 tests failed") } } @@ -268,6 +268,80 @@ func g() { } } +// TestExecute tests the 'execute' subcommand (../execute.go). +func TestExecute(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ` +-- go.mod -- +module example.com +go 1.18 + +-- hello.go -- +package a +func main() {} + +-- hello_test.go -- +package a +import "testing" +func TestHello(t *testing.T) { + t.Fatal("oops") +} +`) + // missing command name + { + res := gopls(t, tree, "execute") + res.checkExit(false) + res.checkStderr("requires a command") + } + // bad command + { + res := gopls(t, tree, "execute", "gopls.foo") + res.checkExit(false) + res.checkStderr("unrecognized command: gopls.foo") + } + // too few arguments + { + res := gopls(t, tree, "execute", "gopls.run_tests") + res.checkExit(false) + res.checkStderr("expected 1 input arguments, got 0") + } + // too many arguments + { + res := gopls(t, tree, "execute", "gopls.run_tests", "null", "null") + res.checkExit(false) + res.checkStderr("expected 1 input arguments, got 2") + } + // argument is not JSON + { + res := gopls(t, tree, "execute", "gopls.run_tests", "hello") + res.checkExit(false) + res.checkStderr("argument 1 is not valid JSON: invalid character 'h'") + } + // add import, show diff + hello := "file://" + filepath.ToSlash(tree) + "/hello.go" + { + res := gopls(t, tree, "execute", "-d", "gopls.add_import", `{"ImportPath": "fmt", "URI": "`+hello+`"}`) + res.checkExit(true) + res.checkStdout(`[+]import "fmt"`) + } + // list known packages (has a result) + { + res := gopls(t, tree, "execute", "gopls.list_known_packages", `{"URI": "`+hello+`"}`) + res.checkExit(true) + res.checkStdout(`"fmt"`) + res.checkStdout(`"encoding/json"`) + } + // run tests + { + helloTest := "file://" + filepath.ToSlash(tree) + "/hello_test.go" + res := gopls(t, tree, "execute", "gopls.run_tests", `{"URI": "`+helloTest+`", "Tests": ["TestHello"]}`) + res.checkExit(false) + res.checkStderr(`hello_test.go:4: oops`) + res.checkStderr(`1 / 1 tests failed`) + } +} + // TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go). func TestFoldingRanges(t *testing.T) { t.Parallel() diff --git a/gopls/internal/cmd/usage/api-json.hlp b/gopls/internal/cmd/usage/api-json.hlp index cb9fbfbea9d..529cca976ba 100644 --- a/gopls/internal/cmd/usage/api-json.hlp +++ b/gopls/internal/cmd/usage/api-json.hlp @@ -1,4 +1,4 @@ -print json describing gopls API +print JSON describing gopls API Usage: gopls [flags] api-json diff --git a/gopls/internal/cmd/usage/execute.hlp b/gopls/internal/cmd/usage/execute.hlp new file mode 100644 index 00000000000..954924809ed --- /dev/null +++ b/gopls/internal/cmd/usage/execute.hlp @@ -0,0 +1,31 @@ +Execute a gopls custom LSP command + +Usage: + gopls [flags] execute [flags] command argument... + +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: + -d,-diff + display diffs instead of edited file content + -l,-list + display names of edited files + -preserve + with -write, make copies of original files + -w,-write + write edited content to source files diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp index 19e146b7c9c..46c3f57b02d 100644 --- a/gopls/internal/cmd/usage/usage-v.hlp +++ b/gopls/internal/cmd/usage/usage-v.hlp @@ -15,7 +15,7 @@ Main version print the gopls version information bug report a bug in gopls help print usage information for subcommands - api-json print json describing gopls API + api-json print JSON describing gopls API licenses print licenses of included software Features @@ -23,6 +23,7 @@ Features check show diagnostic results for the specified file codelens List or execute code lenses for a file definition show declaration of selected identifier + execute Execute a gopls custom LSP command folding_ranges display selected file's folding ranges format format the code according to the go standard highlight display selected identifier's highlights diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp index 750e0b232ab..e791bbc0d55 100644 --- a/gopls/internal/cmd/usage/usage.hlp +++ b/gopls/internal/cmd/usage/usage.hlp @@ -15,7 +15,7 @@ Main version print the gopls version information bug report a bug in gopls help print usage information for subcommands - api-json print json describing gopls API + api-json print JSON describing gopls API licenses print licenses of included software Features @@ -23,6 +23,7 @@ Features check show diagnostic results for the specified file codelens List or execute code lenses for a file definition show declaration of selected identifier + execute Execute a gopls custom LSP command folding_ranges display selected file's folding ranges format format the code according to the go standard highlight display selected identifier's highlights diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index 066f16f790f..4d2cd779fbd 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -32,6 +32,14 @@ import ( // 3. The first line of the doc string is special. Everything after the colon // is considered the command 'Title'. // TODO(rFindley): reconsider this -- Title may be unnecessary. +// +// The doc comment on each method is eventually published at +// https://github.com/golang/tools/blob/master/gopls/doc/commands.md, +// so please be consistent in using this form: +// +// Command: Capitalized verb phrase with no period +// +// Longer description here... type Interface interface { // ApplyFix: Apply a fix // @@ -105,7 +113,7 @@ type Interface interface { // Reset diagnostics in the go.mod file of a module. ResetGoModDiagnostics(context.Context, ResetGoModDiagnosticsArgs) error - // GoGetPackage: go get a package + // GoGetPackage: 'go get' a package // // Runs `go get` to fetch a package. GoGetPackage(context.Context, GoGetPackageArgs) error @@ -146,7 +154,7 @@ type Interface interface { // address. StartDebugging(context.Context, DebuggingArgs) (DebuggingResult, error) - // StartProfile: start capturing a profile of gopls' execution. + // StartProfile: Start capturing a profile of gopls' execution // // Start a new pprof profile. Before using the resulting file, profiling must // be stopped with a corresponding call to StopProfile. @@ -155,13 +163,13 @@ type Interface interface { // runner. StartProfile(context.Context, StartProfileArgs) (StartProfileResult, error) - // StopProfile: stop an ongoing profile. + // StopProfile: Stop an ongoing profile // // This command is intended for internal use only, by the gopls benchmark // runner. StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error) - // RunGovulncheck: Run vulncheck. + // RunGovulncheck: Run vulncheck // // Run vulnerability check (`govulncheck`). RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error) @@ -171,7 +179,7 @@ type Interface interface { // Fetch the result of latest vulnerability check (`govulncheck`). FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) - // MemStats: fetch memory statistics + // MemStats: Fetch memory statistics // // Call runtime.GC multiple times and return memory statistics as reported by // runtime.MemStats. @@ -179,7 +187,7 @@ type Interface interface { // This command is used for benchmarking, and may change in the future. MemStats(context.Context) (MemStatsResult, error) - // WorkspaceStats: fetch workspace statistics + // WorkspaceStats: Fetch workspace statistics // // Query statistics about workspace builds, modules, packages, and files. // @@ -187,22 +195,24 @@ type Interface interface { // command. WorkspaceStats(context.Context) (WorkspaceStatsResult, error) - // RunGoWorkCommand: run `go work [args...]`, and apply the resulting go.work - // edits to the current go.work file. + // RunGoWorkCommand: Run `go work [args...]`, and apply the resulting go.work + // edits to the current go.work file RunGoWorkCommand(context.Context, RunGoWorkArgs) error - // AddTelemetryCounters: update the given telemetry counters. + // AddTelemetryCounters: Update the given telemetry counters // // Gopls will prepend "fwd/" to all the counters updated using this command // to avoid conflicts with other counters gopls collects. AddTelemetryCounters(context.Context, AddTelemetryCountersArgs) error - // MaybePromptForTelemetry: 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". + // MaybePromptForTelemetry: Prompt user to enable telemetry + // + // 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". MaybePromptForTelemetry(context.Context) error - // ChangeSignature: performs a "change signature" refactoring. + // ChangeSignature: Perform a "change signature" refactoring // // This command is experimental, currently only supporting parameter removal. // Its signature will certainly change in the future (pun intended). diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 9094bd57d4a..5a7c999d1f0 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -13,6 +13,7 @@ import ( "io" "os" "path/filepath" + "regexp" "runtime" "runtime/pprof" "sort" @@ -84,6 +85,15 @@ func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddT // commandConfig configures common command set-up and execution. type commandConfig struct { + // TODO(adonovan): whether a command is synchronous or + // asynchronous is part of the server interface contract, not + // a mere implementation detail of the handler. + // Export a (command.Command).IsAsync() property so that + // clients can tell. (The tricky part is ensuring the handler + // remains consistent with the command.Command metadata, as at + // the point were we read the 'async' field below, we no + // longer know that command.Command.) + async bool // whether to run the command asynchronously. Async commands can only return errors. requireSave bool // whether all files must be saved for the command to work progress string // title to use for progress reporting. If empty, no progress will be reported. @@ -102,6 +112,14 @@ type commandDeps struct { type commandFunc func(context.Context, commandDeps) error +// These strings are reported as the final WorkDoneProgressEnd message +// for each workspace/executeCommand request. +const ( + CommandCanceled = "canceled" + CommandFailed = "failed" + CommandCompleted = "completed" +) + // run performs command setup for command execution, and invokes the given run // function. If cfg.async is set, run executes the given func in a separate // goroutine, and returns as soon as setup is complete and the goroutine is @@ -158,12 +176,12 @@ func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run command if deps.work != nil { switch { case errors.Is(err, context.Canceled): - deps.work.End(ctx, "canceled") + deps.work.End(ctx, CommandCanceled) case err != nil: event.Error(ctx, "command error", err) - deps.work.End(ctx, "failed") + deps.work.End(ctx, CommandFailed) default: - deps.work.End(ctx, "completed") + deps.work.End(ctx, CommandCompleted) } } return err @@ -508,7 +526,7 @@ func (c *commandHandler) runTests(ctx context.Context, snapshot *cache.Snapshot, for _, funcName := range tests { inv := &gocommand.Invocation{ Verb: "test", - Args: []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)}, + Args: []string{pkgPath, "-v", "-count=1", fmt.Sprintf("-run=^%s$", regexp.QuoteMeta(funcName))}, WorkingDir: filepath.Dir(uri.Path()), } if err := snapshot.RunGoCommandPiped(ctx, cache.Normal, inv, out, out); err != nil { @@ -524,7 +542,7 @@ func (c *commandHandler) runTests(ctx context.Context, snapshot *cache.Snapshot, for _, funcName := range benchmarks { inv := &gocommand.Invocation{ Verb: "test", - Args: []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)}, + Args: []string{pkgPath, "-v", "-run=^$", fmt.Sprintf("-bench=^%s$", regexp.QuoteMeta(funcName))}, WorkingDir: filepath.Dir(uri.Path()), } if err := snapshot.RunGoCommandPiped(ctx, cache.Normal, inv, out, out); err != nil { diff --git a/gopls/internal/settings/api_json.go b/gopls/internal/settings/api_json.go index a9f3db2a982..48006be55a3 100644 --- a/gopls/internal/settings/api_json.go +++ b/gopls/internal/settings/api_json.go @@ -741,7 +741,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.add_telemetry_counters", - Title: "update the given telemetry counters.", + Title: "Update the given telemetry counters", Doc: "Gopls will prepend \"fwd/\" to all the counters updated using this command\nto avoid conflicts with other counters gopls collects.", ArgDoc: "{\n\t// Names and Values must have the same length.\n\t\"Names\": []string,\n\t\"Values\": []int64,\n}", }, @@ -753,7 +753,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.change_signature", - Title: "performs a \"change signature\" refactoring.", + Title: "Perform a \"change signature\" refactoring", Doc: "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).", ArgDoc: "{\n\t\"RemoveParameter\": {\n\t\t\"uri\": string,\n\t\t\"range\": {\n\t\t\t\"start\": { ... },\n\t\t\t\"end\": { ... },\n\t\t},\n\t},\n}", }, @@ -790,7 +790,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.go_get_package", - Title: "go get a package", + Title: "'go get' a package", Doc: "Runs `go get` to fetch a package.", ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The package to go get.\n\t\"Pkg\": string,\n\t\"AddRequire\": bool,\n}", }, @@ -810,12 +810,12 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.maybe_prompt_for_telemetry", - Title: "checks for the right conditions, and then prompts", - Doc: "the user to ask if they want to enable Go telemetry uploading. If the user\nresponds 'Yes', the telemetry mode is set to \"on\".", + Title: "Prompt user to enable telemetry", + Doc: "Checks for the right conditions, and then prompts the user\nto ask if they want to enable Go telemetry uploading. If\nthe user responds 'Yes', the telemetry mode is set to \"on\".", }, { Command: "gopls.mem_stats", - Title: "fetch memory statistics", + Title: "Fetch memory statistics", Doc: "Call runtime.GC multiple times and return memory statistics as reported by\nruntime.MemStats.\n\nThis command is used for benchmarking, and may change in the future.", ResultDoc: "{\n\t\"HeapAlloc\": uint64,\n\t\"HeapInUse\": uint64,\n\t\"TotalAlloc\": uint64,\n}", }, @@ -839,13 +839,13 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.run_go_work_command", - Title: "run `go work [args...]`, and apply the resulting go.work", - Doc: "edits to the current go.work file.", + Title: "Run `go work [args...]`, and apply the resulting go.work", + Doc: "edits to the current go.work file", ArgDoc: "{\n\t\"ViewID\": string,\n\t\"InitFirst\": bool,\n\t\"Args\": []string,\n}", }, { Command: "gopls.run_govulncheck", - Title: "Run vulncheck.", + Title: "Run vulncheck", Doc: "Run vulnerability check (`govulncheck`).", ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}", @@ -865,14 +865,14 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.start_profile", - Title: "start capturing a profile of gopls' execution.", + Title: "Start capturing a profile of gopls' execution", Doc: "Start a new pprof profile. Before using the resulting file, profiling must\nbe stopped with a corresponding call to StopProfile.\n\nThis command is intended for internal use only, by the gopls benchmark\nrunner.", ArgDoc: "struct{}", ResultDoc: "struct{}", }, { Command: "gopls.stop_profile", - Title: "stop an ongoing profile.", + Title: "Stop an ongoing profile", Doc: "This command is intended for internal use only, by the gopls benchmark\nrunner.", ArgDoc: "struct{}", ResultDoc: "{\n\t// File is the profile file name.\n\t\"File\": string,\n}", @@ -915,7 +915,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Command: "gopls.workspace_stats", - Title: "fetch workspace statistics", + Title: "Fetch workspace statistics", Doc: "Query statistics about workspace builds, modules, packages, and files.\n\nThis command is intended for internal use only, by the gopls stats\ncommand.", ResultDoc: "{\n\t\"Files\": {\n\t\t\"Total\": int,\n\t\t\"Largest\": int,\n\t\t\"Errs\": int,\n\t},\n\t\"Views\": []{\n\t\t\"GoCommandVersion\": string,\n\t\t\"AllPackages\": {\n\t\t\t\"Packages\": int,\n\t\t\t\"LargestPackage\": int,\n\t\t\t\"CompiledGoFiles\": int,\n\t\t\t\"Modules\": int,\n\t\t},\n\t\t\"WorkspacePackages\": {\n\t\t\t\"Packages\": int,\n\t\t\t\"LargestPackage\": int,\n\t\t\t\"CompiledGoFiles\": int,\n\t\t\t\"Modules\": int,\n\t\t},\n\t\t\"Diagnostics\": int,\n\t},\n}", }, @@ -938,7 +938,7 @@ var GeneratedAPIJSON = &APIJSON{ }, { Lens: "run_govulncheck", - Title: "Run vulncheck.", + Title: "Run vulncheck", Doc: "Run vulnerability check (`govulncheck`).", }, { diff --git a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go new file mode 100644 index 00000000000..744cb54c922 --- /dev/null +++ b/gopls/internal/util/slices/slices.go @@ -0,0 +1,16 @@ +// 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 slices + +// Contains reports whether x is present in slice. +// TODO(adonovan): use go1.19 slices.Contains. +func Contains[S ~[]E, E comparable](slice S, x E) bool { + for _, elem := range slice { + if elem == x { + return true + } + } + return false +}