package mod import ( "bytes" "context" "fmt" "go/token" "strings" "golang.org/x/mod/modfile" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/event" ) func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { realURI, _ := snapshot.View().ModFiles() // Only get hover information on the go.mod for the view. if realURI == "" || fh.Identity().URI != realURI { return nil, nil } ctx, done := event.StartSpan(ctx, "mod.Hover") defer done() file, m, why, err := snapshot.ModHandle(ctx, fh).Why(ctx) if err != nil { return nil, err } // Get the position of the cursor. spn, err := m.PointSpan(position) if err != nil { return nil, err } hoverRng, err := spn.Range(m.Converter) if err != nil { return nil, err } var req *modfile.Require var startPos, endPos int for _, r := range file.Require { dep := []byte(r.Mod.Path) s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte i := bytes.Index(m.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 } } if req == nil || why == nil { return nil, nil } explanation, ok := why[req.Mod.Path] if !ok { return nil, nil } // Get the range to highlight for the hover. line, col, err := m.Converter.ToPosition(startPos) if err != nil { return nil, err } start := span.NewPoint(line, col, startPos) line, col, err = m.Converter.ToPosition(endPos) if err != nil { return nil, err } end := span.NewPoint(line, col, endPos) spn = span.New(fh.Identity().URI, start, end) rng, err := m.Range(spn) if err != nil { return nil, err } options := snapshot.View().Options() explanation = formatExplanation(explanation, req, options) return &protocol.Hover{ Contents: protocol.MarkupContent{ Kind: options.PreferredContentFormat, Value: explanation, }, Range: rng, }, nil } func formatExplanation(text string, req *modfile.Require, options source.Options) 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] target := imp if strings.ToLower(options.LinkTarget) == "pkg.go.dev" { target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1) } target = fmt.Sprintf("https://%s/%s", options.LinkTarget, target) b.WriteString("This module is necessary because ") msg := fmt.Sprintf("[%s](%s) is imported in", imp, target) b.WriteString(msg) // 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() }