mirror of
https://github.com/golang/go.git
synced 2025-05-05 15:43:04 +00:00
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 (
|
||||
"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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
9
internal/lsp/testdata/links/links.go
vendored
9
internal/lsp/testdata/links/links.go
vendored
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user