1
0
mirror of https://github.com/golang/go synced 2024-09-30 20:18:33 -06:00
go/internal/lsp/mod/hover.go
Rebecca Stambler 1745ac5bc6 internal/lsp: refactor various module-specific handles in cache
This change separates out different functions of mod handles.
Previously, we had ModHandle and ModTidyHandle. ModHandle was used to
parse go.mod files and get the results of `go mod why` and possible
dependency upgrades.

Now, we factor this out into 4 handles: ParseModHandle, ModWhyHandle,
ModUpgradeHandle, and ModTidyHandle. This allows each handle to be
specific to its own functionality. It also simplifies the code a bit,
as the handles can be written in terms of ParseModHandles instead of
FileHandles.

I may have some follow-up CLs to refactor the `go mod tidy` logic out of
the cache package, though I'm no longer certain that that's a good
choice.

Change-Id: I8e12299dfdda7bb61b05903d9aa474461d7f4836
Reviewed-on: https://go-review.googlesource.com/c/tools/+/239117
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
2020-06-23 18:31:46 +00:00

175 lines
4.5 KiB
Go

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"
)
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.
pmh, err := snapshot.ParseModHandle(ctx, fh)
if err != nil {
return nil, fmt.Errorf("getting modfile handle: %w", err)
}
file, m, _, err := pmh.Parse(ctx)
if err != nil {
return nil, err
}
spn, err := m.PointSpan(position)
if err != nil {
return nil, fmt.Errorf("computing cursor position: %w", err)
}
hoverRng, err := spn.Range(m.Converter)
if err != nil {
return nil, fmt.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 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
}
}
// 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.
mwh, err := snapshot.ModWhyHandle(ctx)
if err != nil {
return nil, err
}
why, err := mwh.Why(ctx)
if err != nil {
return nil, fmt.Errorf("running go mod why: %w", err)
}
if 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.URI(), start, end)
rng, err := m.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()
}