Skip to content

Commit

Permalink
gopls/internal/golang: show package attributes on hover
Browse files Browse the repository at this point in the history
When hovering over a package name in a package declaration, show
language version and GODEBUG values that differ from the toolchain
default. Also, for consistency with other hover content, show the
package declaration as the 'signature' of both a package name and an
import.

Finally, to help differentiate this information, introduce hlines in
the hover output between logical sections. This is similar to what is
done by other LSP servers such as tsserver.

Fixes golang/go#68900

Change-Id: I5013bb9fb4086c71cc3565fd67993764cad69237
Reviewed-on: https://go-review.googlesource.com/c/tools/+/626276
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
  • Loading branch information
findleyr authored and gopherbot committed Nov 8, 2024
1 parent 8a0e08f commit 777f155
Show file tree
Hide file tree
Showing 20 changed files with 480 additions and 55 deletions.
13 changes: 9 additions & 4 deletions gopls/doc/release/v0.17.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,16 @@ which allows editors to request diagnostics directly from gopls using a
`textDocument/publishDiagnostics` notification. This feature is off by default
until the performance of pull diagnostics is comparable to push diagnostics.

## Standard library version information in Hover
## Hover improvements

Hovering over a standard library symbol now displays information about the first
Go release containing the symbol. For example, hovering over `errors.As` shows
"Added in go1.13".
The `textDocument/hover` response has slightly tweaked markdown rendering, and
includes the following additional information:

- Hovering over a standard library symbol now displays information about the
first Go release containing the symbol. For example, hovering over
`errors.As` shows "Added in go1.13".
- Hovering over the package name in a package declaration includes additional
package metadata.

## Semantic token modifiers of top-level constructor of types

Expand Down
153 changes: 103 additions & 50 deletions gopls/internal/golang/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"go/format"
"go/token"
"go/types"
"go/version"
"io/fs"
"path/filepath"
"sort"
Expand Down Expand Up @@ -80,10 +81,6 @@ type hoverJSON struct {
// For example, the "Node" part of "pkg.go.dev/go/ast#Node".
LinkAnchor string `json:"linkAnchor"`

// stdVersion is the Go release version at which this symbol became available.
// It is nil for non-std library.
stdVersion *stdlib.Version

// New fields go below, and are unexported. The existing
// exported fields are underspecified and have already
// constrained our movements too much. A detailed JSON
Expand All @@ -103,6 +100,10 @@ type hoverJSON struct {
// fields of a (struct) type that were promoted through an
// embedded field.
promotedFields string

// footer is additional content to insert at the bottom of the hover
// documentation, before the pkgdoc link.
footer string
}

// Hover implements the "textDocument/hover" RPC for Go files.
Expand Down Expand Up @@ -602,9 +603,9 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
}

var version *stdlib.Version
if symbol := StdSymbolOf(obj); symbol != nil {
version = &symbol.Version
var footer string
if sym := StdSymbolOf(obj); sym != nil && sym.Version > 0 {
footer = fmt.Sprintf("Added in %v", sym.Version)
}

return *hoverRange, &hoverJSON{
Expand All @@ -618,7 +619,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
typeDecl: typeDecl,
methods: methods,
promotedFields: fields,
stdVersion: version,
footer: footer,
}, nil
}

Expand Down Expand Up @@ -733,6 +734,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa

docText := comment.Text()
return rng, &hoverJSON{
Signature: "package " + string(impMetadata.Name),
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
}, nil
Expand All @@ -753,11 +755,47 @@ func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *h
return protocol.Range{}, nil, err
}
docText := comment.Text()

// List some package attributes at the bottom of the documentation, if
// applicable.
type attr struct{ title, value string }
var attrs []attr

if !metadata.IsCommandLineArguments(pkg.Metadata().ID) {
attrs = append(attrs, attr{"Package path", string(pkg.Metadata().PkgPath)})
}

if pkg.Metadata().Module != nil {
attrs = append(attrs, attr{"Module", pkg.Metadata().Module.Path})
}

// Show the effective language version for this package.
if v := pkg.TypesInfo().FileVersions[pgf.File]; v != "" {
attr := attr{value: version.Lang(v)}
if v == pkg.Types().GoVersion() {
attr.title = "Language version"
} else {
attr.title = "Language version (current file)"
}
attrs = append(attrs, attr)
}

// TODO(rfindley): consider exec'ing go here to compute DefaultGODEBUG, or
// propose adding GODEBUG info to go/packages.

var footer string
for i, attr := range attrs {
if i > 0 {
footer += "\n"
}
footer += fmt.Sprintf(" - %s: %s", attr.title, attr.value)
}

return rng, &hoverJSON{
Signature: "package " + string(pkg.Metadata().Name),
Synopsis: doc.Synopsis(docText),
FullDocumentation: docText,
// Note: including a signature is redundant, since the cursor is already on the
// package name.
footer: footer,
}, nil
}

Expand Down Expand Up @@ -1149,8 +1187,9 @@ func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSe

// If pkgURL is non-nil, it should be used to generate doc links.
func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) (string, error) {
maybeMarkdown := func(s string) string {
if s != "" && options.PreferredContentFormat == protocol.Markdown {
markdown := options.PreferredContentFormat == protocol.Markdown
maybeFenced := func(s string) string {
if s != "" && markdown {
s = fmt.Sprintf("```go\n%s\n```", strings.Trim(s, "\n"))
}
return s
Expand All @@ -1161,7 +1200,7 @@ func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path Packa
return h.SingleLine, nil

case settings.NoDocumentation:
return maybeMarkdown(h.Signature), nil
return maybeFenced(h.Signature), nil

case settings.Structured:
b, err := json.Marshal(h)
Expand All @@ -1170,42 +1209,70 @@ func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path Packa
}
return string(b), nil

case settings.SynopsisDocumentation,
settings.FullDocumentation:
case settings.SynopsisDocumentation, settings.FullDocumentation:
var sections [][]string // assembled below

// Signature section.
//
// For types, we display TypeDecl and Methods,
// but not Signature, which is redundant (= TypeDecl + "\n" + Methods).
// For all other symbols, we display Signature;
// TypeDecl and Methods are empty.
// (This awkwardness is to preserve JSON compatibility.)
parts := []string{
maybeMarkdown(h.Signature),
maybeMarkdown(h.typeDecl),
formatDoc(h, options),
maybeMarkdown(h.promotedFields),
maybeMarkdown(h.methods),
fmt.Sprintf("Added in %v", h.stdVersion),
formatLink(h, options, pkgURL),
}
if h.typeDecl != "" {
parts[0] = "" // type: suppress redundant Signature
sections = append(sections, []string{maybeFenced(h.typeDecl)})
} else {
sections = append(sections, []string{maybeFenced(h.Signature)})
}

// Doc section.
var doc string
switch options.HoverKind {
case settings.SynopsisDocumentation:
doc = h.Synopsis
case settings.FullDocumentation:
doc = h.FullDocumentation
}
if h.stdVersion == nil || *h.stdVersion == stdlib.Version(0) {
parts[5] = "" // suppress stdlib version if not applicable or initial version 1.0
if options.PreferredContentFormat == protocol.Markdown {
doc = DocCommentToMarkdown(doc, options)
}
sections = append(sections, []string{
doc,
maybeFenced(h.promotedFields),
maybeFenced(h.methods),
})

// Footer section.
sections = append(sections, []string{
h.footer,
formatLink(h, options, pkgURL),
})

var b strings.Builder
for _, part := range parts {
if part == "" {
continue
newline := func() {
if options.PreferredContentFormat == protocol.Markdown {
b.WriteString("\n\n")
} else {
b.WriteByte('\n')
}
if b.Len() > 0 {
if options.PreferredContentFormat == protocol.Markdown {
b.WriteString("\n\n")
} else {
b.WriteByte('\n')
}
for _, section := range sections {
start := b.Len()
for _, part := range section {
if part == "" {
continue
}
// When markdown is a available, insert an hline before the start of
// the section, if there is content above.
if markdown && b.Len() == start && start > 0 {
newline()
b.WriteString("---")
}
if b.Len() > 0 {
newline()
}
b.WriteString(part)
}
b.WriteString(part)
}
return b.String(), nil

Expand Down Expand Up @@ -1313,20 +1380,6 @@ func formatLink(h *hoverJSON, options *settings.Options, pkgURL func(path Packag
}
}

func formatDoc(h *hoverJSON, options *settings.Options) string {
var doc string
switch options.HoverKind {
case settings.SynopsisDocumentation:
doc = h.Synopsis
case settings.FullDocumentation:
doc = h.FullDocumentation
}
if options.PreferredContentFormat == protocol.Markdown {
return DocCommentToMarkdown(doc, options)
}
return doc
}

// findDeclInfo returns the syntax nodes involved in the declaration of the
// types.Object with position pos, searching the given list of file syntax
// trees.
Expand Down
4 changes: 4 additions & 0 deletions gopls/internal/test/marker/testdata/definition/cgo.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func _() {
func Example()
```

---

[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example)
-- usecgo/usecgo.go --
package cgoimport
Expand All @@ -59,4 +61,6 @@ func _() {
func cgo.Example()
```

---

[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example)
Loading

0 comments on commit 777f155

Please sign in to comment.