github.com/jd-ly/tools@v0.5.7/internal/lsp/mod/hover.go (about)

     1  package mod
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"go/token"
     8  	"strings"
     9  
    10  	"golang.org/x/mod/modfile"
    11  	"github.com/jd-ly/tools/internal/event"
    12  	"github.com/jd-ly/tools/internal/lsp/protocol"
    13  	"github.com/jd-ly/tools/internal/lsp/source"
    14  	"github.com/jd-ly/tools/internal/span"
    15  	errors "golang.org/x/xerrors"
    16  )
    17  
    18  func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
    19  	var found bool
    20  	for _, uri := range snapshot.ModFiles() {
    21  		if fh.URI() == uri {
    22  			found = true
    23  			break
    24  		}
    25  	}
    26  
    27  	// We only provide hover information for the view's go.mod files.
    28  	if !found {
    29  		return nil, nil
    30  	}
    31  
    32  	ctx, done := event.Start(ctx, "mod.Hover")
    33  	defer done()
    34  
    35  	// Get the position of the cursor.
    36  	pm, err := snapshot.ParseMod(ctx, fh)
    37  	if err != nil {
    38  		return nil, errors.Errorf("getting modfile handle: %w", err)
    39  	}
    40  	spn, err := pm.Mapper.PointSpan(position)
    41  	if err != nil {
    42  		return nil, errors.Errorf("computing cursor position: %w", err)
    43  	}
    44  	hoverRng, err := spn.Range(pm.Mapper.Converter)
    45  	if err != nil {
    46  		return nil, errors.Errorf("computing hover range: %w", err)
    47  	}
    48  
    49  	// Confirm that the cursor is at the position of a require statement.
    50  	var req *modfile.Require
    51  	var startPos, endPos int
    52  	for _, r := range pm.File.Require {
    53  		dep := []byte(r.Mod.Path)
    54  		s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte
    55  		i := bytes.Index(pm.Mapper.Content[s:e], dep)
    56  		if i == -1 {
    57  			continue
    58  		}
    59  		// Shift the start position to the location of the
    60  		// dependency within the require statement.
    61  		startPos, endPos = s+i, s+i+len(dep)
    62  		if token.Pos(startPos) <= hoverRng.Start && hoverRng.Start <= token.Pos(endPos) {
    63  			req = r
    64  			break
    65  		}
    66  	}
    67  
    68  	// The cursor position is not on a require statement.
    69  	if req == nil {
    70  		return nil, nil
    71  	}
    72  
    73  	// Get the `go mod why` results for the given file.
    74  	why, err := snapshot.ModWhy(ctx, fh)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	explanation, ok := why[req.Mod.Path]
    79  	if !ok {
    80  		return nil, nil
    81  	}
    82  
    83  	// Get the range to highlight for the hover.
    84  	line, col, err := pm.Mapper.Converter.ToPosition(startPos)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	start := span.NewPoint(line, col, startPos)
    89  
    90  	line, col, err = pm.Mapper.Converter.ToPosition(endPos)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  	end := span.NewPoint(line, col, endPos)
    95  
    96  	spn = span.New(fh.URI(), start, end)
    97  	rng, err := pm.Mapper.Range(spn)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	options := snapshot.View().Options()
   102  	isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path)
   103  	explanation = formatExplanation(explanation, req, options, isPrivate)
   104  	return &protocol.Hover{
   105  		Contents: protocol.MarkupContent{
   106  			Kind:  options.PreferredContentFormat,
   107  			Value: explanation,
   108  		},
   109  		Range: rng,
   110  	}, nil
   111  }
   112  
   113  func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string {
   114  	text = strings.TrimSuffix(text, "\n")
   115  	splt := strings.Split(text, "\n")
   116  	length := len(splt)
   117  
   118  	var b strings.Builder
   119  	// Write the heading as an H3.
   120  	b.WriteString("##" + splt[0])
   121  	if options.PreferredContentFormat == protocol.Markdown {
   122  		b.WriteString("\n\n")
   123  	} else {
   124  		b.WriteRune('\n')
   125  	}
   126  
   127  	// If the explanation is 2 lines, then it is of the form:
   128  	// # golang.org/x/text/encoding
   129  	// (main module does not need package golang.org/x/text/encoding)
   130  	if length == 2 {
   131  		b.WriteString(splt[1])
   132  		return b.String()
   133  	}
   134  
   135  	imp := splt[length-1] // import path
   136  	reference := imp
   137  	// See golang/go#36998: don't link to modules matching GOPRIVATE.
   138  	if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
   139  		target := imp
   140  		if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
   141  			target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
   142  		}
   143  		reference = fmt.Sprintf("[%s](%s)", imp, source.BuildLink(options.LinkTarget, target, ""))
   144  	}
   145  	b.WriteString("This module is necessary because " + reference + " is imported in")
   146  
   147  	// If the explanation is 3 lines, then it is of the form:
   148  	// # github.com/jd-ly/tools
   149  	// modtest
   150  	// github.com/jd-ly/tools/go/packages
   151  	if length == 3 {
   152  		msg := fmt.Sprintf(" `%s`.", splt[1])
   153  		b.WriteString(msg)
   154  		return b.String()
   155  	}
   156  
   157  	// If the explanation is more than 3 lines, then it is of the form:
   158  	// # golang.org/x/text/language
   159  	// rsc.io/quote
   160  	// rsc.io/sampler
   161  	// golang.org/x/text/language
   162  	b.WriteString(":\n```text")
   163  	dash := ""
   164  	for _, imp := range splt[1 : length-1] {
   165  		dash += "-"
   166  		b.WriteString("\n" + dash + " " + imp)
   167  	}
   168  	b.WriteString("\n```")
   169  	return b.String()
   170  }