github.com/v2fly/tools@v0.100.0/internal/lsp/mod/hover.go (about)

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