diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go index 447cf0c883..d730d6ed5c 100644 --- a/internal/lsp/cache/mod.go +++ b/internal/lsp/cache/mod.go @@ -78,6 +78,10 @@ type modData struct { // upgrades is a map of path->version that contains any upgrades for the go.mod. upgrades map[string]string + // why is a map of path->explanation that contains all the "go mod why" contents + // for each require statement. + why map[string]string + // parseErrors are the errors that arise when we diff between a user's go.mod // and the "tidied" go.mod. parseErrors []source.Error @@ -112,6 +116,15 @@ func (mh *modHandle) Upgrades(ctx context.Context) (*modfile.File, *protocol.Col return data.origParsedFile, data.origMapper, data.upgrades, data.err } +func (mh *modHandle) Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) { + v := mh.handle.Get(ctx) + if v == nil { + return nil, nil, nil, errors.Errorf("no parsed file for %s", mh.File().Identity().URI) + } + data := v.(*modData) + return data.origParsedFile, data.origMapper, data.why, data.err +} + func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.ModHandle { uri := fh.Identity().URI if handle := s.getModHandle(uri); handle != nil { @@ -165,7 +178,15 @@ func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.M } } // Only get dependency upgrades if the go.mod file is the same as the view's. - data.upgrades, data.err = dependencyUpgrades(ctx, cfg, folder, data) + if err := dependencyUpgrades(ctx, cfg, folder, data); err != nil { + data.err = err + return data + } + // Only run "go mod why" if the go.mod file is the same as the view's. + if err := goModWhy(ctx, cfg, folder, data); err != nil { + data.err = err + return data + } return data }) s.mu.Lock() @@ -178,9 +199,39 @@ func (s *snapshot) ModHandle(ctx context.Context, fh source.FileHandle) source.M return s.modHandles[uri] } -func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, data *modData) (map[string]string, error) { +func goModWhy(ctx context.Context, cfg *packages.Config, folder string, data *modData) error { if len(data.origParsedFile.Require) == 0 { - return nil, nil + return nil + } + // Run "go mod why" on all the dependencies to get information about the usages. + inv := gocommand.Invocation{ + Verb: "mod", + Args: []string{"why", "-m"}, + BuildFlags: cfg.BuildFlags, + Env: cfg.Env, + WorkingDir: folder, + } + for _, req := range data.origParsedFile.Require { + inv.Args = append(inv.Args, req.Mod.Path) + } + stdout, err := inv.Run(ctx) + if err != nil { + return err + } + whyList := strings.Split(stdout.String(), "\n\n") + if len(whyList) <= 1 || len(whyList) > len(data.origParsedFile.Require) { + return nil + } + data.why = make(map[string]string) + for i, req := range data.origParsedFile.Require { + data.why[req.Mod.Path] = whyList[i] + } + return nil +} + +func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string, data *modData) error { + if len(data.origParsedFile.Require) == 0 { + return nil } // Run "go list -u -m all" to be able to see which deps can be upgraded. inv := gocommand.Invocation{ @@ -192,13 +243,13 @@ func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string } stdout, err := inv.Run(ctx) if err != nil { - return nil, err + return err } upgradesList := strings.Split(stdout.String(), "\n") if len(upgradesList) <= 1 { - return nil, nil + return nil } - upgrades := make(map[string]string) + data.upgrades = make(map[string]string) for _, upgrade := range upgradesList[1:] { // Example: "github.com/x/tools v1.1.0 [v1.2.0]" info := strings.Split(upgrade, " ") @@ -208,9 +259,9 @@ func dependencyUpgrades(ctx context.Context, cfg *packages.Config, folder string dep, version := info[0], info[2] latest := version[1:] // remove the "[" latest = strings.TrimSuffix(latest, "]") // remove the "]" - upgrades[dep] = latest + data.upgrades[dep] = latest } - return upgrades, nil + return nil } func (mh *modHandle) Tidy(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]*modfile.Require, []source.Error, error) { diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go index 842c72fa5a..32af7e217c 100644 --- a/internal/lsp/hover.go +++ b/internal/lsp/hover.go @@ -7,36 +7,21 @@ package lsp import ( "context" + "golang.org/x/tools/internal/lsp/mod" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { - snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go) + snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.UnknownKind) if !ok { return nil, err } - ident, err := source.Identifier(ctx, snapshot, fh, params.Position) - if err != nil { - return nil, nil + switch fh.Identity().Kind { + case source.Mod: + return mod.Hover(ctx, snapshot, fh, params.Position) + case source.Go: + return source.Hover(ctx, snapshot, fh, params.Position) } - h, err := ident.Hover(ctx) - if err != nil { - return nil, err - } - rng, err := ident.Range() - if err != nil { - return nil, err - } - hover, err := source.FormatHover(h, snapshot.View().Options()) - if err != nil { - return nil, err - } - return &protocol.Hover{ - Contents: protocol.MarkupContent{ - Kind: snapshot.View().Options().PreferredContentFormat, - Value: hover, - }, - Range: rng, - }, nil + return nil, nil } diff --git a/internal/lsp/mod/hover.go b/internal/lsp/mod/hover.go new file mode 100644 index 0000000000..a6a79b90b1 --- /dev/null +++ b/internal/lsp/mod/hover.go @@ -0,0 +1,149 @@ +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/trace" +) + +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 := trace.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() +} diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go index 3425bf9331..2135506141 100644 --- a/internal/lsp/source/hover.go +++ b/internal/lsp/source/hover.go @@ -44,6 +44,32 @@ type HoverInformation struct { comment *ast.CommentGroup } +func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { + ident, err := Identifier(ctx, snapshot, fh, position) + if err != nil { + return nil, nil + } + h, err := ident.Hover(ctx) + if err != nil { + return nil, err + } + rng, err := ident.Range() + if err != nil { + return nil, err + } + hover, err := FormatHover(h, snapshot.View().Options()) + if err != nil { + return nil, err + } + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: snapshot.View().Options().PreferredContentFormat, + Value: hover, + }, + Range: rng, + }, nil +} + func (i *IdentifierInfo) Hover(ctx context.Context) (*HoverInformation, error) { ctx, done := trace.StartSpan(ctx, "source.Hover") defer done() diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go index ede4665a90..0c0f5f50dd 100644 --- a/internal/lsp/source/view.go +++ b/internal/lsp/source/view.go @@ -271,6 +271,11 @@ type ModHandle interface { // for the go.mod file. Note that this will only work if the go.mod is the view's go.mod. // If the file is not available, returns nil and an error. Upgrades(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) + + // Why returns the parsed modfile, a mapper, and any explanations why a dependency should be + // in the go.mod file. Note that this will only work if the go.mod is the view's go.mod. + // If the file is not available, returns nil and an error. + Why(ctx context.Context) (*modfile.File, *protocol.ColumnMapper, map[string]string, error) } // ModTidyHandle represents a handle to the modfile for the view.