github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/link.go (about) 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. 4 5 package lsp 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "go/ast" 12 "go/token" 13 "net/url" 14 "regexp" 15 "strconv" 16 "strings" 17 "sync" 18 19 "golang.org/x/mod/modfile" 20 "github.com/april1989/origin-go-tools/internal/event" 21 "github.com/april1989/origin-go-tools/internal/lsp/debug/tag" 22 "github.com/april1989/origin-go-tools/internal/lsp/protocol" 23 "github.com/april1989/origin-go-tools/internal/lsp/source" 24 "github.com/april1989/origin-go-tools/internal/span" 25 ) 26 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) 29 defer release() 30 if !ok { 31 return nil, err 32 } 33 switch fh.Kind() { 34 case source.Mod: 35 links, err = modLinks(ctx, snapshot, fh) 36 case source.Go: 37 links, err = goLinks(ctx, snapshot, fh) 38 } 39 // Don't return errors for document links. 40 if err != nil { 41 event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.URI())) 42 return nil, nil 43 } 44 return links, nil 45 } 46 47 func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) { 48 pm, err := snapshot.ParseMod(ctx, fh) 49 if err != nil { 50 return nil, err 51 } 52 var links []protocol.DocumentLink 53 for _, req := range pm.File.Require { 54 if req.Syntax == nil { 55 continue 56 } 57 // See golang/go#36998: don't link to modules matching GOPRIVATE. 58 if snapshot.View().IsGoPrivatePath(req.Mod.Path) { 59 continue 60 } 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) 64 if i == -1 { 65 continue 66 } 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 := fmt.Sprintf("https://%s/mod/%s", snapshot.View().Options().LinkTarget, req.Mod.String()) 71 l, err := toProtocolLink(snapshot, pm.Mapper, target, start, end, source.Mod) 72 if err != nil { 73 return nil, err 74 } 75 links = append(links, l) 76 } 77 // TODO(ridersofrohan): handle links for replace and exclude directives. 78 if syntax := pm.File.Syntax; syntax == nil { 79 return links, nil 80 } 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() 84 if comments == nil { 85 continue 86 } 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) 90 if err != nil { 91 return nil, err 92 } 93 links = append(links, l...) 94 } 95 } 96 } 97 return links, nil 98 } 99 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 pkgs, err := snapshot.PackagesForFile(ctx, fh.URI(), source.TypecheckWorkspace) 104 if err != nil { 105 return nil, err 106 } 107 pkg, err := source.WidestPackage(pkgs) 108 if err != nil { 109 return nil, err 110 } 111 pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) 112 if err != nil { 113 return nil, err 114 } 115 var imports []*ast.ImportSpec 116 var str []*ast.BasicLit 117 ast.Inspect(pgf.File, func(node ast.Node) bool { 118 switch n := node.(type) { 119 case *ast.ImportSpec: 120 imports = append(imports, n) 121 return false 122 case *ast.BasicLit: 123 // Look for links in string literals. 124 if n.Kind == token.STRING { 125 str = append(str, n) 126 } 127 return false 128 } 129 return true 130 }) 131 var links []protocol.DocumentLink 132 // For import specs, provide a link to a documentation website, like 133 // https://pkg.go.dev. 134 if view.Options().ImportShortcut.ShowLinks() { 135 for _, imp := range imports { 136 target, err := strconv.Unquote(imp.Path.Value) 137 if err != nil { 138 continue 139 } 140 // See golang/go#36998: don't link to modules matching GOPRIVATE. 141 if view.IsGoPrivatePath(target) { 142 continue 143 } 144 if mod, version, ok := moduleAtVersion(ctx, snapshot, target, pkg); ok && strings.ToLower(view.Options().LinkTarget) == "pkg.go.dev" { 145 target = strings.Replace(target, mod, mod+"@"+version, 1) 146 } 147 // Account for the quotation marks in the positions. 148 start := imp.Path.Pos() + 1 149 end := imp.Path.End() - 1 150 target = fmt.Sprintf("https://%s/%s", view.Options().LinkTarget, target) 151 l, err := toProtocolLink(snapshot, pgf.Mapper, target, start, end, source.Go) 152 if err != nil { 153 return nil, err 154 } 155 links = append(links, l) 156 } 157 } 158 for _, s := range str { 159 l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Mapper, source.Go) 160 if err != nil { 161 return nil, err 162 } 163 links = append(links, l...) 164 } 165 for _, commentGroup := range pgf.File.Comments { 166 for _, comment := range commentGroup.List { 167 l, err := findLinksInString(ctx, snapshot, comment.Text, comment.Pos(), pgf.Mapper, source.Go) 168 if err != nil { 169 return nil, err 170 } 171 links = append(links, l...) 172 } 173 } 174 return links, nil 175 } 176 177 func moduleAtVersion(ctx context.Context, snapshot source.Snapshot, target string, pkg source.Package) (string, string, bool) { 178 impPkg, err := pkg.GetImport(target) 179 if err != nil { 180 return "", "", false 181 } 182 if impPkg.Module() == nil { 183 return "", "", false 184 } 185 version, modpath := impPkg.Module().Version, impPkg.Module().Path 186 if modpath == "" || version == "" { 187 return "", "", false 188 } 189 return modpath, version, true 190 } 191 192 func findLinksInString(ctx context.Context, snapshot source.Snapshot, src string, pos token.Pos, m *protocol.ColumnMapper, fileKind source.FileKind) ([]protocol.DocumentLink, error) { 193 var links []protocol.DocumentLink 194 for _, index := range snapshot.View().Options().URLRegexp.FindAllIndex([]byte(src), -1) { 195 start, end := index[0], index[1] 196 startPos := token.Pos(int(pos) + start) 197 endPos := token.Pos(int(pos) + end) 198 link := src[start:end] 199 linkURL, err := url.Parse(link) 200 // Fallback: Linkify IP addresses as suggested in golang/go#18824. 201 if err != nil { 202 linkURL, err = url.Parse("//" + link) 203 // Not all potential links will be valid, so don't return this error. 204 if err != nil { 205 continue 206 } 207 } 208 // If the URL has no scheme, use https. 209 if linkURL.Scheme == "" { 210 linkURL.Scheme = "https" 211 } 212 l, err := toProtocolLink(snapshot, m, linkURL.String(), startPos, endPos, fileKind) 213 if err != nil { 214 return nil, err 215 } 216 links = append(links, l) 217 } 218 // Handle golang/go#1234-style links. 219 r := getIssueRegexp() 220 for _, index := range r.FindAllIndex([]byte(src), -1) { 221 start, end := index[0], index[1] 222 startPos := token.Pos(int(pos) + start) 223 endPos := token.Pos(int(pos) + end) 224 matches := r.FindStringSubmatch(src) 225 if len(matches) < 4 { 226 continue 227 } 228 org, repo, number := matches[1], matches[2], matches[3] 229 target := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) 230 l, err := toProtocolLink(snapshot, m, target, startPos, endPos, fileKind) 231 if err != nil { 232 return nil, err 233 } 234 links = append(links, l) 235 } 236 return links, nil 237 } 238 239 func getIssueRegexp() *regexp.Regexp { 240 once.Do(func() { 241 issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`) 242 }) 243 return issueRegexp 244 } 245 246 var ( 247 once sync.Once 248 issueRegexp *regexp.Regexp 249 ) 250 251 func toProtocolLink(snapshot source.Snapshot, m *protocol.ColumnMapper, target string, start, end token.Pos, fileKind source.FileKind) (protocol.DocumentLink, error) { 252 var rng protocol.Range 253 switch fileKind { 254 case source.Go: 255 spn, err := span.NewRange(snapshot.FileSet(), start, end).Span() 256 if err != nil { 257 return protocol.DocumentLink{}, err 258 } 259 rng, err = m.Range(spn) 260 if err != nil { 261 return protocol.DocumentLink{}, err 262 } 263 case source.Mod: 264 s, e := int(start), int(end) 265 line, col, err := m.Converter.ToPosition(s) 266 if err != nil { 267 return protocol.DocumentLink{}, err 268 } 269 start := span.NewPoint(line, col, s) 270 line, col, err = m.Converter.ToPosition(e) 271 if err != nil { 272 return protocol.DocumentLink{}, err 273 } 274 end := span.NewPoint(line, col, e) 275 rng, err = m.Range(span.New(m.URI, start, end)) 276 if err != nil { 277 return protocol.DocumentLink{}, err 278 } 279 } 280 return protocol.DocumentLink{ 281 Range: rng, 282 Target: target, 283 }, nil 284 }