github.com/v2fly/tools@v0.100.0/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 "github.com/v2fly/tools/internal/event" 20 "github.com/v2fly/tools/internal/lsp/debug/tag" 21 "github.com/v2fly/tools/internal/lsp/protocol" 22 "github.com/v2fly/tools/internal/lsp/source" 23 "github.com/v2fly/tools/internal/span" 24 "golang.org/x/mod/modfile" 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 := source.BuildLink(snapshot.View().Options().LinkTarget, "mod/"+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 pkg, err := snapshot.PackageForFile(ctx, fh.URI(), source.TypecheckWorkspace, source.WidestPackage) 104 if err != nil { 105 return nil, err 106 } 107 pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) 108 if err != nil { 109 return nil, err 110 } 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) 117 return false 118 case *ast.BasicLit: 119 // Look for links in string literals. 120 if n.Kind == token.STRING { 121 str = append(str, n) 122 } 123 return false 124 } 125 return true 126 }) 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) 133 if err != nil { 134 continue 135 } 136 // See golang/go#36998: don't link to modules matching GOPRIVATE. 137 if view.IsGoPrivatePath(target) { 138 continue 139 } 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) 142 } 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) 148 if err != nil { 149 return nil, err 150 } 151 links = append(links, l) 152 } 153 } 154 for _, s := range str { 155 l, err := findLinksInString(ctx, snapshot, s.Value, s.Pos(), pgf.Mapper, source.Go) 156 if err != nil { 157 return nil, err 158 } 159 links = append(links, l...) 160 } 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) 164 if err != nil { 165 return nil, err 166 } 167 links = append(links, l...) 168 } 169 } 170 return links, nil 171 } 172 173 func moduleAtVersion(ctx context.Context, snapshot source.Snapshot, target string, pkg source.Package) (string, string, bool) { 174 impPkg, err := pkg.GetImport(target) 175 if err != nil { 176 return "", "", false 177 } 178 if impPkg.Version() == nil { 179 return "", "", false 180 } 181 version, modpath := impPkg.Version().Version, impPkg.Version().Path 182 if modpath == "" || version == "" { 183 return "", "", false 184 } 185 return modpath, version, true 186 } 187 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. 197 if err != nil { 198 linkURL, err = url.Parse("//" + link) 199 // Not all potential links will be valid, so don't return this error. 200 if err != nil { 201 continue 202 } 203 } 204 // If the URL has no scheme, use https. 205 if linkURL.Scheme == "" { 206 linkURL.Scheme = "https" 207 } 208 l, err := toProtocolLink(snapshot, m, linkURL.String(), startPos, endPos, fileKind) 209 if err != nil { 210 return nil, err 211 } 212 links = append(links, l) 213 } 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 { 222 continue 223 } 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) 227 if err != nil { 228 return nil, err 229 } 230 links = append(links, l) 231 } 232 return links, nil 233 } 234 235 func getIssueRegexp() *regexp.Regexp { 236 once.Do(func() { 237 issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`) 238 }) 239 return issueRegexp 240 } 241 242 var ( 243 once sync.Once 244 issueRegexp *regexp.Regexp 245 ) 246 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 249 switch fileKind { 250 case source.Go: 251 spn, err := span.NewRange(snapshot.FileSet(), start, end).Span() 252 if err != nil { 253 return protocol.DocumentLink{}, err 254 } 255 rng, err = m.Range(spn) 256 if err != nil { 257 return protocol.DocumentLink{}, err 258 } 259 case source.Mod: 260 s, e := int(start), int(end) 261 line, col, err := m.Converter.ToPosition(s) 262 if err != nil { 263 return protocol.DocumentLink{}, err 264 } 265 start := span.NewPoint(line, col, s) 266 line, col, err = m.Converter.ToPosition(e) 267 if err != nil { 268 return protocol.DocumentLink{}, err 269 } 270 end := span.NewPoint(line, col, e) 271 rng, err = m.Range(span.New(m.URI, start, end)) 272 if err != nil { 273 return protocol.DocumentLink{}, err 274 } 275 } 276 return protocol.DocumentLink{ 277 Range: rng, 278 Target: target, 279 }, nil 280 }