Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x/tools/gopls: render package documentation when hovering over imported package name identifiers #559

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 64 additions & 14 deletions gopls/internal/golang/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
return protocol.Range{}, nil, nil // no object to hover
}

if pkgName, ok := obj.(*types.PkgName); ok {
rng, hoverRes, err := hoverPackageIdent(ctx, snapshot, pkg, pgf, ident, pkgName.Imported().Path())
if err != nil {
return protocol.Range{}, nil, err
}
if hoverRange == nil {
hoverRange = &rng
}
return *hoverRange, hoverRes, nil // (hoverRes may be nil)
}

// Unless otherwise specified, rng covers the ident being hovered.
if hoverRange == nil {
rng, err := pgf.NodeRange(ident)
Expand Down Expand Up @@ -692,27 +703,22 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec
}, nil
}

// hoverImport computes hover information when hovering over the import path of
// imp in the file pgf of pkg.
// hoverPackageRef computes hover information when hovering over the import path or ident of
// imp in the file pgf of pkg or over the identifier for an imported pkg.
//
// If we do not have metadata for the hovered import, it returns _
func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverResult, error) {
rng, err := pgf.NodeRange(imp.Path)
if err != nil {
return protocol.Range{}, nil, err
}

func hoverPackageRef(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, imp *ast.ImportSpec) (*hoverResult, error) {
importPath := metadata.UnquoteImportPath(imp)
if importPath == "" {
return protocol.Range{}, nil, fmt.Errorf("invalid import path")
return nil, fmt.Errorf("invalid import path")
}
impID := pkg.Metadata().DepsByImpPath[importPath]
if impID == "" {
return protocol.Range{}, nil, fmt.Errorf("no package data for import %q", importPath)
return nil, fmt.Errorf("no package data for import %q", importPath)
}
impMetadata := snapshot.Metadata(impID)
if impMetadata == nil {
return protocol.Range{}, nil, bug.Errorf("failed to resolve import ID %q", impID)
return nil, bug.Errorf("failed to resolve import ID %q", impID)
}

// Find the first file with a package doc comment.
Expand All @@ -721,14 +727,14 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa
fh, err := snapshot.ReadFile(ctx, f)
if err != nil {
if ctx.Err() != nil {
return protocol.Range{}, nil, ctx.Err()
return nil, ctx.Err()
}
continue
}
pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header)
if err != nil {
if ctx.Err() != nil {
return protocol.Range{}, nil, ctx.Err()
return nil, ctx.Err()
}
continue
}
Expand All @@ -739,13 +745,57 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa
}

docText := comment.Text()
return rng, &hoverResult{
return &hoverResult{
signature: "package " + string(impMetadata.Name),
synopsis: doc.Synopsis(docText),
fullDocumentation: docText,
}, nil
}

// hoverImport computes hover information when hovering over the import path of
// imp in the file pgf of pkg.
//
// If we do not have metadata for the hovered import, it returns _
func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverResult, error) {
rng, err := pgf.NodeRange(imp.Path)
if err != nil {
return protocol.Range{}, nil, err
}
hoverRes, err := hoverPackageRef(ctx, snapshot, pkg, imp)
if err != nil {
return protocol.Range{}, nil, err
}
return rng, hoverRes, err
}

// hoverPackageIdent computes hover information when hovering over the identifier
// of an imported pkg.
//
// If we do not have metadata for the hovered import, it returns _
func hoverPackageIdent(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, ident *ast.Ident, path string) (protocol.Range, *hoverResult, error) {

for _, spec := range pgf.File.Imports {
importPathString, err := strconv.Unquote(spec.Path.Value)
if err != nil {
return protocol.Range{}, nil, err
}
if importPathString != path {
continue
}
rng, err := pgf.NodeRange(ident)
if err != nil {
return protocol.Range{}, nil, err
}
hoverRes, err := hoverPackageRef(ctx, snapshot, pkg, spec)
if err != nil {
return protocol.Range{}, nil, err
}
return rng, hoverRes, nil // (hoverRes may be nil)
}

return protocol.Range{}, nil, fmt.Errorf("invalid import path")
}

// hoverPackageName computes hover information for the package name of the file
// pgf in pkg.
func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverResult, error) {
Expand Down
88 changes: 88 additions & 0 deletions gopls/internal/test/integration/misc/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,94 @@ func main() {
})
}

func TestHoverPackageIdent(t *testing.T) {
const packageDoc1 = "Package lib1 hover documentation"
const packageDoc2 = "Package lib2 hover documentation"
tests := []struct {
hoverIdent string
want string
wantError bool
}{
{
"lib1",
packageDoc1,
false,
},
{
"lib2",
packageDoc2,
false,
},
{
"lib3",
"",
false,
},
{
"lib4",
"",
true,
},
}
source := fmt.Sprintf(`
-- go.mod --
module mod.com

go 1.12
-- lib1/a.go --
// %s
package lib1

const C = 1

-- lib1/b.go --
package lib1

const D = 1

-- lib2/a.go --
// %s
package lib2

const E = 1

-- lib3/a.go --
package lib3

const F = 1

-- main.go --
package main

import (
"mod.com/lib1"
"mod.com/lib2"
"mod.com/lib3"
"mod.com/lib4"
)

func main() {
println(lib1.C)
println(lib2.E)
println(lib3.F)
println(lib4.Z)
}
`, packageDoc1, packageDoc2)
Run(t, source, func(t *testing.T, env *Env) {
env.OpenFile("main.go")
for _, test := range tests {
got, _, err := env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "("+test.hoverIdent+")\\."))
if test.wantError {
if err == nil {
t.Errorf("Hover(%q) succeeded unexpectedly", test.hoverIdent)
}
} else if !strings.Contains(got.Value, test.want) {
t.Errorf("Hover(%q): got:\n%q\nwant:\n%q", test.hoverIdent, got.Value, test.want)
}
}
})
}

// for x/tools/gopls: unhandled named anchor on the hover #57048
func TestHoverTags(t *testing.T) {
const source = `
Expand Down
4 changes: 3 additions & 1 deletion gopls/internal/test/integration/misc/link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ module import.test

go 1.12
-- import.test@v1.2.3/pkg/const.go --
// package documentation
package pkg

const Hello = "Hello"
Expand All @@ -49,10 +50,11 @@ const Hello = "Hello"

modLink := "https://pkg.go.dev/mod/import.test@v1.2.3"
pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg"
pkgDoc := "package documentation"

// First, check that we get the expected links via hover and documentLink.
content, _ := env.Hover(env.RegexpSearch("main.go", "pkg.Hello"))
if content == nil || !strings.Contains(content.Value, pkgLink) {
if content == nil || !strings.Contains(content.Value, pkgDoc) {
t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink)
}
content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test"))
Expand Down