1
0
mirror of https://github.com/golang/go synced 2024-11-18 13:04:46 -07:00

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 <rohan@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Rohan Challa 2020-02-18 15:47:38 -05:00
parent 807dcd8834
commit 48cfad2f5e
9 changed files with 243 additions and 129 deletions

View File

@ -31,32 +31,26 @@ const (
SyntaxError = "syntax" SyntaxError = "syntax"
) )
type parseModKey struct { type modKey struct {
view string
gomod string
cfg string cfg string
} gomod string
view string
type parseModHandle struct {
handle *memoize.Handle
file source.FileHandle
cfg *packages.Config
} }
type modTidyKey struct { type modTidyKey struct {
view string
imports string
gomod string
cfg string cfg string
gomod string
imports string
view string
} }
type modTidyHandle struct { type modHandle struct {
handle *memoize.Handle handle *memoize.Handle
file source.FileHandle file source.FileHandle
cfg *packages.Config cfg *packages.Config
} }
type modTidyData struct { type modData struct {
memoize.NoCopy memoize.NoCopy
// origfh is the file handle for the original go.mod file. // origfh is the file handle for the original go.mod file.
@ -92,53 +86,77 @@ type modTidyData struct {
err error err error
} }
func (pmh *parseModHandle) String() string { func (mh *modHandle) String() string {
return pmh.File().Identity().URI.Filename() return mh.File().Identity().URI.Filename()
} }
func (pmh *parseModHandle) File() source.FileHandle { func (mh *modHandle) File() source.FileHandle {
return pmh.file return mh.file
} }
func (pmh *parseModHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) { func (mh *modHandle) Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error) {
v := pmh.handle.Get(ctx) v := mh.handle.Get(ctx)
if v == nil { if v == nil {
return nil, nil, nil, errors.Errorf("no parsed file for %s", pmh.File().Identity().URI) return nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
} }
data := v.(*modTidyData) data := v.(*modData)
return data.origParsedFile, data.origMapper, data.err
}
func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) {
v := mh.handle.Get(ctx)
if v == nil {
return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
}
data := v.(*modData)
return data.origParsedFile, data.origMapper, data.upgrades, data.err return data.origParsedFile, data.origMapper, data.upgrades, data.err
} }
func (s *snapshot) ParseModHandle(ctx context.Context) (source.ParseModHandle, error) { func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.ModHandle {
cfg := s.Config(ctx) uri := fh.Identity().URI
folder := s.View().Folder().Filename() if handle := s.getModHandle(uri); handle != nil {
return handle
}
realURI, tempURI := s.view.ModFiles() realURI, tempURI := s.view.ModFiles()
fh, err := s.GetFile(realURI) folder := s.View().Folder().Filename()
if err != nil { cfg := s.Config(ctx)
return nil, err
}
key := parseModKey{ key := modKey{
view: folder,
gomod: fh.Identity().String(),
cfg: hashConfig(cfg), cfg: hashConfig(cfg),
gomod: fh.Identity().String(),
view: folder,
} }
h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} { h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
ctx, done := trace.StartSpan(ctx, "cache.ParseModHandle", telemetry.File.Of(realURI)) ctx, done := trace.StartSpan(ctx, "cache.ModHandle", telemetry.File.Of(uri))
defer done() defer done()
data := &modTidyData{}
contents, _, err := fh.Read(ctx) contents, _, err := fh.Read(ctx)
if err != nil { if err != nil {
data.err = err return &modData{
return data err: err,
}
} }
parsedFile, err := modfile.Parse(realURI.Filename(), contents, nil) parsedFile, err := modfile.Parse(uri.Filename(), contents, nil)
if err != nil { if err != nil {
data.err = err return &modData{
err: err,
}
}
data := &modData{
origfh: fh,
origParsedFile: parsedFile,
origMapper: &protocol.ColumnMapper{
URI: uri,
Converter: span.NewContentConverter(uri.Filename(), contents),
Content: contents,
},
}
// If the go.mod file is not the view's go.mod file, then we just want to parse.
if uri != realURI {
return data return data
} }
// If we have a tempModfile, copy the real go.mod file content into the temp go.mod file. // If we have a tempModfile, copy the real go.mod file content into the temp go.mod file.
if tempURI != "" { if tempURI != "" {
if err := ioutil.WriteFile(tempURI.Filename(), contents, os.ModePerm); err != nil { if err := ioutil.WriteFile(tempURI.Filename(), contents, os.ModePerm); err != nil {
@ -146,27 +164,22 @@ func (s *snapshot) ParseModHandle(ctx context.Context) (source.ParseModHandle, e
return data return data
} }
} }
data = &modTidyData{ // Only get dependency upgrades if the go.mod file is the same as the view's.
origfh: fh,
origParsedFile: parsedFile,
origMapper: &protocol.ColumnMapper{
URI: realURI,
Converter: span.NewContentConverter(realURI.Filename(), contents),
Content: contents,
},
}
data.upgrades, data.err = dependencyUpgrades(ctx, cfg, folder, data) data.upgrades, data.err = dependencyUpgrades(ctx, cfg, folder, data)
return data return data
}) })
return &parseModHandle{ s.mu.Lock()
defer s.mu.Unlock()
s.modHandles[uri] = &modHandle{
handle: h, handle: h,
file: fh, file: fh,
cfg: cfg, cfg: cfg,
}, nil }
return s.modHandles[uri]
} }
func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, modData *modTidyData) (map[string]string, error) { func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, data *modData) (map[string]string, error) {
if len(modData.origParsedFile.Require) == 0 { if len(data.origParsedFile.Require) == 0 {
return nil, nil return nil, nil
} }
// Run "go list -u -m all" to be able to see which deps can be upgraded. // Run "go list -u -m all" to be able to see which deps can be upgraded.
@ -200,20 +213,12 @@ func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string
return upgrades, nil return upgrades, nil
} }
func (mth *modTidyHandle) String() string { func (mh *modHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {
return mth.File().Identity().URI.Filename() v := mh.handle.Get(ctx)
}
func (mth *modTidyHandle) File() source.FileHandle {
return mth.file
}
func (mth *modTidyHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) {
v := mth.handle.Get(ctx)
if v == nil { if v == nil {
return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", mth.File().Identity().URI) return nil, nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI)
} }
data := v.(*modTidyData) data := v.(*modData)
return data.origParsedFile, data.origMapper, data.missingDeps, data.parseErrors, data.err return data.origParsedFile, data.origMapper, data.missingDeps, data.parseErrors, data.err
} }
@ -241,7 +246,7 @@ func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle)
cfg: hashConfig(cfg), cfg: hashConfig(cfg),
} }
h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} { h := s.view.session.cache.store.Bind(key, func(ctx context.Context) interface{} {
data := &modTidyData{} data := &modData{}
// Check the case when the tempModfile flag is turned off. // Check the case when the tempModfile flag is turned off.
if realURI == "" || tempURI == "" { if realURI == "" || tempURI == "" {
@ -306,7 +311,7 @@ func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle)
return data return data
} }
data = &modTidyData{ data = &modData{
origfh: realfh, origfh: realfh,
origParsedFile: origParsedFile, origParsedFile: origParsedFile,
origMapper: realMapper, origMapper: realMapper,
@ -335,7 +340,7 @@ func (s *snapshot) ModTidyHandle(ctx context.Context, realfh source.FileHandle)
} }
return data return data
}) })
return &modTidyHandle{ return &modHandle{
handle: h, handle: h,
file: realfh, file: realfh,
cfg: cfg, cfg: cfg,
@ -385,27 +390,27 @@ func extractModParseErrors(ctx context.Context, uri span.URI, m *protocol.Column
// modRequireErrors extracts the errors that occur on the require directives. // modRequireErrors extracts the errors that occur on the require directives.
// It checks for directness issues and unused dependencies. // It checks for directness issues and unused dependencies.
func modRequireErrors(ctx context.Context, options source.Options, modData *modTidyData) ([]source.Error, error) { func modRequireErrors(ctx context.Context, options source.Options, data *modData) ([]source.Error, error) {
var errors []source.Error var errors []source.Error
for dep, req := range modData.unusedDeps { for dep, req := range data.unusedDeps {
if req.Syntax == nil { if req.Syntax == nil {
continue continue
} }
// Handle dependencies that are incorrectly labeled indirect and vice versa. // Handle dependencies that are incorrectly labeled indirect and vice versa.
if modData.missingDeps[dep] != nil && req.Indirect != modData.missingDeps[dep].Indirect { if data.missingDeps[dep] != nil && req.Indirect != data.missingDeps[dep].Indirect {
directErr, err := modDirectnessErrors(ctx, options, modData, req) directErr, err := modDirectnessErrors(ctx, options, data, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
errors = append(errors, directErr) errors = append(errors, directErr)
} }
// Handle unused dependencies. // Handle unused dependencies.
if modData.missingDeps[dep] == nil { if data.missingDeps[dep] == nil {
rng, err := rangeFromPositions(modData.origfh.Identity().URI, modData.origMapper, req.Syntax.Start, req.Syntax.End) rng, err := rangeFromPositions(data.origfh.Identity().URI, data.origMapper, req.Syntax.Start, req.Syntax.End)
if err != nil { if err != nil {
return nil, err return nil, err
} }
edits, err := dropDependencyEdits(ctx, options, modData, req) edits, err := dropDependencyEdits(ctx, options, data, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -413,10 +418,10 @@ func modRequireErrors(ctx context.Context, options source.Options, modData *modT
Category: ModTidyError, Category: ModTidyError,
Message: fmt.Sprintf("%s is not used in this module.", dep), Message: fmt.Sprintf("%s is not used in this module.", dep),
Range: rng, Range: rng,
URI: modData.origfh.Identity().URI, URI: data.origfh.Identity().URI,
SuggestedFixes: []source.SuggestedFix{{ SuggestedFixes: []source.SuggestedFix{{
Title: fmt.Sprintf("Remove dependency: %s", dep), Title: fmt.Sprintf("Remove dependency: %s", dep),
Edits: map[span.URI][]protocol.TextEdit{modData.origfh.Identity().URI: edits}, Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
}}, }},
}) })
} }
@ -425,8 +430,8 @@ func modRequireErrors(ctx context.Context, options source.Options, modData *modT
} }
// modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa. // modDirectnessErrors extracts errors when a dependency is labeled indirect when it should be direct and vice versa.
func modDirectnessErrors(ctx context.Context, options source.Options, modData *modTidyData, req *modfile.Require) (source.Error, error) { func modDirectnessErrors(ctx context.Context, options source.Options, data *modData, req *modfile.Require) (source.Error, error) {
rng, err := rangeFromPositions(modData.origfh.Identity().URI, modData.origMapper, req.Syntax.Start, req.Syntax.End) rng, err := rangeFromPositions(data.origfh.Identity().URI, data.origMapper, req.Syntax.Start, req.Syntax.End)
if err != nil { if err != nil {
return source.Error{}, err return source.Error{}, err
} }
@ -436,12 +441,12 @@ func modDirectnessErrors(ctx context.Context, options source.Options, modData *m
end := comments.Suffix[0].Start end := comments.Suffix[0].Start
end.LineRune += len(comments.Suffix[0].Token) end.LineRune += len(comments.Suffix[0].Token)
end.Byte += len([]byte(comments.Suffix[0].Token)) end.Byte += len([]byte(comments.Suffix[0].Token))
rng, err = rangeFromPositions(modData.origfh.Identity().URI, modData.origMapper, comments.Suffix[0].Start, end) rng, err = rangeFromPositions(data.origfh.Identity().URI, data.origMapper, comments.Suffix[0].Start, end)
if err != nil { if err != nil {
return source.Error{}, err return source.Error{}, err
} }
} }
edits, err := changeDirectnessEdits(ctx, options, modData, req, false) edits, err := changeDirectnessEdits(ctx, options, data, req, false)
if err != nil { if err != nil {
return source.Error{}, err return source.Error{}, err
} }
@ -449,15 +454,15 @@ func modDirectnessErrors(ctx context.Context, options source.Options, modData *m
Category: ModTidyError, Category: ModTidyError,
Message: fmt.Sprintf("%s should be a direct dependency.", req.Mod.Path), Message: fmt.Sprintf("%s should be a direct dependency.", req.Mod.Path),
Range: rng, Range: rng,
URI: modData.origfh.Identity().URI, URI: data.origfh.Identity().URI,
SuggestedFixes: []source.SuggestedFix{{ SuggestedFixes: []source.SuggestedFix{{
Title: fmt.Sprintf("Make %s direct", req.Mod.Path), Title: fmt.Sprintf("Make %s direct", req.Mod.Path),
Edits: map[span.URI][]protocol.TextEdit{modData.origfh.Identity().URI: edits}, Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
}}, }},
}, nil }, nil
} }
// If the dependency should be indirect, add the // indirect. // If the dependency should be indirect, add the // indirect.
edits, err := changeDirectnessEdits(ctx, options, modData, req, true) edits, err := changeDirectnessEdits(ctx, options, data, req, true)
if err != nil { if err != nil {
return source.Error{}, err return source.Error{}, err
} }
@ -465,10 +470,10 @@ func modDirectnessErrors(ctx context.Context, options source.Options, modData *m
Category: ModTidyError, Category: ModTidyError,
Message: fmt.Sprintf("%s should be an indirect dependency.", req.Mod.Path), Message: fmt.Sprintf("%s should be an indirect dependency.", req.Mod.Path),
Range: rng, Range: rng,
URI: modData.origfh.Identity().URI, URI: data.origfh.Identity().URI,
SuggestedFixes: []source.SuggestedFix{{ SuggestedFixes: []source.SuggestedFix{{
Title: fmt.Sprintf("Make %s indirect", req.Mod.Path), Title: fmt.Sprintf("Make %s indirect", req.Mod.Path),
Edits: map[span.URI][]protocol.TextEdit{modData.origfh.Identity().URI: edits}, Edits: map[span.URI][]protocol.TextEdit{data.origfh.Identity().URI: edits},
}}, }},
}, nil }, nil
} }
@ -485,20 +490,20 @@ func modDirectnessErrors(ctx context.Context, options source.Options, modData *m
// module t // module t
// //
// go 1.11 // go 1.11
func dropDependencyEdits(ctx context.Context, options source.Options, modData *modTidyData, req *modfile.Require) ([]protocol.TextEdit, error) { func dropDependencyEdits(ctx context.Context, options source.Options, data *modData, req *modfile.Require) ([]protocol.TextEdit, error) {
if err := modData.origParsedFile.DropRequire(req.Mod.Path); err != nil { if err := data.origParsedFile.DropRequire(req.Mod.Path); err != nil {
return nil, err return nil, err
} }
modData.origParsedFile.Cleanup() data.origParsedFile.Cleanup()
newContents, err := modData.origParsedFile.Format() newContents, err := data.origParsedFile.Format()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Reset the *modfile.File back to before we dropped the dependency. // Reset the *modfile.File back to before we dropped the dependency.
modData.origParsedFile.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect) data.origParsedFile.AddNewRequire(req.Mod.Path, req.Mod.Version, req.Indirect)
// Calculate the edits to be made due to the change. // Calculate the edits to be made due to the change.
diff := options.ComputeEdits(modData.origfh.Identity().URI, string(modData.origMapper.Content), string(newContents)) diff := options.ComputeEdits(data.origfh.Identity().URI, string(data.origMapper.Content), string(newContents))
edits, err := source.ToProtocolEdits(modData.origMapper, diff) edits, err := source.ToProtocolEdits(data.origMapper, diff)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -519,34 +524,34 @@ func dropDependencyEdits(ctx context.Context, options source.Options, modData *m
// go 1.11 // go 1.11
// //
// require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee // indirect // require golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee // indirect
func changeDirectnessEdits(ctx context.Context, options source.Options, modData *modTidyData, req *modfile.Require, indirect bool) ([]protocol.TextEdit, error) { func changeDirectnessEdits(ctx context.Context, options source.Options, data *modData, req *modfile.Require, indirect bool) ([]protocol.TextEdit, error) {
var newReq []*modfile.Require var newReq []*modfile.Require
prevIndirect := false prevIndirect := false
// Change the directness in the matching require statement. // Change the directness in the matching require statement.
for _, r := range modData.origParsedFile.Require { for _, r := range data.origParsedFile.Require {
if req.Mod.Path == r.Mod.Path { if req.Mod.Path == r.Mod.Path {
prevIndirect = req.Indirect prevIndirect = req.Indirect
req.Indirect = indirect req.Indirect = indirect
} }
newReq = append(newReq, r) newReq = append(newReq, r)
} }
modData.origParsedFile.SetRequire(newReq) data.origParsedFile.SetRequire(newReq)
modData.origParsedFile.Cleanup() data.origParsedFile.Cleanup()
newContents, err := modData.origParsedFile.Format() newContents, err := data.origParsedFile.Format()
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Change the dependency back to the way it was before we got the newContents. // Change the dependency back to the way it was before we got the newContents.
for _, r := range modData.origParsedFile.Require { for _, r := range data.origParsedFile.Require {
if req.Mod.Path == r.Mod.Path { if req.Mod.Path == r.Mod.Path {
req.Indirect = prevIndirect req.Indirect = prevIndirect
} }
newReq = append(newReq, r) newReq = append(newReq, r)
} }
modData.origParsedFile.SetRequire(newReq) data.origParsedFile.SetRequire(newReq)
// Calculate the edits to be made due to the change. // Calculate the edits to be made due to the change.
diff := options.ComputeEdits(modData.origfh.Identity().URI, string(modData.origMapper.Content), string(newContents)) diff := options.ComputeEdits(data.origfh.Identity().URI, string(data.origMapper.Content), string(newContents))
edits, err := source.ToProtocolEdits(modData.origMapper, diff) edits, err := source.ToProtocolEdits(data.origMapper, diff)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -125,6 +125,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
actions: make(map[actionKey]*actionHandle), actions: make(map[actionKey]*actionHandle),
workspacePackages: make(map[packageID]packagePath), workspacePackages: make(map[packageID]packagePath),
unloadableFiles: make(map[span.URI]struct{}), unloadableFiles: make(map[span.URI]struct{}),
modHandles: make(map[span.URI]*modHandle),
}, },
ignoredURIs: make(map[span.URI]struct{}), ignoredURIs: make(map[span.URI]struct{}),
} }

View File

@ -57,6 +57,9 @@ type snapshot struct {
// unloadableFiles keeps track of files that we've failed to load. // unloadableFiles keeps track of files that we've failed to load.
unloadableFiles map[span.URI]struct{} unloadableFiles map[span.URI]struct{}
// modHandles keeps track of any ParseModHandles for this snapshot.
modHandles map[span.URI]*modHandle
} }
type packageKey struct { type packageKey struct {
@ -218,6 +221,12 @@ func (s *snapshot) transitiveReverseDependencies(id packageID, ids map[packageID
} }
} }
func (s *snapshot) getModHandle(uri span.URI) *modHandle {
s.mu.Lock()
defer s.mu.Unlock()
return s.modHandles[uri]
}
func (s *snapshot) getImportedBy(id packageID) []packageID { func (s *snapshot) getImportedBy(id packageID) []packageID {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -612,6 +621,7 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Fi
files: make(map[span.URI]source.FileHandle), files: make(map[span.URI]source.FileHandle),
workspacePackages: make(map[packageID]packagePath), workspacePackages: make(map[packageID]packagePath),
unloadableFiles: make(map[span.URI]struct{}), unloadableFiles: make(map[span.URI]struct{}),
modHandles: make(map[span.URI]*modHandle),
} }
// Copy all of the FileHandles. // Copy all of the FileHandles.
@ -622,6 +632,10 @@ func (s *snapshot) clone(ctx context.Context, withoutURIs map[span.URI]source.Fi
for k, v := range s.unloadableFiles { for k, v := range s.unloadableFiles {
result.unloadableFiles[k] = v result.unloadableFiles[k] = v
} }
// Copy all of the modHandles.
for k, v := range s.modHandles {
result.modHandles[k] = v
}
// transitiveIDs keeps track of transitive reverse dependencies. // transitiveIDs keeps track of transitive reverse dependencies.
// If an ID is present in the map, invalidate its types. // 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
for id := range s.workspacePackages { for id := range s.workspacePackages {
directIDs[id] = struct{}{} directIDs[id] = struct{}{}
} }
delete(result.modHandles, withoutURI)
} }
// If this is a file we don't yet know about, // If this is a file we don't yet know about,

View File

@ -5,6 +5,7 @@
package lsp package lsp
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"go/ast" "go/ast"
@ -23,11 +24,68 @@ import (
func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) { func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
// TODO(golang/go#36501): Support document links for go.mod files. // TODO(golang/go#36501): Support document links for go.mod files.
snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go) snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind)
if !ok { if !ok {
return nil, err return nil, err
} }
switch fh.Identity().Kind {
case source.Mod:
return modLinks(ctx, snapshot, fh)
case source.Go:
return goLinks(ctx, snapshot.View(), fh)
}
return nil, nil
}
func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
view := snapshot.View() view := snapshot.View()
file, m, err := snapshot.ModHandle(ctx, fh).Parse(ctx)
if err != nil {
return nil, err
}
var links []protocol.DocumentLink
for _, req := range file.Require {
dep := []byte(req.Mod.Path)
s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
i := bytes.Index(m.Content[s:e], dep)
if i == -1 {
continue
}
// Shift the start position to the location of the
// dependency within the require statement.
start, end := token.Pos(s+i), token.Pos(s+i+len(dep))
target := fmt.Sprintf("https://%s/mod/%s", view.Options().LinkTarget, req.Mod.String())
if l, err := toProtocolLink(view, m, target, start, end, source.Mod); err == nil {
links = append(links, l)
} else {
log.Error(ctx, "failed to create protocol link", err)
}
}
// TODO(ridersofrohan): handle links for replace and exclude directives
if syntax := file.Syntax; syntax == nil {
return links, nil
}
// Get all the links that are contained in the comments of the file.
for _, expr := range file.Syntax.Stmt {
comments := expr.Comment()
if comments == nil {
continue
}
for _, cmt := range comments.Before {
links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
}
for _, cmt := range comments.Suffix {
links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
}
for _, cmt := range comments.After {
links = append(links, findLinksInString(ctx, view, cmt.Token, token.Pos(cmt.Start.Byte), m, source.Mod)...)
}
}
return links, nil
}
func goLinks(ctx context.Context, view source.View, fh source.FileHandle) ([]protocol.DocumentLink, error) {
phs, err := view.Snapshot().PackageHandles(ctx, fh) phs, err := view.Snapshot().PackageHandles(ctx, fh)
if err != nil { if err != nil {
return nil, err return nil, err
@ -52,7 +110,7 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target) target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
// Account for the quotation marks in the positions. // Account for the quotation marks in the positions.
start, end := n.Path.Pos()+1, n.Path.End()-1 start, end := n.Path.Pos()+1, n.Path.End()-1
if l, err := toProtocolLink(view, m, target, start, end); err == nil { if l, err := toProtocolLink(view, m, target, start, end, source.Go); err == nil {
links = append(links, l) links = append(links, l)
} else { } else {
log.Error(ctx, "failed to create protocol link", err) log.Error(ctx, "failed to create protocol link", err)
@ -62,7 +120,7 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
case *ast.BasicLit: case *ast.BasicLit:
// Look for links in string literals. // Look for links in string literals.
if n.Kind == token.STRING { if n.Kind == token.STRING {
links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m)...) links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m, source.Go)...)
} }
return false return false
} }
@ -71,7 +129,7 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
// Look for links in comments. // Look for links in comments.
for _, commentGroup := range file.Comments { for _, commentGroup := range file.Comments {
for _, comment := range commentGroup.List { for _, comment := range commentGroup.List {
links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m)...) links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m, source.Go)...)
} }
} }
return links, nil return links, nil
@ -96,7 +154,7 @@ func moduleAtVersion(ctx context.Context, target string, ph source.PackageHandle
return modpath, version, true return modpath, version, true
} }
func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) []protocol.DocumentLink { func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) []protocol.DocumentLink {
var links []protocol.DocumentLink var links []protocol.DocumentLink
for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) { for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) {
start, end := index[0], index[1] start, end := index[0], index[1]
@ -111,7 +169,7 @@ func findLinksInString(ctx context.Context, view source.View, src string, pos to
if url.Scheme == "" { if url.Scheme == "" {
url.Scheme = "https" url.Scheme = "https"
} }
l, err := toProtocolLink(view, m, url.String(), startPos, endPos) l, err := toProtocolLink(view, m, url.String(), startPos, endPos, fileKind)
if err != nil { if err != nil {
log.Error(ctx, "failed to create protocol link", err) log.Error(ctx, "failed to create protocol link", err)
continue continue
@ -130,7 +188,7 @@ func findLinksInString(ctx context.Context, view source.View, src string, pos to
} }
org, repo, number := matches[1], matches[2], matches[3] org, repo, number := matches[1], matches[2], matches[3]
target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
l, err := toProtocolLink(view, m, target, startPos, endPos) l, err := toProtocolLink(view, m, target, startPos, endPos, fileKind)
if err != nil { if err != nil {
log.Error(ctx, "failed to create protocol link", err) log.Error(ctx, "failed to create protocol link", err)
continue continue
@ -152,14 +210,34 @@ var (
issueRegexp *regexp.Regexp issueRegexp *regexp.Regexp
) )
func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos) (protocol.DocumentLink, error) { func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) {
spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span() var rng protocol.Range
if err != nil { switch fileKind {
return protocol.DocumentLink{}, err case source.Go:
} spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
rng, err := m.Range(spn) if err != nil {
if err != nil { return protocol.DocumentLink{}, err
return protocol.DocumentLink{}, err }
rng, err = m.Range(spn)
if err != nil {
return protocol.DocumentLink{}, err
}
case source.Mod:
s, e := int(start), int(end)
line, col, err := m.Converter.ToPosition(s)
if err != nil {
return protocol.DocumentLink{}, err
}
start := span.NewPoint(line, col, s)
line, col, err = m.Converter.ToPosition(e)
if err != nil {
return protocol.DocumentLink{}, err
}
end := span.NewPoint(line, col, e)
rng, err = m.Range(span.New(m.URI, start, end))
if err != nil {
return protocol.DocumentLink{}, err
}
} }
return protocol.DocumentLink{ return protocol.DocumentLink{
Range: rng, Range: rng,

View File

@ -13,7 +13,6 @@ import (
func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]protocol.CodeLens, error) { func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]protocol.CodeLens, error) {
realURI, _ := snapshot.View().ModFiles() realURI, _ := snapshot.View().ModFiles()
// Check the case when the tempModfile flag is turned off.
if realURI == "" { if realURI == "" {
return nil, nil return nil, nil
} }
@ -24,11 +23,11 @@ func CodeLens(ctx context.Context, snapshot source.Snapshot, uri span.URI) ([]pr
ctx, done := trace.StartSpan(ctx, "mod.CodeLens", telemetry.File.Of(realURI)) ctx, done := trace.StartSpan(ctx, "mod.CodeLens", telemetry.File.Of(realURI))
defer done() defer done()
pmh, err := snapshot.ParseModHandle(ctx) fh, err := snapshot.GetFile(realURI)
if err != nil { if err != nil {
return nil, err return nil, err
} }
f, m, upgrades, err := pmh.Upgrades(ctx) f, m, upgrades, err := snapshot.ModHandle(ctx, fh).Upgrades(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -51,9 +51,9 @@ type Snapshot interface {
// This function can have no data or error if there is no modfile detected. // This function can have no data or error if there is no modfile detected.
ModTidyHandle(ctx context.Context, fh FileHandle) (ModTidyHandle, error) ModTidyHandle(ctx context.Context, fh FileHandle) (ModTidyHandle, error)
// ParseModHandle returns a ParseModHandle for the view's go.mod file handle. // ModHandle returns a ModHandle for the passed in go.mod file handle.
// This function can have no data or error if there is no modfile detected. // This function can have no data if there is no modfile detected.
ParseModHandle(ctx context.Context) (ParseModHandle, error) ModHandle(ctx context.Context, fh FileHandle) ModHandle
// PackageHandles returns the PackageHandles for the packages that this file // PackageHandles returns the PackageHandles for the packages that this file
// belongs to. // belongs to.
@ -258,17 +258,23 @@ type ParseGoHandle interface {
Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error) Cached() (file *ast.File, src []byte, m *protocol.ColumnMapper, parseErr error, err error)
} }
// ParseModHandle represents a handle to the modfile for a go.mod. // ModHandle represents a handle to the modfile for a go.mod.
type ParseModHandle interface { type ModHandle interface {
// File returns a file handle for which to get the modfile. // File returns a file handle for which to get the modfile.
File() FileHandle File() FileHandle
// Parse returns the parsed modfile and a mapper for the go.mod file.
// If the file is not available, returns nil and an error.
Parse(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, error)
// Upgrades returns the parsed modfile, a mapper, and any dependency upgrades // Upgrades returns the parsed modfile, a mapper, and any dependency upgrades
// for the go.mod file. If the file is not available, returns nil and an error. // for the go.mod file. Note that this will only work if the go.mod is the view's go.mod.
// If the file is not available, returns nil and an error.
Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error)
} }
// ModTidyHandle represents a handle to the modfile for a go.mod. // ModTidyHandle represents a handle to the modfile for the view.
// Specifically for the purpose of getting diagnostics by running "go mod tidy".
type ModTidyHandle interface { type ModTidyHandle interface {
// File returns a file handle for which to get the modfile. // File returns a file handle for which to get the modfile.
File() FileHandle File() FileHandle

View File

@ -1,5 +1,11 @@
module upgradedep module upgradedep
// TODO(microsoft/vscode-go#12): Another issue. //@link(`microsoft/vscode-go#12`, `https://github.com/microsoft/vscode-go/issues/12`)
go 1.12 go 1.12
require example.com/extramodule v1.0.0 //@codelens("require example.com/extramodule v1.0.0", "Upgrade dependency to v1.1.0", "upgrade.dependency") // TODO(golang/go#1234): Link the relevant issue. //@link(`golang/go#1234`, `https://github.com/golang/go/issues/1234`)
require example.com/extramodule v1.0.0 //@link(`example.com/extramodule`, `https://pkg.go.dev/mod/example.com/extramodule@v1.0.0`),codelens("require example.com/extramodule v1.0.0", "Upgrade dependency to v1.1.0", "upgrade.dependency")
// https://example.com/comment: Another issue. //@link(`https://example.com/comment`,`https://example.com/comment`)

View File

@ -23,6 +23,6 @@ WorkspaceSymbolsCount = 0
FuzzyWorkspaceSymbolsCount = 0 FuzzyWorkspaceSymbolsCount = 0
CaseSensitiveWorkspaceSymbolsCount = 0 CaseSensitiveWorkspaceSymbolsCount = 0
SignaturesCount = 0 SignaturesCount = 0
LinksCount = 0 LinksCount = 4
ImplementationsCount = 0 ImplementationsCount = 0

View File

@ -702,8 +702,12 @@ func Run(t *testing.T, tests Tests, data *Data) {
t.Helper() t.Helper()
for uri, wantLinks := range data.Links { for uri, wantLinks := range data.Links {
// If we are testing GOPATH, then we do not want links with // If we are testing GOPATH, then we do not want links with
// the versions attached (pkg.go.dev/repoa/moda@v1.1.0/pkg). // the versions attached (pkg.go.dev/repoa/moda@v1.1.0/pkg),
// unless the file is a go.mod, then we can skip it alltogether.
if data.Exported.Exporter == packagestest.GOPATH { if data.Exported.Exporter == packagestest.GOPATH {
if strings.HasSuffix(uri.Filename(), ".mod") {
continue
}
re := regexp.MustCompile(`@v\d+\.\d+\.[\w-]+`) re := regexp.MustCompile(`@v\d+\.\d+\.[\w-]+`)
for i, link := range wantLinks { for i, link := range wantLinks {
wantLinks[i].Target = re.ReplaceAllString(link.Target, "") wantLinks[i].Target = re.ReplaceAllString(link.Target, "")
@ -1223,7 +1227,7 @@ func shouldSkip(data *Data, uri span.URI) bool {
} }
// If the -modfile flag is not available, then we do not want to run // If the -modfile flag is not available, then we do not want to run
// any tests on the go.mod file. // any tests on the go.mod file.
if strings.Contains(uri.Filename(), ".mod") { if strings.HasSuffix(uri.Filename(), ".mod") {
return true return true
} }
// If the -modfile flag is not available, then we do not want to test any // If the -modfile flag is not available, then we do not want to test any