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
+}