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.
14 "golang.org/x/mod/modfile"
15 "golang.org/x/tools/internal/event"
16 "golang.org/x/tools/internal/lsp/protocol"
17 "golang.org/x/tools/internal/lsp/source"
18 "golang.org/x/tools/internal/span"
19 errors "golang.org/x/xerrors"
22 func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) {
24 for _, uri := range snapshot.ModFiles() {
31 // We only provide hover information for the view's go.mod files.
36 ctx, done := event.Start(ctx, "mod.Hover")
39 // Get the position of the cursor.
40 pm, err := snapshot.ParseMod(ctx, fh)
42 return nil, errors.Errorf("getting modfile handle: %w", err)
44 spn, err := pm.Mapper.PointSpan(position)
46 return nil, errors.Errorf("computing cursor position: %w", err)
48 hoverRng, err := spn.Range(pm.Mapper.Converter)
50 return nil, errors.Errorf("computing hover range: %w", err)
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)
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) {
72 // The cursor position is not on a require statement.
77 // Get the `go mod why` results for the given file.
78 why, err := snapshot.ModWhy(ctx, fh)
82 explanation, ok := why[req.Mod.Path]
87 // Get the range to highlight for the hover.
88 line, col, err := pm.Mapper.Converter.ToPosition(startPos)
92 start := span.NewPoint(line, col, startPos)
94 line, col, err = pm.Mapper.Converter.ToPosition(endPos)
98 end := span.NewPoint(line, col, endPos)
100 spn = span.New(fh.URI(), start, end)
101 rng, err := pm.Mapper.Range(spn)
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,
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")
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")
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)
135 b.WriteString(splt[1])
139 imp := splt[length-1] // import path
141 // See golang/go#36998: don't link to modules matching GOPRIVATE.
142 if !isPrivate && options.PreferredContentFormat == protocol.Markdown {
144 if strings.ToLower(options.LinkTarget) == "pkg.go.dev" {
145 target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1)
147 reference = fmt.Sprintf("[%s](%s)", imp, source.BuildLink(options.LinkTarget, target, ""))
149 b.WriteString("This module is necessary because " + reference + " is imported in")
151 // If the explanation is 3 lines, then it is of the form:
152 // # golang.org/x/tools
154 // golang.org/x/tools/go/packages
156 msg := fmt.Sprintf(" `%s`.", splt[1])
161 // If the explanation is more than 3 lines, then it is of the form:
162 // # golang.org/x/text/language
165 // golang.org/x/text/language
166 b.WriteString(":\n```text")
168 for _, imp := range splt[1 : length-1] {
170 b.WriteString("\n" + dash + " " + imp)
172 b.WriteString("\n```")