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 <adonovan@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md
index 5eda89f..7a4e251 100644
--- a/gopls/doc/commands.md
+++ b/gopls/doc/commands.md
@@ -262,6 +262,17 @@
 }
 ```
 
+<a id='gopls.client_open_url'></a>
+## `gopls.client_open_url`: **Request that the client open a URL in a browser.**
+
+
+
+Args:
+
+```
+string
+```
+
 <a id='gopls.diagnose_files'></a>
 ## `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 fc559fd..56648c2 100644
--- a/gopls/doc/features/README.md
+++ b/gopls/doc/features/README.md
@@ -38,7 +38,7 @@
   - [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 @@
   - [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 83fcccd..cb82e95 100644
--- a/gopls/internal/cmd/codeaction.go
+++ b/gopls/internal/cmd/codeaction.go
@@ -58,6 +58,7 @@
 	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 e9bf1ab..f4d76b9 100644
--- a/gopls/internal/cmd/integration_test.go
+++ b/gopls/internal/cmd/integration_test.go
@@ -977,10 +977,13 @@
 	}
 	// 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 977bcd4..edc6a3e 100644
--- a/gopls/internal/cmd/usage/codeaction.hlp
+++ b/gopls/internal/cmd/usage/codeaction.hlp
@@ -29,6 +29,7 @@
 	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 88a77cf..c0c4370 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -990,6 +990,13 @@
 			"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.",
 			"Doc": "This command is needed by the 'gopls {check,fix}' CLI subcommands.",
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index bf26458..31d036b 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -48,7 +48,8 @@
 	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 @@
 				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 d89525b..7f9ba1b 100644
--- a/gopls/internal/protocol/command/command_gen.go
+++ b/gopls/internal/protocol/command/command_gen.go
@@ -31,6 +31,7 @@
 	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 @@
 	Assembly,
 	ChangeSignature,
 	CheckUpgrades,
+	ClientOpenURL,
 	DiagnoseFiles,
 	Doc,
 	EditGoDirective,
@@ -155,6 +157,12 @@
 			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 @@
 	}, 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 ba9f200..7bb1006 100644
--- a/gopls/internal/protocol/command/interface.go
+++ b/gopls/internal/protocol/command/interface.go
@@ -265,6 +265,9 @@
 	// 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 dd6abe0..fe1c885 100644
--- a/gopls/internal/server/code_action.go
+++ b/gopls/internal/server/code_action.go
@@ -143,7 +143,8 @@
 				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 c5871be..25a7f33 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -1462,6 +1462,11 @@
 	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 0731115..dea2e69 100644
--- a/gopls/internal/settings/codeactionkind.go
+++ b/gopls/internal/settings/codeactionkind.go
@@ -31,7 +31,8 @@
 // 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 @@
 // 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 8d5eb6b..7b14d2a 100644
--- a/gopls/internal/settings/default.go
+++ b/gopls/internal/settings/default.go
@@ -53,6 +53,7 @@
 						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 376bbe3..b0325d0 100644
--- a/gopls/internal/test/integration/misc/codeactions_test.go
+++ b/gopls/internal/test/integration/misc/codeactions_test.go
@@ -67,12 +67,14 @@
 			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 5bb709f..8105fd0 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 @@
 
 	"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 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 @@
 		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 @@
 		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 @@
 		}
 	}
 }
+
+// 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)
+}