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