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:
parent
63f37bb4d3
commit
8308f91286
|
@ -7,9 +7,14 @@ package lsp
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/token"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/tools/internal/lsp/protocol"
|
"golang.org/x/tools/internal/lsp/protocol"
|
||||||
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
"golang.org/x/tools/internal/span"
|
"golang.org/x/tools/internal/span"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,26 +29,99 @@ func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLink
|
||||||
if file == nil {
|
if file == nil {
|
||||||
return nil, fmt.Errorf("no AST for %v", uri)
|
return nil, fmt.Errorf("no AST for %v", uri)
|
||||||
}
|
}
|
||||||
// Add a Godoc link for each imported package.
|
|
||||||
var result []protocol.DocumentLink
|
var links []protocol.DocumentLink
|
||||||
for _, imp := range file.Imports {
|
|
||||||
spn, err := span.NewRange(view.Session().Cache().FileSet(), imp.Pos(), imp.End()).Span()
|
ast.Inspect(file, func(node ast.Node) bool {
|
||||||
if err != nil {
|
switch n := node.(type) {
|
||||||
return nil, err
|
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)
|
return true
|
||||||
if err != nil {
|
})
|
||||||
return nil, err
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -762,15 +762,30 @@ func (r *runner) Link(t *testing.T, data tests.Links) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
var notePositions []token.Position
|
||||||
links := make(map[span.Span]string, len(wantLinks))
|
links := make(map[span.Span]string, len(wantLinks))
|
||||||
for _, link := range wantLinks {
|
for _, link := range wantLinks {
|
||||||
links[link.Src] = link.Target
|
links[link.Src] = link.Target
|
||||||
|
notePositions = append(notePositions, link.NotePosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, link := range gotLinks {
|
for _, link := range gotLinks {
|
||||||
spn, err := m.RangeSpan(link.Range)
|
spn, err := m.RangeSpan(link.Range)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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 {
|
if target, ok := links[spn]; ok {
|
||||||
delete(links, spn)
|
delete(links, spn)
|
||||||
if target != link.Target {
|
if target != link.Target {
|
||||||
|
|
|
@ -3,10 +3,17 @@ package links
|
||||||
import (
|
import (
|
||||||
"fmt" //@link(re`".*"`,"https://godoc.org/fmt")
|
"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 (
|
var (
|
||||||
_ fmt.Formatter
|
_ fmt.Formatter
|
||||||
_ foo.StructFoo
|
_ 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
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/expect"
|
||||||
"golang.org/x/tools/go/packages"
|
"golang.org/x/tools/go/packages"
|
||||||
"golang.org/x/tools/go/packages/packagestest"
|
"golang.org/x/tools/go/packages/packagestest"
|
||||||
"golang.org/x/tools/internal/lsp/source"
|
"golang.org/x/tools/internal/lsp/source"
|
||||||
|
@ -37,7 +38,7 @@ const (
|
||||||
ExpectedRenamesCount = 16
|
ExpectedRenamesCount = 16
|
||||||
ExpectedSymbolsCount = 1
|
ExpectedSymbolsCount = 1
|
||||||
ExpectedSignaturesCount = 21
|
ExpectedSignaturesCount = 21
|
||||||
ExpectedLinksCount = 2
|
ExpectedLinksCount = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -117,8 +118,9 @@ type CompletionSnippet struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Link struct {
|
type Link struct {
|
||||||
Src span.Span
|
Src span.Span
|
||||||
Target string
|
Target string
|
||||||
|
NotePosition token.Position
|
||||||
}
|
}
|
||||||
|
|
||||||
type Golden struct {
|
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()
|
uri := spn.URI()
|
||||||
data.Links[uri] = append(data.Links[uri], Link{
|
data.Links[uri] = append(data.Links[uri], Link{
|
||||||
Src: spn,
|
Src: spn,
|
||||||
Target: link,
|
Target: link,
|
||||||
|
NotePosition: position,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue