internal/lsp: add links search in comments and string literals

Add to "textDocument/documentLink" request handler ability to search
URLs in string literals and comments.

Fixes golang/go#32339

Change-Id: Ic67ad7bd94feba0bb67ab090a8903e30b2dff996
Reviewed-on: https://go-review.googlesource.com/c/tools/+/185219
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
litleleprikon 2019-07-08 00:25:19 +02:00 committed by Rebecca Stambler
parent 63f37bb4d3
commit 8308f91286
4 changed files with 130 additions and 26 deletions

View File

@ -7,9 +7,14 @@ package lsp
import (
"context"
"fmt"
"go/ast"
"go/token"
"regexp"
"strconv"
"sync"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
@ -24,26 +29,99 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
if file == nil {
return nil, fmt.Errorf("no AST for %v", uri)
}
// Add a Godoc link for each imported package.
var result []protocol.DocumentLink
for _, imp := range file.Imports {
spn, err := span.NewRange(view.Session().Cache().FileSet(), imp.Pos(), imp.End()).Span()
if err != nil {
return nil, err
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 {
view.Session().Logger().Errorf(ctx, "cannot unquote import path %s: %v", n.Path.Value, err)
return false
}
target = "https://godoc.org/" + target
l, err := toProtocolLink(view, m, target, n.Pos(), n.End())
view.Session().Logger().Errorf(ctx, "cannot initialize DocumentLink %s: %v", n.Path.Value, err)
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 {
view.Session().Logger().Errorf(ctx, "cannot find links in string: %v", err)
return false
}
links = append(links, l...)
return false
}
rng, err := m.Range(spn)
if err != nil {
return nil, err
return true
})
for _, commentGroup := range file.Comments {
for _, comment := range commentGroup.List {
l, err := findLinksInString(comment.Text, comment.Pos(), view, m)
if err != nil {
view.Session().Logger().Errorf(ctx, "cannot find links in comment: %v", err)
continue
}
links = append(links, l...)
}
target, err := strconv.Unquote(imp.Path.Value)
if err != nil {
continue
}
target = "https://godoc.org/" + target
result = append(result, protocol.DocumentLink{
Range: rng,
Target: target,
})
}
return result, nil
return links, nil
}
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
}
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 {
return nil, fmt.Errorf("cannot create regexp for links: %s", err.Error())
}
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
}

View File

@ -762,15 +762,30 @@ func (r *runner) Link(t *testing.T, data tests.Links) {
if err != nil {
t.Fatal(err)
}
var notePositions []token.Position
links := make(map[span.Span]string, len(wantLinks))
for _, link := range wantLinks {
links[link.Src] = link.Target
notePositions = append(notePositions, link.NotePosition)
}
for _, link := range gotLinks {
spn, err := m.RangeSpan(link.Range)
if err != nil {
t.Fatal(err)
}
linkInNote := false
for _, notePosition := range notePositions {
// Drop the links found inside expectation notes arguments as this links are not collected by expect package
if notePosition.Line == spn.Start().Line() &&
notePosition.Column <= spn.Start().Column() {
delete(links, spn)
linkInNote = true
}
}
if linkInNote {
continue
}
if target, ok := links[spn]; ok {
delete(links, spn)
if target != link.Target {

View File

@ -3,10 +3,17 @@ package links
import (
"fmt" //@link(re`".*"`,"https://godoc.org/fmt")
"golang.org/x/tools/internal/lsp/foo" //@link(re`".*"`,"https://godoc.org/golang.org/x/tools/internal/lsp/foo")
"golang.org/x/tools/internal/lsp/foo" //@link(re`".*"`,`https://godoc.org/golang.org/x/tools/internal/lsp/foo`)
)
var (
_ fmt.Formatter
_ foo.StructFoo
)
// Foo function
func Foo() string {
/*https://example.com/comment */ //@link("https://example.com/comment","https://example.com/comment")
url := "https://example.com/string_literal" //@link("https://example.com/string_literal","https://example.com/string_literal")
return url
}

View File

@ -15,6 +15,7 @@ import (
"strings"
"testing"
"golang.org/x/tools/go/expect"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/source"
@ -37,7 +38,7 @@ const (
ExpectedRenamesCount = 16
ExpectedSymbolsCount = 1
ExpectedSignaturesCount = 21
ExpectedLinksCount = 2
ExpectedLinksCount = 4
)
const (
@ -117,8 +118,9 @@ type CompletionSnippet struct {
}
type Link struct {
Src span.Span
Target string
Src span.Span
Target string
NotePosition token.Position
}
type Golden struct {
@ -527,10 +529,12 @@ func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain
}
}
func (data *Data) collectLinks(spn span.Span, link string) {
func (data *Data) collectLinks(spn span.Span, link string, note *expect.Note, fset *token.FileSet) {
position := fset.Position(note.Pos)
uri := spn.URI()
data.Links[uri] = append(data.Links[uri], Link{
Src: spn,
Target: link,
Src: spn,
Target: link,
NotePosition: position,
})
}