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