Skip to content

Commit c3c5164

Browse files
rogerykgopherbot
authored andcommitted
gopls/internal/golang: support hover and definition operations over doc links
Now gopls has already support highlight doc links, and it would be useful to support hover and defition over doc links. Fixes golang/go#64648 Change-Id: I53d4e41ec7328fca6cf4988b576b2893d6a02434 Reviewed-on: https://go-review.googlesource.com/c/tools/+/554915 Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent da3408b commit c3c5164

File tree

7 files changed

+306
-32
lines changed

7 files changed

+306
-32
lines changed

gopls/internal/golang/comment.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,24 @@
55
package golang
66

77
import (
8+
"context"
9+
"errors"
810
"fmt"
11+
"go/ast"
912
"go/doc/comment"
13+
"go/token"
14+
"go/types"
15+
"strings"
1016

17+
"golang.org/x/tools/gopls/internal/cache"
18+
"golang.org/x/tools/gopls/internal/cache/parsego"
19+
"golang.org/x/tools/gopls/internal/protocol"
1120
"golang.org/x/tools/gopls/internal/settings"
21+
"golang.org/x/tools/gopls/internal/util/safetoken"
1222
)
1323

24+
var errNoCommentReference = errors.New("no comment reference found")
25+
1426
// CommentToMarkdown converts comment text to formatted markdown.
1527
// The comment was prepared by DocReader,
1628
// so it is known not to have leading, trailing blank lines
@@ -39,3 +51,127 @@ func CommentToMarkdown(text string, options *settings.Options) string {
3951
easy := pr.Markdown(doc)
4052
return string(easy)
4153
}
54+
55+
// docLinkDefinition finds the definition of the doc link in comments at pos.
56+
// If there is no reference at pos, returns errNoCommentReference.
57+
func docLinkDefinition(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) {
58+
obj, _, err := parseDocLink(pkg, pgf, pos)
59+
if err != nil {
60+
return nil, err
61+
}
62+
loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj))
63+
if err != nil {
64+
return nil, err
65+
}
66+
return []protocol.Location{loc}, nil
67+
}
68+
69+
// parseDocLink parses a doc link in a comment such as [fmt.Println]
70+
// and returns the symbol at pos, along with the link's start position.
71+
func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.Object, protocol.Range, error) {
72+
var comment *ast.Comment
73+
for _, cg := range pgf.File.Comments {
74+
for _, c := range cg.List {
75+
if c.Pos() <= pos && pos <= c.End() {
76+
comment = c
77+
break
78+
}
79+
}
80+
if comment != nil {
81+
break
82+
}
83+
}
84+
if comment == nil {
85+
return nil, protocol.Range{}, errNoCommentReference
86+
}
87+
88+
// The canonical parsing algorithm is defined by go/doc/comment, but
89+
// unfortunately its API provides no way to reliably reconstruct the
90+
// position of each doc link from the parsed result.
91+
line := safetoken.Line(pgf.Tok, pos)
92+
var start, end token.Pos
93+
if pgf.Tok.LineStart(line) > comment.Pos() {
94+
start = pgf.Tok.LineStart(line)
95+
} else {
96+
start = comment.Pos()
97+
}
98+
if line < pgf.Tok.LineCount() && pgf.Tok.LineStart(line+1) < comment.End() {
99+
end = pgf.Tok.LineStart(line + 1)
100+
} else {
101+
end = comment.End()
102+
}
103+
104+
offsetStart, offsetEnd, err := safetoken.Offsets(pgf.Tok, start, end)
105+
if err != nil {
106+
return nil, protocol.Range{}, err
107+
}
108+
109+
text := string(pgf.Src[offsetStart:offsetEnd])
110+
lineOffset := int(pos - start)
111+
112+
for _, idx := range docLinkRegex.FindAllStringSubmatchIndex(text, -1) {
113+
// The [idx[2], idx[3]) identifies the first submatch,
114+
// which is the reference name in the doc link.
115+
// e.g. The "[fmt.Println]" reference name is "fmt.Println".
116+
if !(idx[2] <= lineOffset && lineOffset < idx[3]) {
117+
continue
118+
}
119+
p := lineOffset - idx[2]
120+
name := text[idx[2]:idx[3]]
121+
i := strings.LastIndexByte(name, '.')
122+
for i != -1 {
123+
if p > i {
124+
break
125+
}
126+
name = name[:i]
127+
i = strings.LastIndexByte(name, '.')
128+
}
129+
obj := lookupObjectByName(pkg, pgf, name)
130+
if obj == nil {
131+
return nil, protocol.Range{}, errNoCommentReference
132+
}
133+
namePos := start + token.Pos(idx[2]+i+1)
134+
rng, err := pgf.PosRange(namePos, namePos+token.Pos(len(obj.Name())))
135+
if err != nil {
136+
return nil, protocol.Range{}, err
137+
}
138+
return obj, rng, nil
139+
}
140+
141+
return nil, protocol.Range{}, errNoCommentReference
142+
}
143+
144+
func lookupObjectByName(pkg *cache.Package, pgf *parsego.File, name string) types.Object {
145+
scope := pkg.Types().Scope()
146+
fileScope := pkg.TypesInfo().Scopes[pgf.File]
147+
pkgName, suffix, _ := strings.Cut(name, ".")
148+
obj, ok := fileScope.Lookup(pkgName).(*types.PkgName)
149+
if ok {
150+
scope = obj.Imported().Scope()
151+
if suffix == "" {
152+
return obj
153+
}
154+
name = suffix
155+
}
156+
157+
recv, method, ok := strings.Cut(name, ".")
158+
if ok {
159+
obj, ok := scope.Lookup(recv).(*types.TypeName)
160+
if !ok {
161+
return nil
162+
}
163+
t, ok := obj.Type().(*types.Named)
164+
if !ok {
165+
return nil
166+
}
167+
for i := 0; i < t.NumMethods(); i++ {
168+
m := t.Method(i)
169+
if m.Name() == method {
170+
return m
171+
}
172+
}
173+
return nil
174+
}
175+
176+
return scope.Lookup(name)
177+
}

gopls/internal/golang/definition.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,19 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p
6666
// Handle the case where the cursor is in a linkname directive.
6767
locations, err := LinknameDefinition(ctx, snapshot, pgf.Mapper, position)
6868
if !errors.Is(err, ErrNoLinkname) {
69-
return locations, err
69+
return locations, err // may be success or failure
7070
}
7171

7272
// Handle the case where the cursor is in an embed directive.
7373
locations, err = EmbedDefinition(pgf.Mapper, position)
7474
if !errors.Is(err, ErrNoEmbed) {
75-
return locations, err
75+
return locations, err // may be success or failure
76+
}
77+
78+
// Handle the case where the cursor is in a doc link.
79+
locations, err = docLinkDefinition(ctx, snapshot, pkg, pgf, pos)
80+
if !errors.Is(err, errNoCommentReference) {
81+
return locations, err // may be success or failure
7682
}
7783

7884
// The general case: the cursor is on an identifier.

gopls/internal/golang/hover.go

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"golang.org/x/tools/gopls/internal/file"
3434
"golang.org/x/tools/gopls/internal/protocol"
3535
"golang.org/x/tools/gopls/internal/settings"
36+
gastutil "golang.org/x/tools/gopls/internal/util/astutil"
3637
"golang.org/x/tools/gopls/internal/util/bug"
3738
"golang.org/x/tools/gopls/internal/util/safetoken"
3839
"golang.org/x/tools/gopls/internal/util/slices"
@@ -134,52 +135,77 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
134135
return protocol.Range{}, nil, err
135136
}
136137

137-
// Handle hovering over import paths, which do not have an associated
138-
// identifier.
139-
for _, spec := range pgf.File.Imports {
140-
// We are inclusive of the end point here to allow hovering when the cursor
141-
// is just after the import path.
142-
if spec.Path.Pos() <= pos && pos <= spec.Path.End() {
143-
return hoverImport(ctx, snapshot, pkg, pgf, spec)
144-
}
145-
}
146-
147138
// Handle hovering over the package name, which does not have an associated
148139
// object.
149140
// As with import paths, we allow hovering just after the package name.
150-
if pgf.File.Name != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.Pos() {
141+
if pgf.File.Name != nil && gastutil.NodeContains(pgf.File.Name, pos) {
151142
return hoverPackageName(pkg, pgf)
152143
}
153144

154-
// Handle hovering over (non-import-path) literals.
155-
if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 {
156-
if lit, _ := path[0].(*ast.BasicLit); lit != nil {
157-
return hoverLit(pgf, lit, pos)
158-
}
159-
}
160-
161145
// Handle hovering over embed directive argument.
162146
pattern, embedRng := parseEmbedDirective(pgf.Mapper, pp)
163147
if pattern != "" {
164148
return hoverEmbed(fh, embedRng, pattern)
165149
}
166150

151+
// hoverRange is the range reported to the client (e.g. for highlighting).
152+
// It may be an expansion around the selected identifier,
153+
// for instance when hovering over a linkname directive or doc link.
154+
var hoverRange *protocol.Range
167155
// Handle linkname directive by overriding what to look for.
168-
var linkedRange *protocol.Range // range referenced by linkname directive, or nil
169156
if pkgPath, name, offset := parseLinkname(pgf.Mapper, pp); pkgPath != "" && name != "" {
170157
// rng covering 2nd linkname argument: pkgPath.name.
171158
rng, err := pgf.PosRange(pgf.Tok.Pos(offset), pgf.Tok.Pos(offset+len(pkgPath)+len(".")+len(name)))
172159
if err != nil {
173160
return protocol.Range{}, nil, fmt.Errorf("range over linkname arg: %w", err)
174161
}
175-
linkedRange = &rng
162+
hoverRange = &rng
176163

177164
pkg, pgf, pos, err = findLinkname(ctx, snapshot, PackagePath(pkgPath), name)
178165
if err != nil {
179166
return protocol.Range{}, nil, fmt.Errorf("find linkname: %w", err)
180167
}
181168
}
182169

170+
// Handle hovering over a doc link
171+
if obj, rng, _ := parseDocLink(pkg, pgf, pos); obj != nil {
172+
hoverRange = &rng
173+
// Handle builtins, which don't have a package or position.
174+
if !obj.Pos().IsValid() {
175+
h, err := hoverBuiltin(ctx, snapshot, obj)
176+
return *hoverRange, h, err
177+
}
178+
objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos())
179+
pkg, pgf, err = NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(objURI.Filename))
180+
if err != nil {
181+
return protocol.Range{}, nil, err
182+
}
183+
pos = pgf.Tok.Pos(objURI.Offset)
184+
}
185+
186+
// Handle hovering over import paths, which do not have an associated
187+
// identifier.
188+
for _, spec := range pgf.File.Imports {
189+
if gastutil.NodeContains(spec, pos) {
190+
rng, hoverJSON, err := hoverImport(ctx, snapshot, pkg, pgf, spec)
191+
if err != nil {
192+
return protocol.Range{}, nil, err
193+
}
194+
if hoverRange == nil {
195+
hoverRange = &rng
196+
}
197+
return *hoverRange, hoverJSON, nil
198+
}
199+
}
200+
// Handle hovering over (non-import-path) literals.
201+
if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 {
202+
if lit, _ := path[0].(*ast.BasicLit); lit != nil {
203+
return hoverLit(pgf, lit, pos)
204+
}
205+
}
206+
207+
// Handle hover over identifier.
208+
183209
// The general case: compute hover information for the object referenced by
184210
// the identifier at pos.
185211
ident, obj, selectedType := referencedObject(pkg, pgf, pos)
@@ -188,14 +214,12 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
188214
}
189215

190216
// Unless otherwise specified, rng covers the ident being hovered.
191-
var rng protocol.Range
192-
if linkedRange != nil {
193-
rng = *linkedRange
194-
} else {
195-
rng, err = pgf.NodeRange(ident)
217+
if hoverRange == nil {
218+
rng, err := pgf.NodeRange(ident)
196219
if err != nil {
197220
return protocol.Range{}, nil, err
198221
}
222+
hoverRange = &rng
199223
}
200224

201225
// By convention, we qualify hover information relative to the package
@@ -209,7 +233,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
209233
if selectedType != nil {
210234
fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType)
211235
signature := types.ObjectString(fakeObj, qf)
212-
return rng, &hoverJSON{
236+
return *hoverRange, &hoverJSON{
213237
Signature: signature,
214238
SingleLine: signature,
215239
SymbolName: fakeObj.Name(),
@@ -219,7 +243,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
219243
// Handle builtins, which don't have a package or position.
220244
if !obj.Pos().IsValid() {
221245
h, err := hoverBuiltin(ctx, snapshot, obj)
222-
return rng, h, err
246+
return *hoverRange, h, err
223247
}
224248

225249
// For all other objects, consider the full syntax of their declaration in
@@ -523,7 +547,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
523547
linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
524548
}
525549

526-
return rng, &hoverJSON{
550+
return *hoverRange, &hoverJSON{
527551
Synopsis: doc.Synopsis(docText),
528552
FullDocumentation: docText,
529553
SingleLine: singleLineSignature,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
This test executes definition requests over doc links.
2+
3+
-- go.mod --
4+
module mod.com
5+
6+
go 1.21
7+
8+
-- a.go --
9+
package p
10+
11+
import "strconv" //@loc(strconv, `"strconv"`)
12+
13+
const NumberBase = 10 //@loc(NumberBase, "NumberBase")
14+
15+
// [Conv] converts s to an int. //@def("Conv", Conv)
16+
func Conv(s string) int { //@loc(Conv, "Conv")
17+
// [strconv.ParseInt] parses s and returns the integer corresponding to it. //@def("strconv", strconv)
18+
// [NumberBase] is the base to use for number parsing. //@def("NumberBase", NumberBase)
19+
i, _ := strconv.ParseInt(s, NumberBase, 64)
20+
return int(i)
21+
}

0 commit comments

Comments
 (0)