gopls/internal/protocol: separate CodeLens from Command; document

Historically, CodeLenses were identified in the UI (LSP, CLI, docs)
by the command.Command that they return, but this is confusing
and potentially ambiguous as a single lens algorithm may offer
many commands, potentially overlapping.

This change establishes a separate CodeLensKind identifier for
them. The actual string values must remain unchanged to avoid
breaking users.

The documentation generator now uses the doc comments attached
to these CodeLensKind enum declarations. I have updated and
elaborated the documentation for each one.

Change-Id: I4a331930ca6a22b85150615e87ee79a66434ebe3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/586175
Auto-Submit: 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>
diff --git a/gopls/doc/generate/generate.go b/gopls/doc/generate/generate.go
index b0bf461..c7fa2ae 100644
--- a/gopls/doc/generate/generate.go
+++ b/gopls/doc/generate/generate.go
@@ -35,12 +35,15 @@
 
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/go/packages"
+	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/doc"
 	"golang.org/x/tools/gopls/internal/golang"
 	"golang.org/x/tools/gopls/internal/mod"
+	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/protocol/command"
 	"golang.org/x/tools/gopls/internal/protocol/command/commandmeta"
 	"golang.org/x/tools/gopls/internal/settings"
+	"golang.org/x/tools/gopls/internal/util/maps"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
 )
 
@@ -133,12 +136,23 @@
 		&packages.Config{
 			Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps,
 		},
-		"golang.org/x/tools/gopls/internal/settings",
+		"golang.org/x/tools/gopls/internal/settings", // for settings
+		"golang.org/x/tools/gopls/internal/protocol", // for lenses
 	)
 	if err != nil {
 		return nil, err
 	}
-	pkg := pkgs[0]
+	// TODO(adonovan): document at packages.Load that the result
+	// order does not match the pattern order.
+	var protocolPkg, settingsPkg *packages.Package
+	for _, pkg := range pkgs {
+		switch pkg.Types.Name() {
+		case "settings":
+			settingsPkg = pkg
+		case "protocol":
+			protocolPkg = pkg
+		}
+	}
 
 	defaults := settings.DefaultOptions()
 	api := &doc.API{
@@ -150,7 +164,10 @@
 	if err != nil {
 		return nil, err
 	}
-	api.Lenses = loadLenses(api.Commands)
+	api.Lenses, err = loadLenses(protocolPkg, defaults.Codelenses)
+	if err != nil {
+		return nil, err
+	}
 
 	// Transform the internal command name to the external command name.
 	for _, c := range api.Commands {
@@ -161,11 +178,11 @@
 		reflect.ValueOf(defaults.UserOptions),
 	} {
 		// Find the type information and ast.File corresponding to the category.
-		optsType := pkg.Types.Scope().Lookup(category.Type().Name())
+		optsType := settingsPkg.Types.Scope().Lookup(category.Type().Name())
 		if optsType == nil {
-			return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), pkg.Types.Scope())
+			return nil, fmt.Errorf("could not find %v in scope %v", category.Type().Name(), settingsPkg.Types.Scope())
 		}
-		opts, err := loadOptions(category, optsType, pkg, "")
+		opts, err := loadOptions(category, optsType, settingsPkg, "")
 		if err != nil {
 			return nil, err
 		}
@@ -516,30 +533,65 @@
 	return b.String()
 }
 
-func loadLenses(commands []*doc.Command) []*doc.Lens {
-	all := map[command.Command]struct{}{}
-	for k := range golang.LensFuncs() {
-		all[k] = struct{}{}
-	}
-	for k := range mod.LensFuncs() {
-		if _, ok := all[k]; ok {
-			panic(fmt.Sprintf("duplicate lens %q", string(k)))
+// loadLenses combines the syntactic comments from the protocol
+// package with the default values from settings.DefaultOptions(), and
+// returns a list of Code Lens descriptors.
+func loadLenses(protocolPkg *packages.Package, defaults map[protocol.CodeLensSource]bool) ([]*doc.Lens, error) {
+	// Find the CodeLensSource enums among the files of the protocol package.
+	// Map each enum value to its doc comment.
+	enumDoc := make(map[string]string)
+	for _, f := range protocolPkg.Syntax {
+		for _, decl := range f.Decls {
+			if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.CONST {
+				for _, spec := range decl.Specs {
+					spec := spec.(*ast.ValueSpec)
+					posn := safetoken.StartPosition(protocolPkg.Fset, spec.Pos())
+					if id, ok := spec.Type.(*ast.Ident); ok && id.Name == "CodeLensSource" {
+						if len(spec.Names) != 1 || len(spec.Values) != 1 {
+							return nil, fmt.Errorf("%s: declare one CodeLensSource per line", posn)
+						}
+						lit, ok := spec.Values[0].(*ast.BasicLit)
+						if !ok && lit.Kind != token.STRING {
+							return nil, fmt.Errorf("%s: CodeLensSource value is not a string literal", posn)
+						}
+						value, _ := strconv.Unquote(lit.Value) // ignore error: AST is well-formed
+						if spec.Doc == nil {
+							return nil, fmt.Errorf("%s: %s lacks doc comment", posn, spec.Names[0].Name)
+						}
+						enumDoc[value] = spec.Doc.Text()
+					}
+				}
+			}
 		}
-		all[k] = struct{}{}
+	}
+	if len(enumDoc) == 0 {
+		return nil, fmt.Errorf("failed to extract any CodeLensSource declarations")
 	}
 
+	// Build list of Lens descriptors.
 	var lenses []*doc.Lens
-
-	for _, cmd := range commands {
-		if _, ok := all[command.Command(cmd.Command)]; ok {
+	addAll := func(sources map[protocol.CodeLensSource]cache.CodeLensSourceFunc, fileType string) error {
+		slice := maps.Keys(sources)
+		sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
+		for _, source := range slice {
+			docText, ok := enumDoc[string(source)]
+			if !ok {
+				return fmt.Errorf("missing CodeLensSource declaration for %s", source)
+			}
+			title, docText, _ := strings.Cut(docText, "\n") // first line is title
 			lenses = append(lenses, &doc.Lens{
-				Lens:  cmd.Command,
-				Title: cmd.Title,
-				Doc:   cmd.Doc,
+				FileType: fileType,
+				Lens:     string(source),
+				Title:    title,
+				Doc:      docText,
+				Default:  defaults[source],
 			})
 		}
+		return nil
 	}
-	return lenses
+	addAll(golang.CodeLensSources(), "Go")
+	addAll(mod.CodeLensSources(), "go.mod")
+	return lenses, nil
 }
 
 func loadAnalyzers(m map[string]*settings.Analyzer) []*doc.Analyzer {
@@ -711,7 +763,10 @@
 	// Replace the lenses section.
 	var buf bytes.Buffer
 	for _, lens := range api.Lenses {
-		fmt.Fprintf(&buf, "### **%v**\n\nIdentifier: `%v`\n\n%v\n", lens.Title, lens.Lens, lens.Doc)
+		fmt.Fprintf(&buf, "### ⬤ `%s`: %s\n\n", lens.Lens, lens.Title)
+		fmt.Fprintf(&buf, "%s\n\n", lens.Doc)
+		fmt.Fprintf(&buf, "Default: %v\n\n", onOff(lens.Default))
+		fmt.Fprintf(&buf, "File type: %s\n\n", lens.FileType)
 	}
 	return replaceSection(content, "Lenses", buf.Bytes())
 }
@@ -847,3 +902,13 @@
 	result = append(result, content[idx[3]:]...)
 	return result, nil
 }
+
+type onOff bool
+
+func (o onOff) String() string {
+	if o {
+		return "on"
+	} else {
+		return "off"
+	}
+}
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 5247cf6..00a9188 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -170,7 +170,7 @@
 ### UI
 
 <a id='codelenses'></a>
-#### ⬤ **codelenses** *map[string]bool*
+#### ⬤ **codelenses** *map[golang.org/x/tools/gopls/internal/protocol.CodeLensSource]bool*
 
 codelenses overrides the enabled/disabled state of code lenses. See the
 "Code Lenses" section of the
@@ -190,7 +190,7 @@
 }
 ```
 
-Default: `{"gc_details":false,"generate":true,"regenerate_cgo":true,"tidy":true,"upgrade_dependency":true,"vendor":true}`.
+Default: `{"gc_details":false,"generate":true,"regenerate_cgo":true,"run_govulncheck":false,"tidy":true,"upgrade_dependency":true,"vendor":true}`.
 
 <a id='semanticTokens'></a>
 #### ⬤ **semanticTokens** *bool*
@@ -550,49 +550,149 @@
 
 ## Code Lenses
 
-These are the code lenses that `gopls` currently supports. They can be enabled
-and disabled using the `codelenses` setting, documented above. Their names and
-features are subject to change.
+A "code lens" is a command associated with a range of a source file.
+(They are so named because VS Code displays them with a magnifying
+glass icon in the margin.) The VS Code manual describes code lenses as
+"[actionable, contextual information, interspersed in your source
+code](https://code.visualstudio.com/blogs/2017/02/12/code-lens-roundup)".
+The LSP `CodeLens` operation requests the
+current set of code lenses for a file.
+
+Gopls generates code lenses from a number of sources.
+They are described below.
+
+They can be enabled and disabled using the `codelenses` setting,
+documented above. Their names and features are subject to change.
 
 <!-- BEGIN Lenses: DO NOT MANUALLY EDIT THIS SECTION -->
-### **Toggle gc_details**
+### ⬤ `gc_details`: Toggle display of Go compiler optimization decisions
 
-Identifier: `gc_details`
 
-Toggle the calculation of gc annotations.
-### **Run go generate**
+This codelens source causes the `package` declaration of
+each file to be annotated with a command to toggle the
+state of the per-session variable that controls whether
+optimization decisions from the Go compiler (formerly known
+as "gc") should be displayed as diagnostics.
 
-Identifier: `generate`
+Optimization decisions include:
+- whether a variable escapes, and how escape is inferred;
+- whether a nil-pointer check is implied or eliminated;
+- whether a function can be inlined.
 
-Runs `go generate` for a given directory.
-### **Regenerate cgo**
+TODO(adonovan): this source is off by default because the
+annotation is annoying and because VS Code has a separate
+"Toggle gc details" command. Replace it with a Code Action
+("Source action...").
 
-Identifier: `regenerate_cgo`
 
-Regenerates cgo definitions.
-### **Run vulncheck**
+Default: off
 
-Identifier: `run_govulncheck`
+File type: Go
 
-Run vulnerability check (`govulncheck`).
-### **Run test(s) (legacy)**
+### ⬤ `generate`: Run `go generate`
 
-Identifier: `test`
 
-Runs `go test` for a specific set of test or benchmark functions.
-### **Run go mod tidy**
+This codelens source annotates any `//go:generate` comments
+with commands to run `go generate` in this directory, on
+all directories recursively beneath this one.
 
-Identifier: `tidy`
+See [Generating code](https://go.dev/blog/generate) for
+more details.
 
-Runs `go mod tidy` for a module.
-### **Upgrade a dependency**
 
-Identifier: `upgrade_dependency`
+Default: on
 
-Upgrades a dependency in the go.mod file for a module.
-### **Run go mod vendor**
+File type: Go
 
-Identifier: `vendor`
+### ⬤ `regenerate_cgo`: Re-generate cgo declarations
 
-Runs `go mod vendor` for a module.
+
+This codelens source annotates an `import "C"` declaration
+with a command to re-run the [cgo
+command](https://pkg.go.dev/cmd/cgo) to regenerate the
+corresponding Go declarations.
+
+Use this after editing the C code in comments attached to
+the import, or in C header files included by it.
+
+
+Default: on
+
+File type: Go
+
+### ⬤ `test`: Run tests and benchmarks
+
+
+This codelens source annotates each `Test` and `Benchmark`
+function in a `*_test.go` file with a command to run it.
+
+This source is off by default because VS Code has
+a more sophisticated client-side Test Explorer.
+See golang/go#67400 for a discussion of this feature.
+
+
+Default: off
+
+File type: Go
+
+### ⬤ `run_govulncheck`: Run govulncheck
+
+
+This codelens source annotates the `module` directive in a
+go.mod file with a command to run Govulncheck.
+
+[Govulncheck](https://go.dev/blog/vuln) is a static
+analysis tool that computes the set of functions reachable
+within your application, including dependencies;
+queries a database of known security vulnerabilities; and
+reports any potential problems it finds.
+
+
+Default: off
+
+File type: go.mod
+
+### ⬤ `tidy`: Tidy go.mod file
+
+
+This codelens source annotates the `module` directive in a
+go.mod file with a command to run [`go mod
+tidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures
+that the go.mod file matches the source code in the module.
+
+
+Default: on
+
+File type: go.mod
+
+### ⬤ `upgrade_dependency`: Update dependencies
+
+
+This codelens source annotates the `module` directive in a
+go.mod file with commands to:
+
+- check for available upgrades,
+- upgrade direct dependencies, and
+- upgrade all dependencies transitively.
+
+
+Default: on
+
+File type: go.mod
+
+### ⬤ `vendor`: Update vendor directory
+
+
+This codelens source annotates the `module` directive in a
+go.mod file with a command to run [`go mod
+vendor`](https://go.dev/ref/mod#go-mod-vendor), which
+creates or updates the directory named `vendor` in the
+module root so that it contains an up-to-date copy of all
+necessary package dependencies.
+
+
+Default: on
+
+File type: go.mod
+
 <!-- END Lenses: DO NOT MANUALLY EDIT THIS SECTION -->
diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go
index 72529b6..dd868dc 100644
--- a/gopls/internal/cache/snapshot.go
+++ b/gopls/internal/cache/snapshot.go
@@ -2284,3 +2284,7 @@
 	_, ok := s.gcOptimizationDetails[id]
 	return ok
 }
+
+// A CodeLensSourceFunc is a function that reports CodeLenses (range-associated
+// commands) for a given file.
+type CodeLensSourceFunc func(context.Context, *Snapshot, file.Handle) ([]protocol.CodeLens, error)
diff --git a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go
index 032de33..a7017d8 100644
--- a/gopls/internal/cmd/codelens.go
+++ b/gopls/internal/cmd/codelens.go
@@ -68,7 +68,7 @@
 
 	r.app.editFlags = &r.EditFlags // in case a codelens perform an edit
 
-	// Override the default setting for codelenses[Test], which is
+	// Override the default setting for codelenses["test"], which is
 	// off by default because VS Code has a superior client-side
 	// implementation. But this client is not VS Code.
 	// See golang.LensFuncs().
@@ -78,9 +78,9 @@
 			origOptions(opts)
 		}
 		if opts.Codelenses == nil {
-			opts.Codelenses = make(map[string]bool)
+			opts.Codelenses = make(map[protocol.CodeLensSource]bool)
 		}
-		opts.Codelenses["test"] = true
+		opts.Codelenses[protocol.CodeLensTest] = true
 	}
 
 	// TODO(adonovan): cleanup: factor progress with stats subcommand.
diff --git a/gopls/internal/doc/api.go b/gopls/internal/doc/api.go
index 4423ed8..b9e593a 100644
--- a/gopls/internal/doc/api.go
+++ b/gopls/internal/doc/api.go
@@ -63,9 +63,11 @@
 }
 
 type Lens struct {
-	Lens  string
-	Title string
-	Doc   string
+	FileType string // e.g. "Go", "go.mod"
+	Lens     string
+	Title    string
+	Doc      string
+	Default  bool
 }
 
 type Analyzer struct {
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index a4cb360..27a9f98 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -799,55 +799,55 @@
 			},
 			{
 				"Name": "codelenses",
-				"Type": "map[string]bool",
+				"Type": "map[golang.org/x/tools/gopls/internal/protocol.CodeLensSource]bool",
 				"Doc": "codelenses overrides the enabled/disabled state of code lenses. See the\n\"Code Lenses\" section of the\n[Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses)\nfor the list of supported lenses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n  \"codelenses\": {\n    \"generate\": false,  // Don't show the `go generate` lens.\n    \"gc_details\": true  // Show a code lens toggling the display of gc's choices.\n  }\n...\n}\n```\n",
 				"EnumKeys": {
 					"ValueType": "bool",
 					"Keys": [
 						{
 							"Name": "\"gc_details\"",
-							"Doc": "Toggle the calculation of gc annotations.",
+							"Doc": "\nThis codelens source causes the `package` declaration of\neach file to be annotated with a command to toggle the\nstate of the per-session variable that controls whether\noptimization decisions from the Go compiler (formerly known\nas \"gc\") should be displayed as diagnostics.\n\nOptimization decisions include:\n- whether a variable escapes, and how escape is inferred;\n- whether a nil-pointer check is implied or eliminated;\n- whether a function can be inlined.\n\nTODO(adonovan): this source is off by default because the\nannotation is annoying and because VS Code has a separate\n\"Toggle gc details\" command. Replace it with a Code Action\n(\"Source action...\").\n",
 							"Default": "false"
 						},
 						{
 							"Name": "\"generate\"",
-							"Doc": "Runs `go generate` for a given directory.",
+							"Doc": "\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n",
 							"Default": "true"
 						},
 						{
 							"Name": "\"regenerate_cgo\"",
-							"Doc": "Regenerates cgo definitions.",
+							"Doc": "\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n",
 							"Default": "true"
 						},
 						{
-							"Name": "\"run_govulncheck\"",
-							"Doc": "Run vulnerability check (`govulncheck`).",
+							"Name": "\"test\"",
+							"Doc": "\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na more sophisticated client-side Test Explorer.\nSee golang/go#67400 for a discussion of this feature.\n",
 							"Default": "false"
 						},
 						{
-							"Name": "\"test\"",
-							"Doc": "Runs `go test` for a specific set of test or benchmark functions.",
+							"Name": "\"run_govulncheck\"",
+							"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run Govulncheck.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static\nanalysis tool that computes the set of functions reachable\nwithin your application, including dependencies;\nqueries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
 							"Default": "false"
 						},
 						{
 							"Name": "\"tidy\"",
-							"Doc": "Runs `go mod tidy` for a module.",
+							"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n",
 							"Default": "true"
 						},
 						{
 							"Name": "\"upgrade_dependency\"",
-							"Doc": "Upgrades a dependency in the go.mod file for a module.",
+							"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n",
 							"Default": "true"
 						},
 						{
 							"Name": "\"vendor\"",
-							"Doc": "Runs `go mod vendor` for a module.",
+							"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
 							"Default": "true"
 						}
 					]
 				},
 				"EnumValues": null,
-				"Default": "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}",
+				"Default": "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"run_govulncheck\":false,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}",
 				"Status": "",
 				"Hierarchy": "ui"
 			},
@@ -1173,44 +1173,60 @@
 	],
 	"Lenses": [
 		{
+			"FileType": "Go",
 			"Lens": "gc_details",
-			"Title": "Toggle gc_details",
-			"Doc": "Toggle the calculation of gc annotations."
+			"Title": "Toggle display of Go compiler optimization decisions",
+			"Doc": "\nThis codelens source causes the `package` declaration of\neach file to be annotated with a command to toggle the\nstate of the per-session variable that controls whether\noptimization decisions from the Go compiler (formerly known\nas \"gc\") should be displayed as diagnostics.\n\nOptimization decisions include:\n- whether a variable escapes, and how escape is inferred;\n- whether a nil-pointer check is implied or eliminated;\n- whether a function can be inlined.\n\nTODO(adonovan): this source is off by default because the\nannotation is annoying and because VS Code has a separate\n\"Toggle gc details\" command. Replace it with a Code Action\n(\"Source action...\").\n",
+			"Default": false
 		},
 		{
+			"FileType": "Go",
 			"Lens": "generate",
-			"Title": "Run go generate",
-			"Doc": "Runs `go generate` for a given directory."
+			"Title": "Run `go generate`",
+			"Doc": "\nThis codelens source annotates any `//go:generate` comments\nwith commands to run `go generate` in this directory, on\nall directories recursively beneath this one.\n\nSee [Generating code](https://go.dev/blog/generate) for\nmore details.\n",
+			"Default": true
 		},
 		{
+			"FileType": "Go",
 			"Lens": "regenerate_cgo",
-			"Title": "Regenerate cgo",
-			"Doc": "Regenerates cgo definitions."
+			"Title": "Re-generate cgo declarations",
+			"Doc": "\nThis codelens source annotates an `import \"C\"` declaration\nwith a command to re-run the [cgo\ncommand](https://pkg.go.dev/cmd/cgo) to regenerate the\ncorresponding Go declarations.\n\nUse this after editing the C code in comments attached to\nthe import, or in C header files included by it.\n",
+			"Default": true
 		},
 		{
-			"Lens": "run_govulncheck",
-			"Title": "Run vulncheck",
-			"Doc": "Run vulnerability check (`govulncheck`)."
-		},
-		{
+			"FileType": "Go",
 			"Lens": "test",
-			"Title": "Run test(s) (legacy)",
-			"Doc": "Runs `go test` for a specific set of test or benchmark functions."
+			"Title": "Run tests and benchmarks",
+			"Doc": "\nThis codelens source annotates each `Test` and `Benchmark`\nfunction in a `*_test.go` file with a command to run it.\n\nThis source is off by default because VS Code has\na more sophisticated client-side Test Explorer.\nSee golang/go#67400 for a discussion of this feature.\n",
+			"Default": false
 		},
 		{
+			"FileType": "go.mod",
+			"Lens": "run_govulncheck",
+			"Title": "Run govulncheck",
+			"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run Govulncheck.\n\n[Govulncheck](https://go.dev/blog/vuln) is a static\nanalysis tool that computes the set of functions reachable\nwithin your application, including dependencies;\nqueries a database of known security vulnerabilities; and\nreports any potential problems it finds.\n",
+			"Default": false
+		},
+		{
+			"FileType": "go.mod",
 			"Lens": "tidy",
-			"Title": "Run go mod tidy",
-			"Doc": "Runs `go mod tidy` for a module."
+			"Title": "Tidy go.mod file",
+			"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\ntidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures\nthat the go.mod file matches the source code in the module.\n",
+			"Default": true
 		},
 		{
+			"FileType": "go.mod",
 			"Lens": "upgrade_dependency",
-			"Title": "Upgrade a dependency",
-			"Doc": "Upgrades a dependency in the go.mod file for a module."
+			"Title": "Update dependencies",
+			"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with commands to:\n\n- check for available upgrades,\n- upgrade direct dependencies, and\n- upgrade all dependencies transitively.\n",
+			"Default": true
 		},
 		{
+			"FileType": "go.mod",
 			"Lens": "vendor",
-			"Title": "Run go mod vendor",
-			"Doc": "Runs `go mod vendor` for a module."
+			"Title": "Update vendor directory",
+			"Doc": "\nThis codelens source annotates the `module` directive in a\ngo.mod file with a command to run [`go mod\nvendor`](https://go.dev/ref/mod#go-mod-vendor), which\ncreates or updates the directory named `vendor` in the\nmodule root so that it contains an up-to-date copy of all\nnecessary package dependencies.\n",
+			"Default": true
 		}
 	],
 	"Analyzers": [
diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go
index 6e410fe..7b5ebc6 100644
--- a/gopls/internal/golang/code_lens.go
+++ b/gopls/internal/golang/code_lens.go
@@ -19,15 +19,13 @@
 	"golang.org/x/tools/gopls/internal/protocol/command"
 )
 
-type LensFunc func(context.Context, *cache.Snapshot, file.Handle) ([]protocol.CodeLens, error)
-
-// LensFuncs returns the supported lensFuncs for Go files.
-func LensFuncs() map[command.Command]LensFunc {
-	return map[command.Command]LensFunc{
-		command.Generate:      goGenerateCodeLens,
-		command.Test:          runTestCodeLens,
-		command.RegenerateCgo: regenerateCgoLens,
-		command.GCDetails:     toggleDetailsCodeLens,
+// CodeLensSources returns the supported sources of code lenses for Go files.
+func CodeLensSources() map[protocol.CodeLensSource]cache.CodeLensSourceFunc {
+	return map[protocol.CodeLensSource]cache.CodeLensSourceFunc{
+		protocol.CodeLensGenerate:      goGenerateCodeLens,    // commands: Generate
+		protocol.CodeLensTest:          runTestCodeLens,       // commands: Test
+		protocol.CodeLensRegenerateCgo: regenerateCgoLens,     // commands: RegenerateCgo
+		protocol.CodeLensGCDetails:     toggleDetailsCodeLens, // commands: GCDetails
 	}
 }
 
diff --git a/gopls/internal/mod/code_lens.go b/gopls/internal/mod/code_lens.go
index 85d8182..8994272 100644
--- a/gopls/internal/mod/code_lens.go
+++ b/gopls/internal/mod/code_lens.go
@@ -13,18 +13,17 @@
 	"golang.org/x/mod/modfile"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/file"
-	"golang.org/x/tools/gopls/internal/golang"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/protocol/command"
 )
 
-// LensFuncs returns the supported lensFuncs for go.mod files.
-func LensFuncs() map[command.Command]golang.LensFunc {
-	return map[command.Command]golang.LensFunc{
-		command.UpgradeDependency: upgradeLenses,
-		command.Tidy:              tidyLens,
-		command.Vendor:            vendorLens,
-		command.RunGovulncheck:    vulncheckLenses,
+// CodeLensSources returns the sources of code lenses for go.mod files.
+func CodeLensSources() map[protocol.CodeLensSource]cache.CodeLensSourceFunc {
+	return map[protocol.CodeLensSource]cache.CodeLensSourceFunc{
+		protocol.CodeLensUpgradeDependency: upgradeLenses,   // commands: CheckUpgrades, UpgradeDependency
+		protocol.CodeLensTidy:              tidyLens,        // commands: Tidy
+		protocol.CodeLensVendor:            vendorLens,      // commands: Vendor
+		protocol.CodeLensRunGovulncheck:    vulncheckLenses, // commands: RunGovulncheck
 	}
 }
 
diff --git a/gopls/internal/protocol/codeactionkind.go b/gopls/internal/protocol/codeactionkind.go
index 29bc6d4..88d3678 100644
--- a/gopls/internal/protocol/codeactionkind.go
+++ b/gopls/internal/protocol/codeactionkind.go
@@ -4,10 +4,130 @@
 
 package protocol
 
-// Custom code actions that aren't explicitly stated in LSP
+// This file defines constants for non-standard CodeActions and CodeLenses.
+
+// CodeAction kinds
+//
+// See tsprotocol.go for LSP standard kinds, including
+//
+//	"quickfix"
+//	"refactor"
+//	"refactor.extract"
+//	"refactor.inline"
+//	"refactor.move"
+//	"refactor.rewrite"
+//	"source"
+//	"source.organizeImports"
+//	"source.fixAll"
+//	"notebook"
 const (
 	GoTest CodeActionKind = "goTest"
-	// TODO: Add GoGenerate, RegenerateCgo etc.
+	GoDoc  CodeActionKind = "source.doc"
+)
 
-	GoDoc CodeActionKind = "source.doc"
+// A CodeLensSource identifies an (algorithmic) source of code lenses.
+type CodeLensSource string
+
+// CodeLens sources
+//
+// These identifiers appear in the "codelenses" configuration setting,
+// and in the user documentation thereof, which is generated by
+// gopls/doc/generate/generate.go parsing this file.
+//
+// Doc comments should use GitHub Markdown.
+// The first line becomes the title.
+//
+// (For historical reasons, each code lens source identifier typically
+// matches the name of one of the command.Commands returned by it,
+// but that isn't essential.)
+const (
+	// Toggle display of Go compiler optimization decisions
+	//
+	// This codelens source causes the `package` declaration of
+	// each file to be annotated with a command to toggle the
+	// state of the per-session variable that controls whether
+	// optimization decisions from the Go compiler (formerly known
+	// as "gc") should be displayed as diagnostics.
+	//
+	// Optimization decisions include:
+	// - whether a variable escapes, and how escape is inferred;
+	// - whether a nil-pointer check is implied or eliminated;
+	// - whether a function can be inlined.
+	//
+	// TODO(adonovan): this source is off by default because the
+	// annotation is annoying and because VS Code has a separate
+	// "Toggle gc details" command. Replace it with a Code Action
+	// ("Source action...").
+	CodeLensGCDetails CodeLensSource = "gc_details"
+
+	// Run `go generate`
+	//
+	// This codelens source annotates any `//go:generate` comments
+	// with commands to run `go generate` in this directory, on
+	// all directories recursively beneath this one.
+	//
+	// See [Generating code](https://go.dev/blog/generate) for
+	// more details.
+	CodeLensGenerate CodeLensSource = "generate"
+
+	// Re-generate cgo declarations
+	//
+	// This codelens source annotates an `import "C"` declaration
+	// with a command to re-run the [cgo
+	// command](https://pkg.go.dev/cmd/cgo) to regenerate the
+	// corresponding Go declarations.
+	//
+	// Use this after editing the C code in comments attached to
+	// the import, or in C header files included by it.
+	CodeLensRegenerateCgo CodeLensSource = "regenerate_cgo"
+
+	// Run govulncheck
+	//
+	// This codelens source annotates the `module` directive in a
+	// go.mod file with a command to run Govulncheck.
+	//
+	// [Govulncheck](https://go.dev/blog/vuln) is a static
+	// analysis tool that computes the set of functions reachable
+	// within your application, including dependencies;
+	// queries a database of known security vulnerabilities; and
+	// reports any potential problems it finds.
+	CodeLensRunGovulncheck CodeLensSource = "run_govulncheck"
+
+	// Run tests and benchmarks
+	//
+	// This codelens source annotates each `Test` and `Benchmark`
+	// function in a `*_test.go` file with a command to run it.
+	//
+	// This source is off by default because VS Code has
+	// a more sophisticated client-side Test Explorer.
+	// See golang/go#67400 for a discussion of this feature.
+	CodeLensTest CodeLensSource = "test"
+
+	// Tidy go.mod file
+	//
+	// This codelens source annotates the `module` directive in a
+	// go.mod file with a command to run [`go mod
+	// tidy`](https://go.dev/ref/mod#go-mod-tidy), which ensures
+	// that the go.mod file matches the source code in the module.
+	CodeLensTidy CodeLensSource = "tidy"
+
+	// Update dependencies
+	//
+	// This codelens source annotates the `module` directive in a
+	// go.mod file with commands to:
+	//
+	// - check for available upgrades,
+	// - upgrade direct dependencies, and
+	// - upgrade all dependencies transitively.
+	CodeLensUpgradeDependency CodeLensSource = "upgrade_dependency"
+
+	// Update vendor directory
+	//
+	// This codelens source annotates the `module` directive in a
+	// go.mod file with a command to run [`go mod
+	// vendor`](https://go.dev/ref/mod#go-mod-vendor), which
+	// creates or updates the directory named `vendor` in the
+	// module root so that it contains an up-to-date copy of all
+	// necessary package dependencies.
+	CodeLensVendor CodeLensSource = "vendor"
 )
diff --git a/gopls/internal/server/code_lens.go b/gopls/internal/server/code_lens.go
index cd37fe7..5a720cd 100644
--- a/gopls/internal/server/code_lens.go
+++ b/gopls/internal/server/code_lens.go
@@ -9,15 +9,17 @@
 	"fmt"
 	"sort"
 
+	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/golang"
 	"golang.org/x/tools/gopls/internal/label"
 	"golang.org/x/tools/gopls/internal/mod"
 	"golang.org/x/tools/gopls/internal/protocol"
-	"golang.org/x/tools/gopls/internal/protocol/command"
 	"golang.org/x/tools/internal/event"
 )
 
+// CodeLens reports the set of available CodeLenses
+// (range-associated commands) in the given file.
 func (s *server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) {
 	ctx, done := event.Start(ctx, "lsp.Server.codeLens", label.URI.Of(params.TextDocument.URI))
 	defer done()
@@ -28,36 +30,36 @@
 	}
 	defer release()
 
-	var lenses map[command.Command]golang.LensFunc
+	var lensFuncs map[protocol.CodeLensSource]cache.CodeLensSourceFunc
 	switch snapshot.FileKind(fh) {
 	case file.Mod:
-		lenses = mod.LensFuncs()
+		lensFuncs = mod.CodeLensSources()
 	case file.Go:
-		lenses = golang.LensFuncs()
+		lensFuncs = golang.CodeLensSources()
 	default:
 		// Unsupported file kind for a code lens.
 		return nil, nil
 	}
-	var result []protocol.CodeLens
-	for cmd, lf := range lenses {
-		if !snapshot.Options().Codelenses[string(cmd)] {
+	var lenses []protocol.CodeLens
+	for kind, lensFunc := range lensFuncs {
+		if !snapshot.Options().Codelenses[kind] {
 			continue
 		}
-		added, err := lf(ctx, snapshot, fh)
+		added, err := lensFunc(ctx, snapshot, fh)
 		// Code lens is called on every keystroke, so we should just operate in
 		// a best-effort mode, ignoring errors.
 		if err != nil {
-			event.Error(ctx, fmt.Sprintf("code lens %s failed", cmd), err)
+			event.Error(ctx, fmt.Sprintf("code lens %s failed", kind), err)
 			continue
 		}
-		result = append(result, added...)
+		lenses = append(lenses, added...)
 	}
-	sort.Slice(result, func(i, j int) bool {
-		a, b := result[i], result[j]
+	sort.Slice(lenses, func(i, j int) bool {
+		a, b := lenses[i], lenses[j]
 		if cmp := protocol.CompareRange(a.Range, b.Range); cmp != 0 {
 			return cmp < 0
 		}
 		return a.Command.Command < b.Command.Command
 	})
-	return result, nil
+	return lenses, nil
 }
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go
index 3ac3d2b..15e82ec 100644
--- a/gopls/internal/settings/default.go
+++ b/gopls/internal/settings/default.go
@@ -99,14 +99,14 @@
 						ExperimentalPostfixCompletions: true,
 						CompleteFunctionCalls:          true,
 					},
-					Codelenses: map[string]bool{
-						string(command.Generate):          true,
-						string(command.RegenerateCgo):     true,
-						string(command.Tidy):              true,
-						string(command.GCDetails):         false,
-						string(command.UpgradeDependency): true,
-						string(command.Vendor):            true,
-						// TODO(hyangah): enable command.RunGovulncheck.
+					Codelenses: map[protocol.CodeLensSource]bool{
+						protocol.CodeLensGenerate:          true,
+						protocol.CodeLensRegenerateCgo:     true,
+						protocol.CodeLensTidy:              true,
+						protocol.CodeLensGCDetails:         false,
+						protocol.CodeLensUpgradeDependency: true,
+						protocol.CodeLensVendor:            true,
+						protocol.CodeLensRunGovulncheck:    false, // TODO(hyangah): enable
 					},
 					SemanticTokens: true,
 				},
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go
index 05e2629..9a800ed 100644
--- a/gopls/internal/settings/settings.go
+++ b/gopls/internal/settings/settings.go
@@ -13,6 +13,8 @@
 
 	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/protocol"
+	"golang.org/x/tools/gopls/internal/util/maps"
+	"golang.org/x/tools/gopls/internal/util/slices"
 )
 
 type Annotation string
@@ -183,7 +185,7 @@
 	// ...
 	// }
 	// ```
-	Codelenses map[string]bool
+	Codelenses map[protocol.CodeLensSource]bool
 
 	// SemanticTokens controls whether the LSP server will send
 	// semantic tokens to the client. If false, gopls will send empty semantic
@@ -603,7 +605,7 @@
 
 type OptionResult struct {
 	Name  string
-	Value any
+	Value any // JSON value (e.g. string, int, bool, map[string]any)
 	Error error
 }
 
@@ -685,25 +687,12 @@
 		UserOptions:     o.UserOptions,
 	}
 	// Fully clone any slice or map fields. Only UserOptions can be modified.
-	copyStringMap := func(src map[string]bool) map[string]bool {
-		dst := make(map[string]bool)
-		for k, v := range src {
-			dst[k] = v
-		}
-		return dst
-	}
-	result.Analyses = copyStringMap(o.Analyses)
-	result.Codelenses = copyStringMap(o.Codelenses)
-
-	copySlice := func(src []string) []string {
-		dst := make([]string, len(src))
-		copy(dst, src)
-		return dst
-	}
+	result.Analyses = maps.Clone(o.Analyses)
+	result.Codelenses = maps.Clone(o.Codelenses)
 	result.SetEnvSlice(o.EnvSlice())
-	result.BuildFlags = copySlice(o.BuildFlags)
-	result.DirectoryFilters = copySlice(o.DirectoryFilters)
-	result.StandaloneTags = copySlice(o.StandaloneTags)
+	result.BuildFlags = slices.Clone(o.BuildFlags)
+	result.DirectoryFilters = slices.Clone(o.DirectoryFilters)
+	result.StandaloneTags = slices.Clone(o.StandaloneTags)
 
 	return result
 }
@@ -732,12 +721,12 @@
 	return strings.TrimRight(filepath.FromSlash(filter), "/"), nil
 }
 
-func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult {
+func (o *Options) set(name string, value any, seen map[string]struct{}) OptionResult {
 	// Flatten the name in case we get options with a hierarchy.
 	split := strings.Split(name, ".")
 	name = split[len(split)-1]
 
-	result := OptionResult{Name: name, Value: value}
+	result := &OptionResult{Name: name, Value: value}
 	if _, ok := seen[name]; ok {
 		result.parseErrorf("duplicate configuration for %s", name)
 	}
@@ -745,7 +734,7 @@
 
 	switch name {
 	case "env":
-		menv, ok := value.(map[string]interface{})
+		menv, ok := value.(map[string]any)
 		if !ok {
 			result.parseErrorf("invalid type %T, expect map", value)
 			break
@@ -759,7 +748,7 @@
 
 	case "buildFlags":
 		// TODO(rfindley): use asStringSlice.
-		iflags, ok := value.([]interface{})
+		iflags, ok := value.([]any)
 		if !ok {
 			result.parseErrorf("invalid type %T, expect list", value)
 			break
@@ -772,7 +761,7 @@
 
 	case "directoryFilters":
 		// TODO(rfindley): use asStringSlice.
-		ifilters, ok := value.([]interface{})
+		ifilters, ok := value.([]any)
 		if !ok {
 			result.parseErrorf("invalid type %T, expect list", value)
 			break
@@ -782,7 +771,7 @@
 			filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter))
 			if err != nil {
 				result.parseErrorf("%v", err)
-				return result
+				return *result
 			}
 			filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/"))
 		}
@@ -859,10 +848,10 @@
 		}
 
 	case "analyses":
-		result.setBoolMap(&o.Analyses)
+		o.Analyses = asBoolMap[string](result)
 
 	case "hints":
-		result.setBoolMap(&o.Hints)
+		o.Hints = asBoolMap[string](result)
 
 	case "annotations":
 		result.setAnnotationMap(&o.Annotations)
@@ -876,14 +865,13 @@
 		}
 
 	case "codelenses", "codelens":
-		var lensOverrides map[string]bool
-		result.setBoolMap(&lensOverrides)
+		lensOverrides := asBoolMap[protocol.CodeLensSource](result)
 		if result.Error == nil {
 			if o.Codelenses == nil {
-				o.Codelenses = make(map[string]bool)
+				o.Codelenses = make(map[protocol.CodeLensSource]bool)
 			}
-			for lens, enabled := range lensOverrides {
-				o.Codelenses[lens] = enabled
+			for source, enabled := range lensOverrides {
+				o.Codelenses[source] = enabled
 			}
 		}
 
@@ -953,7 +941,7 @@
 		result.deprecated("")
 
 	case "templateExtensions":
-		if iexts, ok := value.([]interface{}); ok {
+		if iexts, ok := value.([]any); ok {
 			ans := []string{}
 			for _, x := range iexts {
 				ans = append(ans, fmt.Sprint(x))
@@ -1076,11 +1064,11 @@
 	default:
 		result.unexpected()
 	}
-	return result
+	return *result
 }
 
 // parseErrorf reports an error parsing the current configuration value.
-func (r *OptionResult) parseErrorf(msg string, values ...interface{}) {
+func (r *OptionResult) parseErrorf(msg string, values ...any) {
 	if false {
 		_ = fmt.Sprintf(msg, values...) // this causes vet to check this like printf
 	}
@@ -1143,13 +1131,8 @@
 	}
 }
 
-func (r *OptionResult) setBoolMap(bm *map[string]bool) {
-	m := r.asBoolMap()
-	*bm = m
-}
-
 func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) {
-	all := r.asBoolMap()
+	all := asBoolMap[string](r)
 	if all == nil {
 		return
 	}
@@ -1188,16 +1171,16 @@
 	*bm = m
 }
 
-func (r *OptionResult) asBoolMap() map[string]bool {
-	all, ok := r.Value.(map[string]interface{})
+func asBoolMap[K ~string](r *OptionResult) map[K]bool {
+	all, ok := r.Value.(map[string]any)
 	if !ok {
 		r.parseErrorf("invalid type %T for map[string]bool option", r.Value)
 		return nil
 	}
-	m := make(map[string]bool)
+	m := make(map[K]bool)
 	for a, enabled := range all {
 		if e, ok := enabled.(bool); ok {
-			m[a] = e
+			m[K(a)] = e
 		} else {
 			r.parseErrorf("invalid type %T for map key %q", enabled, a)
 			return m
@@ -1216,7 +1199,7 @@
 }
 
 func (r *OptionResult) asStringSlice() ([]string, bool) {
-	iList, ok := r.Value.([]interface{})
+	iList, ok := r.Value.([]any)
 	if !ok {
 		r.parseErrorf("invalid type %T, expect list", r.Value)
 		return nil, false
diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go
index 28ef2db..dd3526a 100644
--- a/gopls/internal/settings/settings_test.go
+++ b/gopls/internal/settings/settings_test.go
@@ -21,7 +21,7 @@
 func TestSetOption(t *testing.T) {
 	type testCase struct {
 		name      string
-		value     interface{}
+		value     any
 		wantError bool
 		check     func(Options) bool
 	}
@@ -55,7 +55,7 @@
 		},
 		{
 			name:  "codelenses",
-			value: map[string]interface{}{"generate": true},
+			value: map[string]any{"generate": true},
 			check: func(o Options) bool { return o.Codelenses["generate"] },
 		},
 		{
@@ -123,7 +123,7 @@
 		},
 		{
 			name:  "env",
-			value: map[string]interface{}{"testing": "true"},
+			value: map[string]any{"testing": "true"},
 			check: func(o Options) bool {
 				v, found := o.Env["testing"]
 				return found && v == "true"
@@ -139,14 +139,14 @@
 		},
 		{
 			name:  "directoryFilters",
-			value: []interface{}{"-node_modules", "+project_a"},
+			value: []any{"-node_modules", "+project_a"},
 			check: func(o Options) bool {
 				return len(o.DirectoryFilters) == 2
 			},
 		},
 		{
 			name:      "directoryFilters",
-			value:     []interface{}{"invalid"},
+			value:     []any{"invalid"},
 			wantError: true,
 			check: func(o Options) bool {
 				return len(o.DirectoryFilters) == 0
@@ -162,7 +162,7 @@
 		},
 		{
 			name: "annotations",
-			value: map[string]interface{}{
+			value: map[string]any{
 				"Nil":      false,
 				"noBounds": true,
 			},
@@ -173,7 +173,7 @@
 		},
 		{
 			name:      "vulncheck",
-			value:     []interface{}{"invalid"},
+			value:     []any{"invalid"},
 			wantError: true,
 			check: func(o Options) bool {
 				return o.Vulncheck == "" // For invalid value, default to 'off'.
diff --git a/gopls/internal/util/maps/maps.go b/gopls/internal/util/maps/maps.go
index 92368b6..daa9c3d 100644
--- a/gopls/internal/util/maps/maps.go
+++ b/gopls/internal/util/maps/maps.go
@@ -45,3 +45,12 @@
 	}
 	return true
 }
+
+// Clone returns a new map with the same entries as m.
+func Clone[M ~map[K]V, K comparable, V any](m M) M {
+	copy := make(map[K]V, len(m))
+	for k, v := range m {
+		copy[k] = v
+	}
+	return copy
+}