--- /dev/null
+package mod
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "go/token"
+ "strings"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+ errors "golang.org/x/xerrors"
+)
+
+func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
+ var found bool
+ for _, uri := range snapshot.ModFiles() {
+ if fh.URI() == uri {
+ found = true
+ break
+ }
+ }
+
+ // We only provide hover information for the view's go.mod files.
+ if !found {
+ return nil, nil
+ }
+
+ ctx, done := event.Start(ctx, "mod.Hover")
+ defer done()
+
+ // Get the position of the cursor.
+ pm, err := snapshot.ParseMod(ctx, fh)
+ if err != nil {
+ return nil, errors.Errorf("getting modfile handle: %w", err)
+ }
+ spn, err := pm.Mapper.PointSpan(position)
+ if err != nil {
+ return nil, errors.Errorf("computing cursor position: %w", err)
+ }
+ hoverRng, err := spn.Range(pm.Mapper.Converter)
+ if err != nil {
+ return nil, errors.Errorf("computing hover range: %w", err)
+ }
+
+ // Confirm that the cursor is at the position of a require statement.
+ var req *modfile.Require
+ var startPos, endPos int
+ for _, r := range pm.File.Require {
+ dep := []byte(r.Mod.Path)
+ s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte
+ i := bytes.Index(pm.Mapper.Content[s:e], dep)
+ if i == -1 {
+ continue
+ }
+ // Shift the start position to the location of the
+ // dependency within the require statement.
+ startPos, endPos = s+i, s+i+len(dep)
+ if token.Pos(startPos) <= hoverRng.Start && hoverRng.Start <= token.Pos(endPos) {
+ req = r
+ break
+ }
+ }
+
+ // The cursor position is not on a require statement.
+ if req == nil {
+ return nil, nil
+ }
+
+ // Get the `go mod why` results for the given file.
+ why, err := snapshot.ModWhy(ctx, fh)
+ if err != nil {
+ return nil, err
+ }
+ explanation, ok := why[req.Mod.Path]
+ if !ok {
+ return nil, nil
+ }
+
+ // Get the range to highlight for the hover.
+ line, col, err := pm.Mapper.Converter.ToPosition(startPos)
+ if err != nil {
+ return nil, err
+ }
+ start := span.NewPoint(line, col, startPos)
+
+ line, col, err = pm.Mapper.Converter.ToPosition(endPos)
+ if err != nil {
+ return nil, err
+ }
+ end := span.NewPoint(line, col, endPos)
+
+ spn = span.New(fh.URI(), start, end)
+ rng, err := pm.Mapper.Range(spn)
+ if err != nil {
+ return nil, err
+ }
+ options := snapshot.View().Options()
+ isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path)
+ explanation = formatExplanation(explanation, req, options, isPrivate)
+ return &protocol.Hover{
+ Contents: protocol.MarkupContent{
+ Kind: options.PreferredContentFormat,
+ Value: explanation,
+ },
+ Range: rng,
+ }, nil
+}
+
+func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string {
+ text = strings.TrimSuffix(text, "\n")
+ splt := strings.Split(text, "\n")
+ length := len(splt)
+
+ var b strings.Builder
+ // Write the heading as an H3.
+ b.WriteString("##" + splt[0])
+ if options.PreferredContentFormat == protocol.Markdown {
+ b.WriteString("\n\n")
+ } else {
+ b.WriteRune('\n')
+ }
+
+ // If the explanation is 2 lines, then it is of the form:
+ // # golang.org/x/text/encoding
+ // (main module does not need package golang.org/x/text/encoding)
+ if length == 2 {
+ b.WriteString(splt[1])
+ return b.String()
+ }
+
+ imp := splt[length-1] // import path
+ reference := imp
+ // See golang/go#36998: don't link to modules matching GOPRIVATE.
+ if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
+ target := imp
+ if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
+ target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
+ }
+ reference = fmt.Sprintf("[%s](https://%s/%s)", imp, options.LinkTarget, target)
+ }
+ b.WriteString("This module is necessary because " + reference + " is imported in")
+
+ // If the explanation is 3 lines, then it is of the form:
+ // # golang.org/x/tools
+ // modtest
+ // golang.org/x/tools/go/packages
+ if length == 3 {
+ msg := fmt.Sprintf(" `%s`.", splt[1])
+ b.WriteString(msg)
+ return b.String()
+ }
+
+ // If the explanation is more than 3 lines, then it is of the form:
+ // # golang.org/x/text/language
+ // rsc.io/quote
+ // rsc.io/sampler
+ // golang.org/x/text/language
+ b.WriteString(":\n```text")
+ dash := ""
+ for _, imp := range splt[1 : length-1] {
+ dash += "-"
+ b.WriteString("\n" + dash + " " + imp)
+ }
+ b.WriteString("\n```")
+ return b.String()
+}