| 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() |
| } |