github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/mod/hover.go (about) 1 package mod 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "go/token" 8 "strings" 9 10 "golang.org/x/mod/modfile" 11 "github.com/april1989/origin-go-tools/internal/event" 12 "github.com/april1989/origin-go-tools/internal/lsp/protocol" 13 "github.com/april1989/origin-go-tools/internal/lsp/source" 14 "github.com/april1989/origin-go-tools/internal/span" 15 errors "golang.org/x/xerrors" 16 ) 17 18 func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { 19 uri := snapshot.View().ModFile() 20 21 // For now, we only provide hover information for the view's go.mod file. 22 if uri == "" || fh.URI() != uri { 23 return nil, nil 24 } 25 26 ctx, done := event.Start(ctx, "mod.Hover") 27 defer done() 28 29 // Get the position of the cursor. 30 pm, err := snapshot.ParseMod(ctx, fh) 31 if err != nil { 32 return nil, errors.Errorf("getting modfile handle: %w", err) 33 } 34 spn, err := pm.Mapper.PointSpan(position) 35 if err != nil { 36 return nil, errors.Errorf("computing cursor position: %w", err) 37 } 38 hoverRng, err := spn.Range(pm.Mapper.Converter) 39 if err != nil { 40 return nil, errors.Errorf("computing hover range: %w", err) 41 } 42 43 // Confirm that the cursor is at the position of a require statement. 44 var req *modfile.Require 45 var startPos, endPos int 46 for _, r := range pm.File.Require { 47 dep := []byte(r.Mod.Path) 48 s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte 49 i := bytes.Index(pm.Mapper.Content[s:e], dep) 50 if i == -1 { 51 continue 52 } 53 // Shift the start position to the location of the 54 // dependency within the require statement. 55 startPos, endPos = s+i, s+i+len(dep) 56 if token.Pos(startPos) <= hoverRng.Start && hoverRng.Start <= token.Pos(endPos) { 57 req = r 58 break 59 } 60 } 61 62 // The cursor position is not on a require statement. 63 if req == nil { 64 return nil, nil 65 } 66 67 // Get the `go mod why` results for the given file. 68 why, err := snapshot.ModWhy(ctx, fh) 69 if err != nil { 70 return nil, err 71 } 72 explanation, ok := why[req.Mod.Path] 73 if !ok { 74 return nil, nil 75 } 76 77 // Get the range to highlight for the hover. 78 line, col, err := pm.Mapper.Converter.ToPosition(startPos) 79 if err != nil { 80 return nil, err 81 } 82 start := span.NewPoint(line, col, startPos) 83 84 line, col, err = pm.Mapper.Converter.ToPosition(endPos) 85 if err != nil { 86 return nil, err 87 } 88 end := span.NewPoint(line, col, endPos) 89 90 spn = span.New(fh.URI(), start, end) 91 rng, err := pm.Mapper.Range(spn) 92 if err != nil { 93 return nil, err 94 } 95 options := snapshot.View().Options() 96 isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path) 97 explanation = formatExplanation(explanation, req, options, isPrivate) 98 return &protocol.Hover{ 99 Contents: protocol.MarkupContent{ 100 Kind: options.PreferredContentFormat, 101 Value: explanation, 102 }, 103 Range: rng, 104 }, nil 105 } 106 107 func formatExplanation(text string, req *modfile.Require, options source.Options, isPrivate bool) string { 108 text = strings.TrimSuffix(text, "\n") 109 splt := strings.Split(text, "\n") 110 length := len(splt) 111 112 var b strings.Builder 113 // Write the heading as an H3. 114 b.WriteString("##" + splt[0]) 115 if options.PreferredContentFormat == protocol.Markdown { 116 b.WriteString("\n\n") 117 } else { 118 b.WriteRune('\n') 119 } 120 121 // If the explanation is 2 lines, then it is of the form: 122 // # golang.org/x/text/encoding 123 // (main module does not need package golang.org/x/text/encoding) 124 if length == 2 { 125 b.WriteString(splt[1]) 126 return b.String() 127 } 128 129 imp := splt[length-1] // import path 130 reference := imp 131 // See golang/go#36998: don't link to modules matching GOPRIVATE. 132 if !isPrivate && options.PreferredContentFormat == protocol.Markdown { 133 target := imp 134 if strings.ToLower(options.LinkTarget) == "pkg.go.dev" { 135 target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1) 136 } 137 reference = fmt.Sprintf("[%s](https://%s/%s)", imp, options.LinkTarget, target) 138 } 139 b.WriteString("This module is necessary because " + reference + " is imported in") 140 141 // If the explanation is 3 lines, then it is of the form: 142 // # github.com/april1989/origin-go-tools 143 // modtest 144 // github.com/april1989/origin-go-tools/go/packages 145 if length == 3 { 146 msg := fmt.Sprintf(" `%s`.", splt[1]) 147 b.WriteString(msg) 148 return b.String() 149 } 150 151 // If the explanation is more than 3 lines, then it is of the form: 152 // # golang.org/x/text/language 153 // rsc.io/quote 154 // rsc.io/sampler 155 // golang.org/x/text/language 156 b.WriteString(":\n```text") 157 dash := "" 158 for _, imp := range splt[1 : length-1] { 159 dash += "-" 160 b.WriteString("\n" + dash + " " + imp) 161 } 162 b.WriteString("\n```") 163 return b.String() 164 }