From 2154cbf88df752a2011d2c01b43eb8c8af7373ee Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 27 Jun 2024 15:23:51 -0400 Subject: [PATCH] gopls/internal/golang: add "Browse gopls features" code action This command opens the Index of Features doc page: $ gopls codeaction -kind=gopls.doc.features -exec ./gopls/main.go VS Code exposes this new code action through the Quick Fix menu (Command-.) under the section "More actions...". It should probably also be given a top-level command similar to "Go: Add Import", etc. Other editors seem to treat code actions more uniformly, so special handling is unnecessary. Change-Id: I633dd34cdb9005009a098bcd7bb50d0db06044c7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/595557 Commit-Queue: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/doc/commands.md | 11 ++++ gopls/doc/features/README.md | 7 ++- gopls/internal/cmd/codeaction.go | 1 + gopls/internal/cmd/integration_test.go | 7 ++- gopls/internal/cmd/usage/codeaction.hlp | 1 + gopls/internal/doc/api.json | 7 +++ gopls/internal/golang/codeaction.go | 19 ++++++- .../internal/protocol/command/command_gen.go | 20 +++++++ gopls/internal/protocol/command/interface.go | 3 ++ gopls/internal/server/code_action.go | 3 +- gopls/internal/server/command.go | 5 ++ gopls/internal/settings/codeactionkind.go | 12 +++-- gopls/internal/settings/default.go | 1 + .../test/integration/misc/codeactions_test.go | 4 +- .../test/integration/misc/webserver_test.go | 53 ++++++++----------- 15 files changed, 111 insertions(+), 43 deletions(-) diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 5eda89f9eb6..7a4e2515d52 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -262,6 +262,17 @@ Args: } ``` + +## `gopls.client_open_url`: **Request that the client open a URL in a browser.** + + + +Args: + +``` +string +``` + ## `gopls.diagnose_files`: **Cause server to publish diagnostics for the specified files.** diff --git a/gopls/doc/features/README.md b/gopls/doc/features/README.md index fc559fd9ab3..56648c2d2ec 100644 --- a/gopls/doc/features/README.md +++ b/gopls/doc/features/README.md @@ -38,7 +38,7 @@ when making significant changes to existing features or when adding new ones. - [Type Definition](navigation.md#type-definition): go to definition of type of selected symbol - [References](navigation.md#references): list references to selected symbol - [Implementation](navigation.md#implementation): show "implements" relationships of selected type - - [Document Symbol](passive.md#document-symbol): outline of symbols defined in current file + - [Document Symbol](navigation.md#document-symbol): outline of symbols defined in current file - [Symbol](navigation.md#symbol): fuzzy search for symbol by name - [Selection Range](navigation.md#selection-range): select enclosing unit of syntax - [Call Hierarchy](navigation.md#call-hierarchy): show outgoing/incoming calls to the current function @@ -59,3 +59,8 @@ when making significant changes to existing features or when adding new ones. - [go.mod and go.work files](modfiles.md): Go module and workspace manifests - [Command-line interface](../command-line.md): CLI for debugging and scripting (unstable) - [Non-standard commands](../commands.md): gopls-specific RPC protocol extensions (unstable) + +You can find this page from within your editor by executing the +`gopls.doc.features` [code action](transformation.md#code-actions), +which opens it in a web browser. +In VS Code, you can find it on the Quick fix menu. diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go index 83fcccddcaa..cb82e951b41 100644 --- a/gopls/internal/cmd/codeaction.go +++ b/gopls/internal/cmd/codeaction.go @@ -58,6 +58,7 @@ Valid kinds include: source.doc source.freesymbols goTest + gopls.doc.features Kinds are hierarchical, so "refactor" includes "refactor.inline". (Note: actions of kind "goTest" are not returned unless explicitly diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index e9bf1ab0221..f4d76b90b27 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -977,10 +977,13 @@ type C struct{} } // list code actions in file, filtering by title { - res := gopls(t, tree, "codeaction", "-title=Br.wse", "a.go") + res := gopls(t, tree, "codeaction", "-title=Browse.*doc", "a.go") res.checkExit(true) got := res.stdout - want := `command "Browse documentation for package a" [source.doc]` + "\n" + want := `command "Browse gopls feature documentation" [gopls.doc.features]` + + "\n" + + `command "Browse documentation for package a" [source.doc]` + + "\n" if got != want { t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) } diff --git a/gopls/internal/cmd/usage/codeaction.hlp b/gopls/internal/cmd/usage/codeaction.hlp index 977bcd4df2f..edc6a3e8f99 100644 --- a/gopls/internal/cmd/usage/codeaction.hlp +++ b/gopls/internal/cmd/usage/codeaction.hlp @@ -29,6 +29,7 @@ Valid kinds include: source.doc source.freesymbols goTest + gopls.doc.features Kinds are hierarchical, so "refactor" includes "refactor.inline". (Note: actions of kind "goTest" are not returned unless explicitly diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 88a77cfb146..c0c437006e1 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -989,6 +989,13 @@ "ArgDoc": "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The modules to check.\n\t\"Modules\": []string,\n}", "ResultDoc": "" }, + { + "Command": "gopls.client_open_url", + "Title": "Request that the client open a URL in a browser.", + "Doc": "", + "ArgDoc": "string", + "ResultDoc": "" + }, { "Command": "gopls.diagnose_files", "Title": "Cause server to publish diagnostics for the specified files.", diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index bf26458b99b..31d036bdf40 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -48,7 +48,8 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] || - want[settings.GoFreeSymbols] { + want[settings.GoFreeSymbols] || + want[settings.GoplsDocFeatures] { pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { @@ -115,6 +116,22 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, Command: &cmd, }) } + + if want[settings.GoplsDocFeatures] { + // TODO(adonovan): after the docs are published in gopls/v0.17.0, + // use the gopls release tag instead of master. + cmd, err := command.NewClientOpenURLCommand( + "Browse gopls feature documentation", + "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") + if err != nil { + return nil, err + } + actions = append(actions, protocol.CodeAction{ + Title: cmd.Title, + Kind: settings.GoplsDocFeatures, + Command: &cmd, + }) + } } // Code actions requiring type information. diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index d89525bd97e..7f9ba1bc9db 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go @@ -31,6 +31,7 @@ const ( Assembly Command = "gopls.assembly" ChangeSignature Command = "gopls.change_signature" CheckUpgrades Command = "gopls.check_upgrades" + ClientOpenURL Command = "gopls.client_open_url" DiagnoseFiles Command = "gopls.diagnose_files" Doc Command = "gopls.doc" EditGoDirective Command = "gopls.edit_go_directive" @@ -74,6 +75,7 @@ var Commands = []Command{ Assembly, ChangeSignature, CheckUpgrades, + ClientOpenURL, DiagnoseFiles, Doc, EditGoDirective, @@ -155,6 +157,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.CheckUpgrades(ctx, a0) + case ClientOpenURL: + var a0 string + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.ClientOpenURL(ctx, a0) case DiagnoseFiles: var a0 DiagnoseFilesArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -424,6 +432,18 @@ func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Comma }, nil } +func NewClientOpenURLCommand(title string, a0 string) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: ClientOpenURL.String(), + Arguments: args, + }, nil +} + func NewDiagnoseFilesCommand(title string, a0 DiagnoseFilesArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index ba9f200bd07..7bb100603c7 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -265,6 +265,9 @@ type Interface interface { // The machine architecture is determined by the view. Assembly(_ context.Context, viewID, packageID, symbol string) error + // ClientOpenURL: Request that the client open a URL in a browser. + ClientOpenURL(_ context.Context, url string) error + // ScanImports: force a sychronous scan of the imports cache. // // This command is intended for use by gopls tests only. diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index dd6abe0a3b6..fe1c885b87f 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go @@ -143,7 +143,8 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara case settings.GoTest, settings.GoDoc, settings.GoFreeSymbols, - settings.GoAssembly: + settings.GoAssembly, + settings.GoplsDocFeatures: return false // read-only query } return true // potential write operation diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index c5871beb5fd..25a7f33372f 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -1462,6 +1462,11 @@ func (c *commandHandler) Assembly(ctx context.Context, viewID, packageID, symbol return nil } +func (c *commandHandler) ClientOpenURL(ctx context.Context, url string) error { + openClientBrowser(ctx, c.s.client, url) + return nil +} + func (c *commandHandler) ScanImports(ctx context.Context) error { for _, v := range c.s.session.Views() { v.ScanImports() diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index 073111536f8..dea2e699700 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -31,7 +31,8 @@ import "golang.org/x/tools/gopls/internal/protocol" // actions with kind="source.*". A lightbulb appears in both cases. // A third menu, "Quick fix...", not found on the usual context // menu but accessible through the command palette or "⌘.", -// displays code actions of kind "quickfix.*" and "refactor.*". +// displays code actions of kind "quickfix.*" and "refactor.*", +// and ad hoc ones ("More actions...") such as "gopls.*". // All of these CodeAction requests have triggerkind=Invoked. // // Cursor motion also performs a CodeAction request, but with @@ -76,8 +77,9 @@ import "golang.org/x/tools/gopls/internal/protocol" // instead of == for CodeActionKinds throughout gopls. // See golang/go#40438 for related discussion. const ( - GoAssembly protocol.CodeActionKind = "source.assembly" - GoDoc protocol.CodeActionKind = "source.doc" - GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" - GoTest protocol.CodeActionKind = "goTest" // TODO(adonovan): rename "source.test" + GoAssembly protocol.CodeActionKind = "source.assembly" + GoDoc protocol.CodeActionKind = "source.doc" + GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" + GoTest protocol.CodeActionKind = "goTest" // TODO(adonovan): rename "source.test" + GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features" ) diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 8d5eb6b786b..7b14d2a5d79 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -53,6 +53,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options { GoDoc: true, GoFreeSymbols: true, // Not GoTest: it must be explicit in CodeActionParams.Context.Only + GoplsDocFeatures: true, }, file.Mod: { protocol.SourceOrganizeImports: true, diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index 376bbe3fd87..b0325d0f872 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go @@ -67,12 +67,14 @@ func g() {} settings.GoAssembly, settings.GoDoc, settings.GoFreeSymbols, + settings.GoplsDocFeatures, protocol.RefactorExtract, protocol.RefactorInline) check("gen/a.go", settings.GoAssembly, settings.GoDoc, - settings.GoFreeSymbols) + settings.GoFreeSymbols, + settings.GoplsDocFeatures) }) } diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 5bb709f9227..8105fd06896 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -5,6 +5,7 @@ package misc import ( + "fmt" "html" "io" "net/http" @@ -15,6 +16,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/gopls/internal/settings" . "golang.org/x/tools/gopls/internal/test/integration" "golang.org/x/tools/internal/testenv" ) @@ -271,18 +273,10 @@ func (*T) M() { /*in T.M*/} func viewPkgDoc(t *testing.T, env *Env, loc protocol.Location) protocol.URI { // Invoke the "Browse package documentation" code // action to start the server. - var docAction *protocol.CodeAction actions := env.CodeAction(loc, nil, 0) - for _, act := range actions { - if strings.HasPrefix(act.Title, "Browse ") && - strings.Contains(act.Title, "documentation") { - docAction = &act - break - } - } - if docAction == nil { - t.Fatalf("can't find action with Title 'Browse...documentation', only %#v", - actions) + docAction, err := codeActionByKind(actions, settings.GoDoc) + if err != nil { + t.Fatal(err) } // Execute the command. @@ -335,16 +329,9 @@ func f(buf bytes.Buffer, greeting string) { if err != nil { t.Fatalf("CodeAction: %v", err) } - var action *protocol.CodeAction - for _, a := range actions { - if a.Title == "Browse free symbols" { - action = &a - break - } - } - if action == nil { - t.Fatalf("can't find action with Title 'Browse free symbols', only %#v", - actions) + action, err := codeActionByKind(actions, settings.GoFreeSymbols) + if err != nil { + t.Fatal(err) } // Execute the command. @@ -401,17 +388,9 @@ func g() { if err != nil { t.Fatalf("CodeAction: %v", err) } - const wantTitle = "Browse " + runtime.GOARCH + " assembly for f" - var action *protocol.CodeAction - for _, a := range actions { - if a.Title == wantTitle { - action = &a - break - } - } - if action == nil { - t.Fatalf("can't find action with Title %s, only %#v", - wantTitle, actions) + action, err := codeActionByKind(actions, settings.GoAssembly) + if err != nil { + t.Fatal(err) } // Execute the command. @@ -504,3 +483,13 @@ func checkMatch(t *testing.T, want bool, got []byte, pattern string) { } } } + +// codeActionByKind returns the first action of the specified kind, or an error. +func codeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { + for _, act := range actions { + if act.Kind == kind { + return &act, nil + } + } + return nil, fmt.Errorf("can't find action with kind %s, only %#v", kind, actions) +}