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>
diff --git a/internal/lsp/link.go b/internal/lsp/link.go
index 90fd2ba..734d3eb 100644
--- a/internal/lsp/link.go
+++ b/internal/lsp/link.go
@@ -7,9 +7,14 @@
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 @@
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 b056fe9..f448059 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -762,15 +762,30 @@
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 b97da74..44053b3 100644
--- a/internal/lsp/testdata/links/links.go
+++ b/internal/lsp/testdata/links/links.go
@@ -3,10 +3,17 @@
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 bfa1bc2..4f09d72 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -15,6 +15,7 @@
"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 @@
ExpectedRenamesCount = 16
ExpectedSymbolsCount = 1
ExpectedSignaturesCount = 21
- ExpectedLinksCount = 2
+ ExpectedLinksCount = 4
)
const (
@@ -117,8 +118,9 @@
}
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) 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,
})
}