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-07-07 16:25:19 -06:00
|
|
|
"go/ast"
|
|
|
|
"go/token"
|
|
|
|
"regexp"
|
2019-04-24 09:33:45 -06:00
|
|
|
"strconv"
|
2019-07-07 16:25:19 -06: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"
|
|
|
|
"golang.org/x/tools/internal/telemetry/tag"
|
2019-08-06 13:13:11 -06:00
|
|
|
errors "golang.org/x/xerrors"
|
2019-04-24 09:33:45 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) {
|
|
|
|
uri := span.NewURI(params.TextDocument.URI)
|
2019-05-15 10:24:49 -06:00
|
|
|
view := s.session.ViewOf(uri)
|
2019-08-16 11:49:17 -06:00
|
|
|
f, err := getGoFile(ctx, view, uri)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-09-06 21:58:07 -06:00
|
|
|
fh := f.Handle(ctx)
|
2019-09-09 22:36:39 -06:00
|
|
|
file, m, err := view.Session().Cache().ParseGoHandle(fh, source.ParseFull).Parse(ctx)
|
2019-05-20 13:23:02 -06:00
|
|
|
if file == 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:
|
|
|
|
target, err := strconv.Unquote(n.Path.Value)
|
|
|
|
if err != nil {
|
2019-07-14 21:08:10 -06:00
|
|
|
log.Error(ctx, "cannot unquote import path", err, tag.Of("Path", n.Path.Value))
|
2019-07-07 16:25:19 -06:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
target = "https://godoc.org/" + target
|
|
|
|
l, err := toProtocolLink(view, m, target, n.Pos(), n.End())
|
2019-07-12 13:16:29 -06:00
|
|
|
if err != nil {
|
2019-07-14 21:08:10 -06:00
|
|
|
log.Error(ctx, "cannot initialize DocumentLink", err, tag.Of("Path", n.Path.Value))
|
2019-07-12 13:16:29 -06:00
|
|
|
return false
|
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
links = append(links, l)
|
|
|
|
return false
|
|
|
|
case *ast.BasicLit:
|
|
|
|
if n.Kind != token.STRING {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
l, err := findLinksInString(n.Value, n.Pos(), view, m)
|
|
|
|
if err != nil {
|
2019-07-14 21:08:10 -06:00
|
|
|
log.Error(ctx, "cannot find links in string", err)
|
2019-07-07 16:25:19 -06:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
links = append(links, l...)
|
|
|
|
return false
|
2019-04-24 09:33:45 -06:00
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
return true
|
|
|
|
})
|
|
|
|
|
|
|
|
for _, commentGroup := range file.Comments {
|
|
|
|
for _, comment := range commentGroup.List {
|
|
|
|
l, err := findLinksInString(comment.Text, comment.Pos(), view, m)
|
|
|
|
if err != nil {
|
2019-07-14 21:08:10 -06:00
|
|
|
log.Error(ctx, "cannot find links in comment", err)
|
2019-07-07 16:25:19 -06:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
links = append(links, l...)
|
2019-04-24 09:33:45 -06:00
|
|
|
}
|
2019-07-07 16:25:19 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return links, nil
|
|
|
|
}
|
|
|
|
|
2019-07-11 19:05:55 -06:00
|
|
|
func findLinksInString(src string, pos token.Pos, view source.View, mapper *protocol.ColumnMapper) ([]protocol.DocumentLink, error) {
|
|
|
|
var links []protocol.DocumentLink
|
|
|
|
re, err := getURLRegexp()
|
|
|
|
if err != nil {
|
2019-08-06 13:13:11 -06:00
|
|
|
return nil, errors.Errorf("cannot create regexp for links: %s", err.Error())
|
2019-07-11 19:05:55 -06:00
|
|
|
}
|
|
|
|
for _, urlIndex := range re.FindAllIndex([]byte(src), -1) {
|
|
|
|
start := urlIndex[0]
|
|
|
|
end := urlIndex[1]
|
|
|
|
startPos := token.Pos(int(pos) + start)
|
|
|
|
endPos := token.Pos(int(pos) + end)
|
|
|
|
target := src[start:end]
|
|
|
|
l, err := toProtocolLink(view, mapper, target, startPos, endPos)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
links = append(links, l)
|
|
|
|
}
|
|
|
|
return links, nil
|
|
|
|
}
|
|
|
|
|
2019-07-07 16:25:19 -06:00
|
|
|
const urlRegexpString = "(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?"
|
|
|
|
|
|
|
|
var (
|
|
|
|
urlRegexp *regexp.Regexp
|
|
|
|
regexpOnce sync.Once
|
|
|
|
regexpErr error
|
|
|
|
)
|
|
|
|
|
|
|
|
func getURLRegexp() (*regexp.Regexp, error) {
|
|
|
|
regexpOnce.Do(func() {
|
|
|
|
urlRegexp, regexpErr = regexp.Compile(urlRegexpString)
|
|
|
|
})
|
|
|
|
return urlRegexp, regexpErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func toProtocolLink(view source.View, mapper *protocol.ColumnMapper, target string, start, end token.Pos) (protocol.DocumentLink, error) {
|
|
|
|
spn, err := span.NewRange(view.Session().Cache().FileSet(), start, end).Span()
|
|
|
|
if err != nil {
|
|
|
|
return protocol.DocumentLink{}, err
|
|
|
|
}
|
|
|
|
rng, err := mapper.Range(spn)
|
|
|
|
if err != nil {
|
|
|
|
return protocol.DocumentLink{}, err
|
|
|
|
}
|
|
|
|
l := protocol.DocumentLink{
|
|
|
|
Range: rng,
|
|
|
|
Target: target,
|
|
|
|
}
|
|
|
|
return l, nil
|
|
|
|
}
|