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