10 "golang.org/x/mod/modfile"
11 "golang.org/x/tools/internal/event"
12 "golang.org/x/tools/internal/lsp/protocol"
13 "golang.org/x/tools/internal/lsp/source"
14 "golang.org/x/tools/internal/span"
15 errors "golang.org/x/xerrors"
18 func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
20 for _, uri := range snapshot.ModFiles() {
27 // We only provide hover information for the view's go.mod files.
32 ctx, done := event.Start(ctx, "mod.Hover")
35 // Get the position of the cursor.
36 pm, err := snapshot.ParseMod(ctx, fh)
38 return nil, errors.Errorf("getting modfile handle: %w", err)
40 spn, err := pm.Mapper.PointSpan(position)
42 return nil, errors.Errorf("computing cursor position: %w", err)
44 hoverRng, err := spn.Range(pm.Mapper.Converter)
46 return nil, errors.Errorf("computing hover range: %w", err)
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)
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) {
68 // The cursor position is not on a require statement.
73 // Get the `go mod why` results for the given file.
74 why, err := snapshot.ModWhy(ctx, fh)
78 explanation, ok := why[req.Mod.Path]
83 // Get the range to highlight for the hover.
84 line, col, err := pm.Mapper.Converter.ToPosition(startPos)
88 start := span.NewPoint(line, col, startPos)
90 line, col, err = pm.Mapper.Converter.ToPosition(endPos)
94 end := span.NewPoint(line, col, endPos)
96 spn = span.New(fh.URI(), start, end)
97 rng, err := pm.Mapper.Range(spn)
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,
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")
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")
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)
131 b.WriteString(splt[1])
135 imp := splt[length-1] // import path
137 // See golang/go#36998: don't link to modules matching GOPRIVATE.
138 if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
140 if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
141 target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
143 reference = fmt.Sprintf("[%s](https://%s/%s)", imp, options.LinkTarget, target)
145 b.WriteString("This module is necessary because " + reference + " is imported in")
147 // If the explanation is 3 lines, then it is of the form:
148 // # golang.org/x/tools
150 // golang.org/x/tools/go/packages
152 msg := fmt.Sprintf(" `%s`.", splt[1])
157 // If the explanation is more than 3 lines, then it is of the form:
158 // # golang.org/x/text/language
161 // golang.org/x/text/language
162 b.WriteString(":\n```text")
164 for _, imp := range splt[1 : length-1] {
166 b.WriteString("\n" + dash + " " + imp)
168 b.WriteString("\n```")