Skip to content

Commit 48cfad2

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

File tree

9 files changed

+243
-129
lines changed

9 files changed

+243
-129
lines changed

internal/lsp/cache/mod.go

Lines changed: 105 additions & 100 deletions
Large diffs are not rendered by default.

internal/lsp/cache/session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
125125
actions: make(map[actionKey]*actionHandle),
126126
workspacePackages: make(map[packageID]packagePath),
127127
unloadableFiles: make(map[span.URI]struct{}),
128+
modHandles: make(map[span.URI]*modHandle),
128129
},
129130
ignoredURIs: make(map[span.URI]struct{}),
130131
}

internal/lsp/cache/snapshot.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ type snapshot struct {
5757

5858
// unloadableFiles keeps track of files that we've failed to load.
5959
unloadableFiles map[span.URI]struct{}
60+
61+
// modHandles keeps track of any ParseModHandles for this snapshot.
62+
modHandles map[span.URI]*modHandle
6063
}
6164

6265
type packageKey struct {
@@ -218,6 +221,12 @@ func (s *snapshot) transitiveReverseDependencies(id packageID, ids map[packageID
218221
}
219222
}
220223

224+
func (s *snapshot) getModHandle(uri span.URI) *modHandle {
225+
s.mu.Lock()
226+
defer s.mu.Unlock()
227+
return s.modHandles[uri]
228+
}
229+
221230
func (s *snapshot) getImportedBy(id packageID) []packageID {
222231
s.mu.Lock()
223232
defer s.mu.Unlock()
@@ -612,6 +621,7 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Fi
612621
files: make(map[span.URI]source.FileHandle),
613622
workspacePackages: make(map[packageID]packagePath),
614623
unloadableFiles: make(map[span.URI]struct{}),
624+
modHandles: make(map[span.URI]*modHandle),
615625
}
616626

617627
// Copy all of the FileHandles.
@@ -622,6 +632,10 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Fi
622632
for k, v := range s.unloadableFiles {
623633
result.unloadableFiles[k] = v
624634
}
635+
// Copy all of the modHandles.
636+
for k, v := range s.modHandles {
637+
result.modHandles[k] = v
638+
}
625639

626640
// transitiveIDs keeps track of transitive reverse dependencies.
627641
// If an ID is present in the map, invalidate its types.
@@ -649,6 +663,7 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Fi
649663
for id := range s.workspacePackages {
650664
directIDs[id] = struct{}{}
651665
}
666+
delete(result.modHandles, withoutURI)
652667
}
653668

654669
// If this is a file we don't yet know about,

internal/lsp/link.go

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package lsp
66

77
import (
8+
"bytes"
89
"context"
910
"fmt"
1011
"go/ast"
@@ -23,11 +24,68 @@ import (
2324

2425
func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
2526
// TODO(golang/go#36501): Support document links for go.mod files.
26-
snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
27+
snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
2728
if !ok {
2829
return nil, err
2930
}
31+
switch fh.Identity().Kind {
32+
case source.Mod:
33+
return modLinks(ctx, snapshot, fh)
34+
case source.Go:
35+
return goLinks(ctx, snapshot.View(), fh)
36+
}
37+
return nil, nil
38+
}
39+
40+
func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
3041
view := snapshot.View()
42+
43+
file, m, err := snapshot.ModHandle(ctx, fh).Parse(ctx)
44+
if err != nil {
45+
return nil, err
46+
}
47+
var links []protocol.DocumentLink
48+
for _, req := range file.Require {
49+
dep := []byte(req.Mod.Path)
50+
s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
51+
i := bytes.Index(m.Content[s:e], dep)
52+
if i == -1 {
53+
continue
54+
}
55+
// Shift the start position to the location of the
56+
// dependency within the require statement.
57+
start, end := token.Pos(s+i), token.Pos(s+i+len(dep))
58+
target := fmt.Sprintf("https://%s/mod/%s", view.Options().LinkTarget, req.Mod.String())
59+
if l, err := toProtocolLink(view, m, target, start, end, source.Mod); err == nil {
60+
links = append(links, l)
61+
} else {
62+
log.Error(ctx, "failed to create protocol link", err)
63+
}
64+
}
65+
// TODO(ridersofrohan): handle links for replace and exclude directives
66+
if syntax := file.Syntax; syntax == nil {
67+
return links, nil
68+
}
69+
// Get all the links that are contained in the comments of the file.
70+
for _, expr := range file.Syntax.Stmt {
71+
comments := expr.Comment()
72+
if comments == nil {
73+
continue
74+
}
75+
for _, cmt := range comments.Before {
76+
links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
77+
}
78+
for _, cmt := range comments.Suffix {
79+
links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
80+
}
81+
for _, cmt := range comments.After {
82+
links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
83+
}
84+
}
85+
return links, nil
86+
}
87+
88+
func goLinks(ctx context.Context, view source.View, fh source.FileHandle) ([]protocol.DocumentLink, error) {
3189
phs, err := view.Snapshot().PackageHandles(ctx, fh)
3290
if err != nil {
3391
return nil, err
@@ -52,7 +110,7 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
52110
target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
53111
// Account for the quotation marks in the positions.
54112
start, end := n.Path.Pos()+1, n.Path.End()-1
55-
if l, err := toProtocolLink(view, m, target, start, end); err == nil {
113+
if l, err := toProtocolLink(view, m, target, start, end, source.Go); err == nil {
56114
links = append(links, l)
57115
} else {
58116
log.Error(ctx, "failed to create protocol link", err)
@@ -62,7 +120,7 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
62120
case *ast.BasicLit:
63121
// Look for links in string literals.
64122
if n.Kind == token.STRING {
65-
links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m)...)
123+
links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m, source.Go)...)
66124
}
67125
return false
68126
}
@@ -71,7 +129,7 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
71129
// Look for links in comments.
72130
for _, commentGroup := range file.Comments {
73131
for _, comment := range commentGroup.List {
74-
links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m)...)
132+
links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m, source.Go)...)
75133
}
76134
}
77135
return links, nil
@@ -96,7 +154,7 @@ func moduleAtVersion(ctx context.Context, target string, ph source.PackageHandle
96154
return modpath, version, true
97155
}
98156

99-
func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) []protocol.DocumentLink {
157+
func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) []protocol.DocumentLink {
100158
var links []protocol.DocumentLink
101159
for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) {
102160
start, end := index[0], index[1]
@@ -111,7 +169,7 @@ func findLinksInString(ctx context.Context, view source.View, src string, pos to
111169
if url.Scheme == "" {
112170
url.Scheme = "https"
113171
}
114-
l, err := toProtocolLink(view, m, url.String(), startPos, endPos)
172+
l, err := toProtocolLink(view, m, url.String(), startPos, endPos, fileKind)
115173
if err != nil {
116174
log.Error(ctx, "failed to create protocol link", err)
117175
continue
@@ -130,7 +188,7 @@ func findLinksInString(ctx context.Context, view source.View, src string, pos to
130188
}
131189
org, repo, number := matches[1], matches[2], matches[3]
132190
target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
133-
l, err := toProtocolLink(view, m, target, startPos, endPos)
191+
l, err := toProtocolLink(view, m, target, startPos, endPos, fileKind)
134192
if err != nil {
135193
log.Error(ctx, "failed to create protocol link", err)
136194
continue
@@ -152,14 +210,34 @@ var (
152210
issueRegexp *regexp.Regexp
153211
)
154212

155-
func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos) (protocol.DocumentLink, error) {
156-
spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
157-
if err != nil {
158-
return protocol.DocumentLink{}, err
159-
}
160-
rng, err := m.Range(spn)
161-
if err != nil {
162-
return protocol.DocumentLink{}, err
213+
func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) {
214+
var rng protocol.Range
215+
switch fileKind {
216+
case source.Go:
217+
spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
218+
if err != nil {
219+
return protocol.DocumentLink{}, err
220+
}
221+
rng, err = m.Range(spn)
222+
if err != nil {
223+
return protocol.DocumentLink{}, err
224+
}
225+
case source.Mod:
226+
s, e := int(start), int(end)
227+
line, col, err := m.Converter.ToPosition(s)
228+
if err != nil {
229+
return protocol.DocumentLink{}, err
230+
}
231+
start := span.NewPoint(line, col, s)
232+
line, col, err = m.Converter.ToPosition(e)
233+
if err != nil {
234+
return protocol.DocumentLink{}, err
235+
}
236+
end := span.NewPoint(line, col, e)
237+
rng, err = m.Range(span.New(m.URI, start, end))
238+
if err != nil {
239+
return protocol.DocumentLink{}, err
240+
}
163241
}
164242
return protocol.DocumentLink{
165243
Range: rng,

internal/lsp/mod/code_lens.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313

1414
func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]protocol.CodeLens, error) {
1515
realURI, _ := snapshot.View().ModFiles()
16-
// Check the case when the tempModfile flag is turned off.
1716
if realURI == "" {
1817
return nil, nil
1918
}
@@ -24,11 +23,11 @@ func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]pr
2423
ctx, done := trace.StartSpan(ctx, "mod.CodeLens", telemetry.File.Of(realURI))
2524
defer done()
2625

27-
pmh, err := snapshot.ParseModHandle(ctx)
26+
fh, err := snapshot.GetFile(realURI)
2827
if err != nil {
2928
return nil, err
3029
}
31-
f, m, upgrades, err := pmh.Upgrades(ctx)
30+
f, m, upgrades, err := snapshot.ModHandle(ctx, fh).Upgrades(ctx)
3231
if err != nil {
3332
return nil, err
3433
}

internal/lsp/source/view.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ type Snapshot interface {
5151
// This function can have no data or error if there is no modfile detected.
5252
ModTidyHandle(ctx context.Context, fh FileHandle) (ModTidyHandle, error)
5353

54-
// ParseModHandle returns a ParseModHandle for the view's go.mod file handle.
55-
// This function can have no data or error if there is no modfile detected.
56-
ParseModHandle(ctx context.Context) (ParseModHandle, error)
54+
// ModHandle returns a ModHandle for the passed in go.mod file handle.
55+
// This function can have no data if there is no modfile detected.
56+
ModHandle(ctx context.Context, fh FileHandle) ModHandle
5757

5858
// PackageHandles returns the PackageHandles for the packages that this file
5959
// belongs to.
@@ -258,17 +258,23 @@ type ParseGoHandle interface {
258258
Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
259259
}
260260

261-
// ParseModHandle represents a handle to the modfile for a go.mod.
262-
type ParseModHandle interface {
261+
// ModHandle represents a handle to the modfile for a go.mod.
262+
type ModHandle interface {
263263
// File returns a file handle for which to get the modfile.
264264
File() FileHandle
265265

266+
// Parse returns the parsed modfile and a mapper for the go.mod file.
267+
// If the file is not available, returns nil and an error.
268+
Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error)
269+
266270
// Upgrades returns the parsed modfile, a mapper, and any dependency upgrades
267-
// for the go.mod file. If the file is not available, returns nil and an error.
271+
// for the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
272+
// If the file is not available, returns nil and an error.
268273
Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
269274
}
270275

271-
// ModTidyHandle represents a handle to the modfile for a go.mod.
276+
// ModTidyHandle represents a handle to the modfile for the view.
277+
// Specifically for the purpose of getting diagnostics by running "go mod tidy".
272278
type ModTidyHandle interface {
273279
// File returns a file handle for which to get the modfile.
274280
File() FileHandle
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
module upgradedep
22

3+
// TODO(microsoft/vscode-go#12): Another issue. //@link(`microsoft/vscode-go#12`, `https://github.com/microsoft/vscode-go/issues/12`)
4+
35
go 1.12
46

5-
require example.com/extramodule v1.0.0 //@codelens("require example.com/extramodule v1.0.0", "Upgrade dependency to v1.1.0", "upgrade.dependency")
7+
// TODO(golang/go#1234): Link the relevant issue. //@link(`golang/go#1234`, `https://github.com/golang/go/issues/1234`)
8+
9+
require example.com/extramodule v1.0.0 //@link(`example.com/extramodule`, `https://pkg.go.dev/mod/example.com/[email protected]`),codelens("require example.com/extramodule v1.0.0", "Upgrade dependency to v1.1.0", "upgrade.dependency")
10+
11+
// https://example.com/comment: Another issue. //@link(`https://example.com/comment`,`https://example.com/comment`)

internal/lsp/testdata/upgradedep/summary.txt.golden

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,6 @@ WorkspaceSymbolsCount = 0
2323
FuzzyWorkspaceSymbolsCount = 0
2424
CaseSensitiveWorkspaceSymbolsCount = 0
2525
SignaturesCount = 0
26-
LinksCount = 0
26+
LinksCount = 4
2727
ImplementationsCount = 0
2828

internal/lsp/tests/tests.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,12 @@ func Run(t *testing.T, tests Tests, data *Data) {
702702
t.Helper()
703703
for uri, wantLinks := range data.Links {
704704
// If we are testing GOPATH, then we do not want links with
705-
// the versions attached (pkg.go.dev/repoa/[email protected]/pkg).
705+
// the versions attached (pkg.go.dev/repoa/[email protected]/pkg),
706+
// unless the file is a go.mod, then we can skip it alltogether.
706707
if data.Exported.Exporter == packagestest.GOPATH {
708+
if strings.HasSuffix(uri.Filename(), ".mod") {
709+
continue
710+
}
707711
re := regexp.MustCompile(`@v\d+\.\d+\.[\w-]+`)
708712
for i, link := range wantLinks {
709713
wantLinks[i].Target = re.ReplaceAllString(link.Target, "")
@@ -1223,7 +1227,7 @@ func shouldSkip(data *Data, uri span.URI) bool {
12231227
}
12241228
// If the -modfile flag is not available, then we do not want to run
12251229
// any tests on the go.mod file.
1226-
if strings.Contains(uri.Filename(), ".mod") {
1230+
if strings.HasSuffix(uri.Filename(), ".mod") {
12271231
return true
12281232
}
12291233
// If the -modfile flag is not available, then we do not want to test any

0 commit comments

Comments
 (0)