1 // Copyright 2018 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
19 "golang.org/x/mod/modfile"
20 "golang.org/x/tools/internal/event"
21 "golang.org/x/tools/internal/lsp/debug/tag"
22 "golang.org/x/tools/internal/lsp/protocol"
23 "golang.org/x/tools/internal/lsp/source"
24 "golang.org/x/tools/internal/span"
27 func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) {
28 snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
35 links, err = modLinks(ctx, snapshot, fh)
37 links, err = goLinks(ctx, snapshot, fh)
39 // Don't return errors for document links.
41 event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.URI()))
47 func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
48 pm, err := snapshot.ParseMod(ctx, fh)
52 var links []protocol.DocumentLink
53 for _, req := range pm.File.Require {
54 if req.Syntax == nil {
57 // See golang/go#36998: don't link to modules matching GOPRIVATE.
58 if snapshot.View().IsGoPrivatePath(req.Mod.Path) {
61 dep := []byte(req.Mod.Path)
62 s, e := req.Syntax.Start.Byte, req.Syntax.End.Byte
63 i := bytes.Index(pm.Mapper.Content[s:e], dep)
67 // Shift the start position to the location of the
68 // dependency within the require statement.
69 start, end := token.Pos(s+i), token.Pos(s+i+len(dep))
70 target := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+req.Mod.String(), "")
71 l, err := toProtocolLink(snapshot, pm.Mapper, target, start, end, source.Mod)
75 links = append(links, l)
77 // TODO(ridersofrohan): handle links for replace and exclude directives.
78 if syntax := pm.File.Syntax; syntax == nil {
81 // Get all the links that are contained in the comments of the file.
82 for _, expr := range pm.File.Syntax.Stmt {
83 comments := expr.Comment()
87 for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} {
88 for _, comment := range section {
89 l, err := findLinksInString(ctx, snapshot, comment.Token, token.Pos(comment.Start.Byte), pm.Mapper, source.Mod)
93 links = append(links, l...)
100 func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) {
101 view := snapshot.View()
102 // We don't actually need type information, so any typecheck mode is fine.
103 pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckWorkspace, source.WidestPackage)
107 pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull)
111 var imports []*ast.ImportSpec
112 var str []*ast.BasicLit
113 ast.Inspect(pgf.File, func(node ast.Node) bool {
114 switch n := node.(type) {
115 case *ast.ImportSpec:
116 imports = append(imports, n)
119 // Look for links in string literals.
120 if n.Kind == token.STRING {
127 var links []protocol.DocumentLink
128 // For import specs, provide a link to a documentation website, like
129 // https://pkg.go.dev.
130 if view.Options().ImportShortcut.ShowLinks() {
131 for _, imp := range imports {
132 target, err := strconv.Unquote(imp.Path.Value)
136 // See golang/go#36998: don't link to modules matching GOPRIVATE.
137 if view.IsGoPrivatePath(target) {
140 if mod, version, ok := moduleAtVersion(ctx, snapshot, target, pkg); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" {
141 target = strings.Replace(target, mod, mod+"@"+version, 1)
143 // Account for the quotation marks in the positions.
144 start := imp.Path.Pos() + 1
145 end := imp.Path.End() - 1
146 target = source.BuildLink(view.Options().LinkTarget, target, "")
147 l, err := toProtocolLink(snapshot, pgf.Mapper, target, start, end, source.Go)
151 links = append(links, l)
154 for _, s := range str {
155 l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Mapper, source.Go)
159 links = append(links, l...)
161 for _, commentGroup := range pgf.File.Comments {
162 for _, comment := range commentGroup.List {
163 l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Mapper, source.Go)
167 links = append(links, l...)
173 func moduleAtVersion(ctx context.Context, snapshot source.Snapshot, target string, pkg source.Package) (string, string, bool) {
174 impPkg, err := pkg.GetImport(target)
178 if impPkg.Version() == nil {
181 version, modpath := impPkg.Version().Version, impPkg.Version().Path
182 if modpath == "" || version == "" {
185 return modpath, version, true
188 func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) ([]protocol.DocumentLink, error) {
189 var links []protocol.DocumentLink
190 for _, index := range snapshot.View().Options().URLRegexp.FindAllIndex([]byte(src), -1) {
191 start, end := index[0], index[1]
192 startPos := token.Pos(int(pos) + start)
193 endPos := token.Pos(int(pos) + end)
194 link := src[start:end]
195 linkURL, err := url.Parse(link)
196 // Fallback: Linkify IP addresses as suggested in golang/go#18824.
198 linkURL, err = url.Parse("//" + link)
199 // Not all potential links will be valid, so don't return this error.
204 // If the URL has no scheme, use https.
205 if linkURL.Scheme == "" {
206 linkURL.Scheme = "https"
208 l, err := toProtocolLink(snapshot, m, linkURL.String(), startPos, endPos, fileKind)
212 links = append(links, l)
214 // Handle golang/go#1234-style links.
215 r := getIssueRegexp()
216 for _, index := range r.FindAllIndex([]byte(src), -1) {
217 start, end := index[0], index[1]
218 startPos := token.Pos(int(pos) + start)
219 endPos := token.Pos(int(pos) + end)
220 matches := r.FindStringSubmatch(src)
221 if len(matches) < 4 {
224 org, repo, number := matches[1], matches[2], matches[3]
225 target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number)
226 l, err := toProtocolLink(snapshot, m, target, startPos, endPos, fileKind)
230 links = append(links, l)
235 func getIssueRegexp() *regexp.Regexp {
237 issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`)
244 issueRegexp *regexp.Regexp
247 func toProtocolLink(snapshot source.Snapshot, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) {
248 var rng protocol.Range
251 spn, err := span.NewRange(snapshot.FileSet(), start, end).Span()
253 return protocol.DocumentLink{}, err
255 rng, err = m.Range(spn)
257 return protocol.DocumentLink{}, err
260 s, e := int(start), int(end)
261 line, col, err := m.Converter.ToPosition(s)
263 return protocol.DocumentLink{}, err
265 start := span.NewPoint(line, col, s)
266 line, col, err = m.Converter.ToPosition(e)
268 return protocol.DocumentLink{}, err
270 end := span.NewPoint(line, col, e)
271 rng, err = m.Range(span.New(m.URI, start, end))
273 return protocol.DocumentLink{}, err
276 return protocol.DocumentLink{