Skip to content

Commit 118b98b

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/golang: RenderPackageDoc: emit anchors for var/const
This CL causes the package rendering to emit an <a id=...> anchor for each name defined by a var or const declaration. Also, choose the initial fragment id (e.g. "#Buffer.Len") based on the selection. + Test that anchors are emitted. Change-Id: Idb4838a6d2741a26cd9dbb5ad31a76d6f811ff26 Reviewed-on: https://go-review.googlesource.com/c/tools/+/575697 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 1e68fee commit 118b98b

File tree

4 files changed

+110
-28
lines changed

4 files changed

+110
-28
lines changed

gopls/doc/release/v0.16.0.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ go install golang.org/x/tools/[email protected]
1010

1111
Gopls now offers a "View package documentation" code action that opens
1212
a local web page displaying the generated documentation for the
13-
current Go package in a form similar to https://pkg.go.dev. Use it to
14-
preview the marked-up documentation as you prepare API changes, or to
15-
read the documentation for locally edited packages, even ones that
16-
have not yet been saved. Reload the page after an edit to see updated
17-
documentation.
13+
current Go package in a form similar to https://pkg.go.dev.
14+
The page will be initially scrolled to the documentation for the
15+
declaration containing the cursor.
16+
Use this feature to preview the marked-up documentation as you prepare API
17+
changes, or to read the documentation for locally edited packages,
18+
even ones that have not yet been saved. Reload the page after an edit
19+
to see updated documentation.
1820

1921
TODO: demo in VS Code.
2022

gopls/internal/golang/pkgdoc.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ package golang
2020
// - abbreviate long signatures by replacing parameters 4 onwards with "...".
2121
// - style the <li> bullets in the index as invisible.
2222
// - add push notifications such as didChange -> reload.
23+
// - there appears to be a maximum file size beyond which the
24+
// "source.doc" code action is not offered. Remove that.
2325

2426
import (
2527
"bytes"
@@ -254,8 +256,6 @@ function httpGET(url) {
254256
emit(n.Pos())
255257
pos = n.End()
256258
if url := linkify(n); url != "" {
257-
// TODO(adonovan): emit anchors (not hrefs)
258-
// for package-level defining idents.
259259
fmt.Fprintf(&buf, "<a class='id' href='%s'>%s</a>", url, escape(n.Name))
260260
} else {
261261
buf.WriteString(escape(n.Name)) // plain
@@ -355,6 +355,11 @@ function httpGET(url) {
355355
// constants and variables
356356
values := func(vals []*doc.Value) {
357357
for _, v := range vals {
358+
// anchors
359+
for _, name := range v.Names {
360+
fmt.Fprintf(&buf, "<a id='%s'></a>\n", escape(name))
361+
}
362+
358363
// declaration
359364
decl2 := *v.Decl // shallow copy
360365
decl2.Doc = nil

gopls/internal/server/command.go

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"encoding/json"
1111
"errors"
1212
"fmt"
13+
"go/ast"
1314
"io"
1415
"os"
1516
"path/filepath"
@@ -507,16 +508,16 @@ func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error {
507508
progress: "", // the operation should be fast
508509
forURI: loc.URI,
509510
}, func(ctx context.Context, deps commandDeps) error {
510-
pkg, _, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, loc.URI)
511+
pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, loc.URI)
511512
if err != nil {
512513
return err
513514
}
514515

515516
// When invoked from a _test.go file, show the
516517
// documentation of the package under test.
517-
path := pkg.Metadata().PkgPath
518+
pkgpath := pkg.Metadata().PkgPath
518519
if pkg.Metadata().ForTest != "" {
519-
path = pkg.Metadata().ForTest
520+
pkgpath = pkg.Metadata().ForTest
520521
}
521522

522523
// Start web server.
@@ -525,12 +526,59 @@ func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error {
525526
return err
526527
}
527528

529+
// Compute fragment (e.g. "#Buffer.Len") based on
530+
// enclosing top-level declaration, if exported.
531+
var fragment string
532+
pos, err := pgf.PositionPos(loc.Range.Start)
533+
if err != nil {
534+
return err
535+
}
536+
path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
537+
if n := len(path); n > 1 {
538+
switch decl := path[n-2].(type) {
539+
case *ast.FuncDecl:
540+
if decl.Name.IsExported() {
541+
// e.g. "#Println"
542+
fragment = decl.Name.Name
543+
544+
// method?
545+
if decl.Recv != nil && len(decl.Recv.List) > 0 {
546+
recv := decl.Recv.List[0].Type
547+
if star, ok := recv.(*ast.StarExpr); ok {
548+
recv = star.X // *N -> N
549+
}
550+
if id, ok := recv.(*ast.Ident); ok && id.IsExported() {
551+
// e.g. "#Buffer.Len"
552+
fragment = id.Name + "." + fragment
553+
} else {
554+
fragment = ""
555+
}
556+
}
557+
}
558+
559+
case *ast.GenDecl:
560+
// path=[... Spec? GenDecl File]
561+
for _, spec := range decl.Specs {
562+
if n > 2 && spec == path[n-3] {
563+
var name *ast.Ident
564+
switch spec := spec.(type) {
565+
case *ast.ValueSpec:
566+
// var, const: use first name
567+
name = spec.Names[0]
568+
case *ast.TypeSpec:
569+
name = spec.Name
570+
}
571+
if name != nil && name.IsExported() {
572+
fragment = name.Name
573+
}
574+
break
575+
}
576+
}
577+
}
578+
}
579+
528580
// Direct the client to open the /pkg page.
529-
//
530-
// TODO(adonovan): compute fragment (e.g. "#fmt.Println") based on loc.Range.
531-
// (Should it document the selected symbol, or the enclosing decl?)
532-
fragment := ""
533-
url := web.pkgURL(deps.snapshot.View(), path, fragment)
581+
url := web.pkgURL(deps.snapshot.View(), pkgpath, fragment)
534582
openClientBrowser(ctx, c.s.client, url)
535583

536584
return nil

gopls/internal/test/integration/misc/webserver_test.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -81,34 +81,61 @@ func TestRenderNoPanic66449(t *testing.T) {
8181
// that would corrupt the AST while filtering out unexported
8282
// symbols such as b, causing nodeHTML to panic.
8383
// Now it doesn't crash.
84+
//
85+
// We also check cross-reference anchors for all symbols.
8486
const files = `
8587
-- go.mod --
8688
module example.com
8789
8890
-- a/a.go --
8991
package a
9092
91-
var A, b = 0, 0
93+
// The '1' suffix is to reduce risk of spurious matches with other HTML substrings.
9294
93-
func ExportedFunc()
94-
func unexportedFunc()
95-
type ExportedType int
96-
type unexportedType int
95+
var V1, v1 = 0, 0
96+
const C1, c1 = 0, 0
97+
98+
func F1()
99+
func f1()
100+
101+
type T1 int
102+
type t1 int
103+
104+
func (T1) M1() {}
105+
func (T1) m1() {}
106+
107+
func (t1) M1() {}
108+
func (t1) m1() {}
97109
`
98110
Run(t, files, func(t *testing.T, env *Env) {
99111
uri1 := viewPkgDoc(t, env, "a/a.go")
100-
doc1 := get(t, uri1)
112+
doc := get(t, uri1)
101113
// (Ideally our code rendering would also
102114
// eliminate unexported symbols...)
103-
// TODO(adonovan): when we emit var anchors,
104-
// check that #A exists and #b does not.
105-
checkMatch(t, true, doc1, "var A, b = .*0.*0")
115+
checkMatch(t, true, doc, "var V1, v1 = .*0.*0")
116+
checkMatch(t, true, doc, "const C1, c1 = .*0.*0")
106117

107118
// Unexported funcs/types/... must still be discarded.
108-
checkMatch(t, true, doc1, "ExportedFunc")
109-
checkMatch(t, false, doc1, "unexportedFunc")
110-
checkMatch(t, true, doc1, "ExportedType")
111-
checkMatch(t, false, doc1, "unexportedType")
119+
checkMatch(t, true, doc, "F1")
120+
checkMatch(t, false, doc, "f1")
121+
checkMatch(t, true, doc, "T1")
122+
checkMatch(t, false, doc, "t1")
123+
124+
// Also, check that anchors exist (only) for exported symbols.
125+
// exported:
126+
checkMatch(t, true, doc, "<a id='V1'")
127+
checkMatch(t, true, doc, "<a id='C1'")
128+
checkMatch(t, true, doc, "<h3 id='T1'")
129+
checkMatch(t, true, doc, "<h3 id='F1'")
130+
checkMatch(t, true, doc, "<h4 id='T1.M1'")
131+
// unexported:
132+
checkMatch(t, false, doc, "<a id='v1'")
133+
checkMatch(t, false, doc, "<a id='c1'")
134+
checkMatch(t, false, doc, "<h3 id='t1'")
135+
checkMatch(t, false, doc, "<h3 id='f1'")
136+
checkMatch(t, false, doc, "<h4 id='T1.m1'")
137+
checkMatch(t, false, doc, "<h4 id='t1.M1'")
138+
checkMatch(t, false, doc, "<h4 id='t1.m1'")
112139
})
113140
}
114141

0 commit comments

Comments
 (0)