Skip to content

Commit abb57c6

Browse files
committed
internal/lsp: support textDocument/hover for .mod extension
This change implements support for textDocument/hover when it comes to go.mod files. Updates golang/go#36501 Change-Id: Ie7da0194bb972955b7ab9cf7b9c9972bd9f4b8a9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/220359 Run-TryBot: Rohan Challa <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 48cfad2 commit abb57c6

File tree

5 files changed

+247
-31
lines changed

5 files changed

+247
-31
lines changed

internal/lsp/cache/mod.go

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ type modData struct {
7878
// upgrades is a map of path->version that contains any upgrades for the go.mod.
7979
upgrades map[string]string
8080

81+
// why is a map of path->explanation that contains all the "go mod why" contents
82+
// for each require statement.
83+
why map[string]string
84+
8185
// parseErrors are the errors that arise when we diff between a user's go.mod
8286
// and the "tidied" go.mod.
8387
parseErrors []source.Error
@@ -112,6 +116,15 @@ func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.Col
112116
return data.origParsedFile, data.origMapper, data.upgrades, data.err
113117
}
114118

119+
func (mh *modHandle) Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
120+
v := mh.handle.Get(ctx)
121+
if v == nil {
122+
return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
123+
}
124+
data := v.(*modData)
125+
return data.origParsedFile, data.origMapper, data.why, data.err
126+
}
127+
115128
func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.ModHandle {
116129
uri := fh.Identity().URI
117130
if handle := s.getModHandle(uri); handle != nil {
@@ -165,7 +178,15 @@ func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.M
165178
}
166179
}
167180
// Only get dependency upgrades if the go.mod file is the same as the view's.
168-
data.upgrades, data.err = dependencyUpgrades(ctx, cfg, folder, data)
181+
if err := dependencyUpgrades(ctx, cfg, folder, data); err != nil {
182+
data.err = err
183+
return data
184+
}
185+
// Only run "go mod why" if the go.mod file is the same as the view's.
186+
if err := goModWhy(ctx, cfg, folder, data); err != nil {
187+
data.err = err
188+
return data
189+
}
169190
return data
170191
})
171192
s.mu.Lock()
@@ -178,9 +199,39 @@ func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.M
178199
return s.modHandles[uri]
179200
}
180201

181-
func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, data *modData) (map[string]string, error) {
202+
func goModWhy(ctx context.Context, cfg *packages.Config, folder string, data *modData) error {
203+
if len(data.origParsedFile.Require) == 0 {
204+
return nil
205+
}
206+
// Run "go mod why" on all the dependencies to get information about the usages.
207+
inv := gocommand.Invocation{
208+
Verb: "mod",
209+
Args: []string{"why", "-m"},
210+
BuildFlags: cfg.BuildFlags,
211+
Env: cfg.Env,
212+
WorkingDir: folder,
213+
}
214+
for _, req := range data.origParsedFile.Require {
215+
inv.Args = append(inv.Args, req.Mod.Path)
216+
}
217+
stdout, err := inv.Run(ctx)
218+
if err != nil {
219+
return err
220+
}
221+
whyList := strings.Split(stdout.String(), "\n\n")
222+
if len(whyList) <= 1 || len(whyList) > len(data.origParsedFile.Require) {
223+
return nil
224+
}
225+
data.why = make(map[string]string)
226+
for i, req := range data.origParsedFile.Require {
227+
data.why[req.Mod.Path] = whyList[i]
228+
}
229+
return nil
230+
}
231+
232+
func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, data *modData) error {
182233
if len(data.origParsedFile.Require) == 0 {
183-
return nil, nil
234+
return nil
184235
}
185236
// Run "go list -u -m all" to be able to see which deps can be upgraded.
186237
inv := gocommand.Invocation{
@@ -192,13 +243,13 @@ func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string
192243
}
193244
stdout, err := inv.Run(ctx)
194245
if err != nil {
195-
return nil, err
246+
return err
196247
}
197248
upgradesList := strings.Split(stdout.String(), "\n")
198249
if len(upgradesList) <= 1 {
199-
return nil, nil
250+
return nil
200251
}
201-
upgrades := make(map[string]string)
252+
data.upgrades = make(map[string]string)
202253
for _, upgrade := range upgradesList[1:] {
203254
// Example: "github.com/x/tools v1.1.0 [v1.2.0]"
204255
info := strings.Split(upgrade, " ")
@@ -208,9 +259,9 @@ func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string
208259
dep, version := info[0], info[2]
209260
latest := version[1:] // remove the "["
210261
latest = strings.TrimSuffix(latest, "]") // remove the "]"
211-
upgrades[dep] = latest
262+
data.upgrades[dep] = latest
212263
}
213-
return upgrades, nil
264+
return nil
214265
}
215266

216267
func (mh *modHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {

internal/lsp/hover.go

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,36 +7,21 @@ package lsp
77
import (
88
"context"
99

10+
"golang.org/x/tools/internal/lsp/mod"
1011
"golang.org/x/tools/internal/lsp/protocol"
1112
"golang.org/x/tools/internal/lsp/source"
1213
)
1314

1415
func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) {
15-
snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
16+
snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
1617
if !ok {
1718
return nil, err
1819
}
19-
ident, err := source.Identifier(ctx, snapshot, fh, params.Position)
20-
if err != nil {
21-
return nil, nil
20+
switch fh.Identity().Kind {
21+
case source.Mod:
22+
return mod.Hover(ctx, snapshot, fh, params.Position)
23+
case source.Go:
24+
return source.Hover(ctx, snapshot, fh, params.Position)
2225
}
23-
h, err := ident.Hover(ctx)
24-
if err != nil {
25-
return nil, err
26-
}
27-
rng, err := ident.Range()
28-
if err != nil {
29-
return nil, err
30-
}
31-
hover, err := source.FormatHover(h, snapshot.View().Options())
32-
if err != nil {
33-
return nil, err
34-
}
35-
return &protocol.Hover{
36-
Contents: protocol.MarkupContent{
37-
Kind: snapshot.View().Options().PreferredContentFormat,
38-
Value: hover,
39-
},
40-
Range: rng,
41-
}, nil
26+
return nil, nil
4227
}

internal/lsp/mod/hover.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package mod
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"go/token"
8+
"strings"
9+
10+
"golang.org/x/mod/modfile"
11+
"golang.org/x/tools/internal/lsp/protocol"
12+
"golang.org/x/tools/internal/lsp/source"
13+
"golang.org/x/tools/internal/span"
14+
"golang.org/x/tools/internal/telemetry/trace"
15+
)
16+
17+
func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
18+
realURI, _ := snapshot.View().ModFiles()
19+
// Only get hover information on the go.mod for the view.
20+
if realURI == "" || fh.Identity().URI != realURI {
21+
return nil, nil
22+
}
23+
ctx, done := trace.StartSpan(ctx, "mod.Hover")
24+
defer done()
25+
26+
file, m, why, err := snapshot.ModHandle(ctx, fh).Why(ctx)
27+
if err != nil {
28+
return nil, err
29+
}
30+
// Get the position of the cursor.
31+
spn, err := m.PointSpan(position)
32+
if err != nil {
33+
return nil, err
34+
}
35+
hoverRng, err := spn.Range(m.Converter)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
var req *modfile.Require
41+
var startPos, endPos int
42+
for _, r := range file.Require {
43+
dep := []byte(r.Mod.Path)
44+
s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte
45+
i := bytes.Index(m.Content[s:e], dep)
46+
if i == -1 {
47+
continue
48+
}
49+
// Shift the start position to the location of the
50+
// dependency within the require statement.
51+
startPos, endPos = s+i, s+i+len(dep)
52+
if token.Pos(startPos) <= hoverRng.Start && hoverRng.Start <= token.Pos(endPos) {
53+
req = r
54+
break
55+
}
56+
}
57+
if req == nil || why == nil {
58+
return nil, nil
59+
}
60+
explanation, ok := why[req.Mod.Path]
61+
if !ok {
62+
return nil, nil
63+
}
64+
// Get the range to highlight for the hover.
65+
line, col, err := m.Converter.ToPosition(startPos)
66+
if err != nil {
67+
return nil, err
68+
}
69+
start := span.NewPoint(line, col, startPos)
70+
71+
line, col, err = m.Converter.ToPosition(endPos)
72+
if err != nil {
73+
return nil, err
74+
}
75+
end := span.NewPoint(line, col, endPos)
76+
77+
spn = span.New(fh.Identity().URI, start, end)
78+
rng, err := m.Range(spn)
79+
if err != nil {
80+
return nil, err
81+
}
82+
options := snapshot.View().Options()
83+
explanation = formatExplanation(explanation, req, options)
84+
return &protocol.Hover{
85+
Contents: protocol.MarkupContent{
86+
Kind: options.PreferredContentFormat,
87+
Value: explanation,
88+
},
89+
Range: rng,
90+
}, nil
91+
}
92+
93+
func formatExplanation(text string, req *modfile.Require, options source.Options) string {
94+
text = strings.TrimSuffix(text, "\n")
95+
splt := strings.Split(text, "\n")
96+
length := len(splt)
97+
98+
var b strings.Builder
99+
// Write the heading as an H3.
100+
b.WriteString("##" + splt[0])
101+
if options.PreferredContentFormat == protocol.Markdown {
102+
b.WriteString("\n\n")
103+
} else {
104+
b.WriteRune('\n')
105+
}
106+
107+
// If the explanation is 2 lines, then it is of the form:
108+
// # golang.org/x/text/encoding
109+
// (main module does not need package golang.org/x/text/encoding)
110+
if length == 2 {
111+
b.WriteString(splt[1])
112+
return b.String()
113+
}
114+
115+
imp := splt[length-1]
116+
target := imp
117+
if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
118+
target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
119+
}
120+
target = fmt.Sprintf("https://%s/%s", options.LinkTarget, target)
121+
122+
b.WriteString("This module is necessary because ")
123+
msg := fmt.Sprintf("[%s](%s) is imported in", imp, target)
124+
b.WriteString(msg)
125+
126+
// If the explanation is 3 lines, then it is of the form:
127+
// # golang.org/x/tools
128+
// modtest
129+
// golang.org/x/tools/go/packages
130+
if length == 3 {
131+
msg := fmt.Sprintf(" `%s`.", splt[1])
132+
b.WriteString(msg)
133+
return b.String()
134+
}
135+
136+
// If the explanation is more than 3 lines, then it is of the form:
137+
// # golang.org/x/text/language
138+
// rsc.io/quote
139+
// rsc.io/sampler
140+
// golang.org/x/text/language
141+
b.WriteString(":\n```text")
142+
dash := ""
143+
for _, imp := range splt[1 : length-1] {
144+
dash += "-"
145+
b.WriteString("\n" + dash + " " + imp)
146+
}
147+
b.WriteString("\n```")
148+
return b.String()
149+
}

internal/lsp/source/hover.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ type HoverInformation struct {
4444
comment *ast.CommentGroup
4545
}
4646

47+
func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) {
48+
ident, err := Identifier(ctx, snapshot, fh, position)
49+
if err != nil {
50+
return nil, nil
51+
}
52+
h, err := ident.Hover(ctx)
53+
if err != nil {
54+
return nil, err
55+
}
56+
rng, err := ident.Range()
57+
if err != nil {
58+
return nil, err
59+
}
60+
hover, err := FormatHover(h, snapshot.View().Options())
61+
if err != nil {
62+
return nil, err
63+
}
64+
return &protocol.Hover{
65+
Contents: protocol.MarkupContent{
66+
Kind: snapshot.View().Options().PreferredContentFormat,
67+
Value: hover,
68+
},
69+
Range: rng,
70+
}, nil
71+
}
72+
4773
func (i *IdentifierInfo) Hover(ctx context.Context) (*HoverInformation, error) {
4874
ctx, done := trace.StartSpan(ctx, "source.Hover")
4975
defer done()

internal/lsp/source/view.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,11 @@ type ModHandle interface {
271271
// for the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
272272
// If the file is not available, returns nil and an error.
273273
Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
274+
275+
// Why returns the parsed modfile, a mapper, and any explanations why a dependency should be
276+
// in the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
277+
// If the file is not available, returns nil and an error.
278+
Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
274279
}
275280

276281
// ModTidyHandle represents a handle to the modfile for the view.

0 commit comments

Comments
 (0)