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  }