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  }