| 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) { | |
| uri := snapshot.View().ModFile() | |
| // For now, we only provide hover information for the view's go.mod file. | |
| if uri == "" || fh.URI() != uri { | |
| 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() | |
| } |