2019-04-24 09:33:45 -06:00
|
|
|
// Copyright 2018 The Go Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package lsp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2019-11-28 00:00:44 -07:00
|
|
|
"fmt"
|
2019-07-07 16:25:19 -06:00
|
|
|
"go/ast"
|
|
|
|
"go/token"
|
2020-01-10 00:06:12 -07:00
|
|
|
"net/url"
|
2019-12-24 13:58:48 -07:00
|
|
|
"regexp"
|
2019-04-24 09:33:45 -06:00
|
|
|
"strconv"
|
2020-02-11 08:12:40 -07:00
|
|
|
"strings"
|
2019-12-24 13:58:48 -07:00
|
|
|
"sync"
|
2019-04-24 09:33:45 -06:00
|
|
|
|
|
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
2019-07-07 16:25:19 -06:00
|
|
|
"golang.org/x/tools/internal/lsp/source"
|
2019-04-24 09:33:45 -06:00
|
|
|
"golang.org/x/tools/internal/span"
|
2019-08-13 13:07:39 -06:00
|
|
|
"golang.org/x/tools/internal/telemetry/log"
|
2019-04-24 09:33:45 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
2020-01-10 00:06:12 -07:00
|
|
|
// TODO(golang/go#36501): Support document links for go.mod files.
|
2020-02-13 11:46:49 -07:00
|
|
|
snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go)
|
|
|
|
if !ok {
|
|
|
|
return nil, err
|
2019-12-10 09:51:34 -07:00
|
|
|
}
|
2020-02-13 11:46:49 -07:00
|
|
|
view := snapshot.View()
|
2020-02-11 08:12:40 -07:00
|
|
|
phs, err := view.Snapshot().PackageHandles(ctx, fh)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ph, err := source.WidestPackageHandle(phs)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-02-10 21:10:59 -07:00
|
|
|
file, _, m, _, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx)
|
2019-09-17 09:19:11 -06:00
|
|
|
if err != nil {
|
2019-07-11 19:05:55 -06:00
|
|
|
return nil, err
|
2019-05-17 11:45:50 -06:00
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
var links []protocol.DocumentLink
|
|
|
|
ast.Inspect(file, func(node ast.Node) bool {
|
|
|
|
switch n := node.(type) {
|
|
|
|
case *ast.ImportSpec:
|
2020-01-10 00:06:12 -07:00
|
|
|
// For import specs, provide a link to a documentation website, like https://pkg.go.dev.
|
|
|
|
if target, err := strconv.Unquote(n.Path.Value); err == nil {
|
2020-02-11 08:12:40 -07:00
|
|
|
if mod, version, ok := moduleAtVersion(ctx, target, ph); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" {
|
|
|
|
target = strings.Replace(target, mod, mod+"@"+version, 1)
|
|
|
|
}
|
2020-01-10 00:06:12 -07:00
|
|
|
target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target)
|
|
|
|
// Account for the quotation marks in the positions.
|
|
|
|
start, end := n.Path.Pos()+1, n.Path.End()-1
|
|
|
|
if l, err := toProtocolLink(view, m, target, start, end); err == nil {
|
|
|
|
links = append(links, l)
|
|
|
|
} else {
|
|
|
|
log.Error(ctx, "failed to create protocol link", err)
|
|
|
|
}
|
2019-07-12 13:16:29 -06:00
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
return false
|
|
|
|
case *ast.BasicLit:
|
2020-01-10 00:06:12 -07:00
|
|
|
// Look for links in string literals.
|
|
|
|
if n.Kind == token.STRING {
|
|
|
|
links = append(links, findLinksInString(ctx, view, n.Value, n.Pos(), m)...)
|
2019-07-07 16:25:19 -06:00
|
|
|
}
|
|
|
|
return false
|
2019-04-24 09:33:45 -06:00
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
return true
|
|
|
|
})
|
2020-01-10 00:06:12 -07:00
|
|
|
// Look for links in comments.
|
2019-07-07 16:25:19 -06:00
|
|
|
for _, commentGroup := range file.Comments {
|
|
|
|
for _, comment := range commentGroup.List {
|
2020-01-10 00:06:12 -07:00
|
|
|
links = append(links, findLinksInString(ctx, view, comment.Text, comment.Pos(), m)...)
|
2019-04-24 09:33:45 -06:00
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
}
|
|
|
|
return links, nil
|
|
|
|
}
|
|
|
|
|
2020-02-11 08:12:40 -07:00
|
|
|
func moduleAtVersion(ctx context.Context, target string, ph source.PackageHandle) (string, string, bool) {
|
|
|
|
pkg, err := ph.Check(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
impPkg, err := pkg.GetImport(target)
|
|
|
|
if err != nil {
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
if impPkg.Module() == nil {
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
version, modpath := impPkg.Module().Version, impPkg.Module().Path
|
|
|
|
if modpath == "" || version == "" {
|
|
|
|
return "", "", false
|
|
|
|
}
|
|
|
|
return modpath, version, true
|
|
|
|
}
|
|
|
|
|
2020-01-10 00:06:12 -07:00
|
|
|
func findLinksInString(ctx context.Context, view source.View, src string, pos token.Pos, m *protocol.ColumnMapper) []protocol.DocumentLink {
|
2019-07-11 19:05:55 -06:00
|
|
|
var links []protocol.DocumentLink
|
2019-12-24 13:58:48 -07:00
|
|
|
for _, index := range view.Options().URLRegexp.FindAllIndex([]byte(src), -1) {
|
|
|
|
start, end := index[0], index[1]
|
|
|
|
startPos := token.Pos(int(pos) + start)
|
|
|
|
endPos := token.Pos(int(pos) + end)
|
2020-01-10 00:06:12 -07:00
|
|
|
url, err := url.Parse(src[start:end])
|
2019-12-24 13:58:48 -07:00
|
|
|
if err != nil {
|
2020-01-10 00:06:12 -07:00
|
|
|
log.Error(ctx, "failed to parse matching URL", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// If the URL has no scheme, use https.
|
|
|
|
if url.Scheme == "" {
|
|
|
|
url.Scheme = "https"
|
|
|
|
}
|
|
|
|
l, err := toProtocolLink(view, m, url.String(), startPos, endPos)
|
|
|
|
if err != nil {
|
|
|
|
log.Error(ctx, "failed to create protocol link", err)
|
|
|
|
continue
|
2019-12-24 13:58:48 -07:00
|
|
|
}
|
|
|
|
links = append(links, l)
|
|
|
|
}
|
|
|
|
// Handle golang/go#1234-style links.
|
|
|
|
r := getIssueRegexp()
|
|
|
|
for _, index := range r.FindAllIndex([]byte(src), -1) {
|
|
|
|
start, end := index[0], index[1]
|
2019-07-11 19:05:55 -06:00
|
|
|
startPos := token.Pos(int(pos) + start)
|
|
|
|
endPos := token.Pos(int(pos) + end)
|
2019-12-24 13:58:48 -07:00
|
|
|
matches := r.FindStringSubmatch(src)
|
|
|
|
if len(matches) < 4 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
org, repo, number := matches[1], matches[2], matches[3]
|
|
|
|
target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
|
|
|
|
l, err := toProtocolLink(view, m, target, startPos, endPos)
|
2019-07-11 19:05:55 -06:00
|
|
|
if err != nil {
|
2020-01-10 00:06:12 -07:00
|
|
|
log.Error(ctx, "failed to create protocol link", err)
|
|
|
|
continue
|
2019-07-11 19:05:55 -06:00
|
|
|
}
|
|
|
|
links = append(links, l)
|
|
|
|
}
|
2020-01-10 00:06:12 -07:00
|
|
|
return links
|
2019-07-11 19:05:55 -06:00
|
|
|
}
|
|
|
|
|
2019-12-24 13:58:48 -07:00
|
|
|
func getIssueRegexp() *regexp.Regexp {
|
|
|
|
once.Do(func() {
|
|
|
|
issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`)
|
|
|
|
})
|
|
|
|
return issueRegexp
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
once sync.Once
|
|
|
|
issueRegexp *regexp.Regexp
|
|
|
|
)
|
|
|
|
|
|
|
|
func toProtocolLink(view source.View, m *protocol.ColumnMapper, target string, start, end token.Pos) (protocol.DocumentLink, error) {
|
2019-07-07 16:25:19 -06:00
|
|
|
spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
|
|
|
|
if err != nil {
|
|
|
|
return protocol.DocumentLink{}, err
|
|
|
|
}
|
2019-12-24 13:58:48 -07:00
|
|
|
rng, err := m.Range(spn)
|
2019-07-07 16:25:19 -06:00
|
|
|
if err != nil {
|
|
|
|
return protocol.DocumentLink{}, err
|
|
|
|
}
|
2020-01-10 00:06:12 -07:00
|
|
|
return protocol.DocumentLink{
|
2019-07-07 16:25:19 -06:00
|
|
|
Range: rng,
|
|
|
|
Target: target,
|
2020-01-10 00:06:12 -07:00
|
|
|
}, nil
|
2019-07-07 16:25:19 -06:00
|
|
|
}
|