gopls/internal/golang: Hover: use internal pkg doc viewer
This change changes the linksInHover option from bool to
a sum of false | true | "gopls".
The "gopls" setting causes Hover(SynopsisDocumentation)
to generate links to gopls' internal web-based doc viewer.
Thanks to Hana for the idea.
+ Test, release note
Fixes golang/go#67949
Change-Id: I384796780436b191a0711c60085d67363d00e5f6
Reviewed-on: https://go-review.googlesource.com/c/tools/+/572037
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/release/v0.16.0.md b/gopls/doc/release/v0.16.0.md
index dcb6005..8060d21 100644
--- a/gopls/doc/release/v0.16.0.md
+++ b/gopls/doc/release/v0.16.0.md
@@ -107,6 +107,11 @@
- TODO: test in vim, neovim, sublime, helix.
+The `linksInHover` setting now supports a new value, `"gopls"`,
+that causes documentation links in the the Markdown output
+of the Hover operation to link to gopls' internal doc viewer.
+
+
### Browse free symbols
Gopls offers another web-based code action, "Browse free symbols",
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 83f6280..1a08c56 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -439,9 +439,15 @@
Default: `"pkg.go.dev"`.
<a id='linksInHover'></a>
-### `linksInHover` *bool*
+### `linksInHover` *any*
-linksInHover toggles the presence of links to documentation in hover.
+linksInHover controls the presence of documentation links
+in hover markdown.
+
+Its legal values are:
+- `false`, for no links;
+- `true`, for links to the `linkTarget` domain; or
+- `"gopls"`, for links to gopls' internal documentation viewer.
Default: `true`.
diff --git a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go
index 7aa1e2c..26dc0c4 100644
--- a/gopls/internal/cache/errors.go
+++ b/gopls/internal/cache/errors.go
@@ -388,7 +388,7 @@
}
// BuildLink constructs a URL with the given target, path, and anchor.
-func BuildLink(target, path, anchor string) string {
+func BuildLink(target, path, anchor string) protocol.URI {
link := fmt.Sprintf("https://%s/%s", target, path)
if anchor == "" {
return link
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index a789483..96ff054 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -154,8 +154,8 @@
},
{
"Name": "linksInHover",
- "Type": "bool",
- "Doc": "linksInHover toggles the presence of links to documentation in hover.\n",
+ "Type": "any",
+ "Doc": "linksInHover controls the presence of documentation links\nin hover markdown.\n\nIts legal values are:\n- `false`, for no links;\n- `true`, for links to the `linkTarget` domain; or\n- `\"gopls\"`, for links to gopls' internal documentation viewer.\n",
"EnumKeys": {
"ValueType": "",
"Keys": null
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go
index ac92fcf..81594a6 100644
--- a/gopls/internal/golang/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -98,7 +98,9 @@
}
// Hover implements the "textDocument/hover" RPC for Go files.
-func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) {
+//
+// If pkgURL is non-nil, it should be used to generate doc links.
+func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position, pkgURL func(path PackagePath, fragment string) protocol.URI) (*protocol.Hover, error) {
ctx, done := event.Start(ctx, "golang.Hover")
defer done()
@@ -109,7 +111,7 @@
if h == nil {
return nil, nil
}
- hover, err := formatHover(h, snapshot.Options())
+ hover, err := formatHover(h, snapshot.Options(), pkgURL)
if err != nil {
return nil, err
}
@@ -1088,7 +1090,8 @@
return pgf, fullPos, nil
}
-func formatHover(h *hoverJSON, options *settings.Options) (string, error) {
+// If pkgURL is non-nil, it should be used to generate doc links.
+func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) (string, error) {
maybeMarkdown := func(s string) string {
if s != "" && options.PreferredContentFormat == protocol.Markdown {
s = fmt.Sprintf("```go\n%s\n```", strings.Trim(s, "\n"))
@@ -1123,7 +1126,7 @@
formatDoc(h, options),
maybeMarkdown(h.promotedFields),
maybeMarkdown(h.methods),
- formatLink(h, options),
+ formatLink(h, options, pkgURL),
}
if h.typeDecl != "" {
parts[0] = "" // type: suppress redundant Signature
@@ -1148,18 +1151,30 @@
}
}
-func formatLink(h *hoverJSON, options *settings.Options) string {
- if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" {
+// If pkgURL is non-nil, it should be used to generate doc links.
+func formatLink(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string {
+ if options.LinksInHover == false || h.LinkPath == "" {
return ""
}
- plainLink := cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
+ var url protocol.URI
+ var caption string
+ if pkgURL != nil { // LinksInHover == "gopls"
+ url = pkgURL(PackagePath(h.LinkPath), h.LinkAnchor)
+ caption = "in gopls doc viewer"
+ } else {
+ if options.LinkTarget == "" {
+ return ""
+ }
+ url = cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor)
+ caption = "on " + options.LinkTarget
+ }
switch options.PreferredContentFormat {
case protocol.Markdown:
- return fmt.Sprintf("[`%s` on %s](%s)", h.SymbolName, options.LinkTarget, plainLink)
+ return fmt.Sprintf("[`%s` %s](%s)", h.SymbolName, caption, url)
case protocol.PlainText:
return ""
default:
- return plainLink
+ return url
}
}
diff --git a/gopls/internal/server/hover.go b/gopls/internal/server/hover.go
index c359825..1470210 100644
--- a/gopls/internal/server/hover.go
+++ b/gopls/internal/server/hover.go
@@ -37,7 +37,18 @@
case file.Mod:
return mod.Hover(ctx, snapshot, fh, params.Position)
case file.Go:
- return golang.Hover(ctx, snapshot, fh, params.Position)
+ var pkgURL func(path golang.PackagePath, fragment string) protocol.URI
+ if snapshot.Options().LinksInHover == "gopls" {
+ web, err := s.getWeb()
+ if err != nil {
+ event.Error(ctx, "failed to start web server", err)
+ } else {
+ pkgURL = func(path golang.PackagePath, fragment string) protocol.URI {
+ return web.PkgURL(snapshot.View().ID(), path, fragment)
+ }
+ }
+ }
+ return golang.Hover(ctx, snapshot, fh, params.Position, pkgURL)
case file.Tmpl:
return template.Hover(ctx, snapshot, fh, params.Position)
case file.Work:
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go
index 18674c9..a5e9518 100644
--- a/gopls/internal/settings/settings.go
+++ b/gopls/internal/settings/settings.go
@@ -244,8 +244,14 @@
// documentation links in hover.
LinkTarget string
- // LinksInHover toggles the presence of links to documentation in hover.
- LinksInHover bool
+ // LinksInHover controls the presence of documentation links
+ // in hover markdown.
+ //
+ // Its legal values are:
+ // - `false`, for no links;
+ // - `true`, for links to the `linkTarget` domain; or
+ // - `"gopls"`, for links to gopls' internal documentation viewer.
+ LinksInHover any
}
// Note: FormattingOptions must be comparable with reflect.DeepEqual.
@@ -810,7 +816,13 @@
return setString(&o.LinkTarget, value)
case "linksInHover":
- return setBool(&o.LinksInHover, value)
+ switch value {
+ case false, true, "gopls":
+ o.LinksInHover = value
+ default:
+ return fmt.Errorf(`invalid value %s; expect false, true, or "gopls"`,
+ value)
+ }
case "importShortcut":
return setEnum(&o.ImportShortcut, value,
diff --git a/gopls/internal/test/integration/misc/hover_test.go b/gopls/internal/test/integration/misc/hover_test.go
index 3853938..d3445dd 100644
--- a/gopls/internal/test/integration/misc/hover_test.go
+++ b/gopls/internal/test/integration/misc/hover_test.go
@@ -6,6 +6,7 @@
import (
"fmt"
+ "regexp"
"strings"
"testing"
@@ -514,3 +515,41 @@
_, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("p.go", "foo[.]"))
})
}
+
+func TestHoverInternalLinks(t *testing.T) {
+ const src = `
+-- main.go --
+package main
+
+import "errors"
+
+func main() {
+ errors.New("oops")
+}
+`
+ for _, test := range []struct {
+ linksInHover any // JSON configuration value
+ wantRE string // pattern to match the Hover Markdown output
+ }{
+ {
+ true, // default: use options.LinkTarget domain
+ regexp.QuoteMeta("[`errors.New` on pkg.go.dev](https://pkg.go.dev/errors#New)"),
+ },
+ {
+ "gopls", // use gopls' internal viewer
+ "\\[`errors.New` in gopls doc viewer\\]\\(http://127.0.0.1:[0-9]+/gopls/[^/]+/pkg/errors\\?view=[0-9]+#New\\)",
+ },
+ } {
+ WithOptions(
+ Settings{"linksInHover": test.linksInHover},
+ ).Run(t, src, func(t *testing.T, env *Env) {
+ env.OpenFile("main.go")
+ got, _ := env.Hover(env.RegexpSearch("main.go", "New"))
+ if m, err := regexp.MatchString(test.wantRE, got.Value); err != nil {
+ t.Fatalf("bad regexp in test: %v", err)
+ } else if !m {
+ t.Fatalf("hover output does not match %q; got:\n\n%s", test.wantRE, got.Value)
+ }
+ })
+ }
+}