From 8308f912863565ef47725163d9193b36ee0a8bc4 Mon Sep 17 00:00:00 2001 From: litleleprikon Date: Mon, 8 Jul 2019 00:25:19 +0200 Subject: [PATCH] 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 Reviewed-by: Rebecca Stambler --- internal/lsp/link.go | 116 ++++++++++++++++++++++----- internal/lsp/lsp_test.go | 15 ++++ internal/lsp/testdata/links/links.go | 9 ++- internal/lsp/tests/tests.go | 16 ++-- 4 files changed, 130 insertions(+), 26 deletions(-) diff --git a/internal/lsp/link.go b/internal/lsp/link.go index 90fd2baf..734d3eb6 100644 --- a/internal/lsp/link.go +++ b/internal/lsp/link.go @@ -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 } diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index b056fe95..f4480592 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -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 { diff --git a/internal/lsp/testdata/links/links.go b/internal/lsp/testdata/links/links.go index b97da745..44053b3d 100644 --- a/internal/lsp/testdata/links/links.go +++ b/internal/lsp/testdata/links/links.go @@ -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 +} diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index bfa1bc23..4f09d720 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -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, }) }