1
0
mirror of https://github.com/golang/go synced 2024-11-19 00:54:42 -07:00
go/internal/lsp/source/hover.go
Barnaby Keene 69111344d4 internal/lsp: expose godoc or pkg.go.dev link on hover
This adds a link to documentation to the hover contents for the
current symbol if it is exported.

Updates golang/go#34240

Change-Id: I19c66e91e46f79284bfd0006c53f518eda4edef7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/200604
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Heschi Kreinick <heschi@google.com>
2019-12-05 20:53:10 +00:00

202 lines
5.9 KiB
Go

// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package source
import (
"context"
"fmt"
"go/ast"
"go/doc"
"go/format"
"go/types"
"strings"
"golang.org/x/tools/internal/telemetry/trace"
errors "golang.org/x/xerrors"
)
type HoverInformation struct {
// Signature is the symbol's signature.
Signature string `json:"signature"`
// SingleLine is a single line describing the symbol.
// This is recommended only for use in clients that show a single line for hover.
SingleLine string `json:"singleLine"`
// Synopsis is a single sentence synopsis of the symbol's documentation.
Synopsis string `json:"synopsis"`
// FullDocumentation is the symbol's full documentation.
FullDocumentation string `json:"fullDocumentation"`
// pkgPath holds the package path of the hovered symbol.
pkgPath string
// symbolName holds the symbol name without any package prefix.
symbolName string
source interface{}
comment *ast.CommentGroup
}
func (h *HoverInformation) DocumentationLink(options Options) string {
if h.symbolName == "" || h.pkgPath == "" || options.LinkTarget == "" {
return ""
}
return fmt.Sprintf("[%s on %s](https://%s/%s#%s)", h.symbolName, options.LinkTarget, options.LinkTarget, h.pkgPath, h.symbolName)
}
func (i *IdentifierInfo) Hover(ctx context.Context) (*HoverInformation, error) {
ctx, done := trace.StartSpan(ctx, "source.Hover")
defer done()
h, err := i.Declaration.hover(ctx)
if err != nil {
return nil, err
}
// Determine the symbol's signature.
switch x := h.source.(type) {
case ast.Node:
var b strings.Builder
if err := format.Node(&b, i.Snapshot.View().Session().Cache().FileSet(), x); err != nil {
return nil, err
}
h.Signature = b.String()
case types.Object:
h.Signature = objectString(x, i.qf)
}
if obj := i.Declaration.obj; obj != nil {
// Only show the documentation links for symbols in the package scope.
// TODO(https://golang.org/issue/34240): Handle other symbols.
if obj.Exported() && obj.Parent() == obj.Pkg().Scope() {
h.pkgPath = obj.Pkg().Path()
h.symbolName = obj.Name()
}
// Set the documentation.
h.SingleLine = objectString(obj, i.qf)
}
if h.comment != nil {
h.FullDocumentation = h.comment.Text()
h.Synopsis = doc.Synopsis(h.FullDocumentation)
}
return h, nil
}
// objectString is a wrapper around the types.ObjectString function.
// It handles adding more information to the object string.
func objectString(obj types.Object, qf types.Qualifier) string {
str := types.ObjectString(obj, qf)
switch obj := obj.(type) {
case *types.Const:
str = fmt.Sprintf("%s = %s", str, obj.Val())
}
return str
}
func (d Declaration) hover(ctx context.Context) (*HoverInformation, error) {
_, done := trace.StartSpan(ctx, "source.hover")
defer done()
obj := d.obj
switch node := d.node.(type) {
case *ast.ImportSpec:
return &HoverInformation{source: node}, nil
case *ast.GenDecl:
switch obj := obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func:
return formatGenDecl(node, obj, obj.Type())
}
case *ast.TypeSpec:
if obj.Parent() == types.Universe {
if obj.Name() == "error" {
return &HoverInformation{source: node}, nil
}
return &HoverInformation{source: node.Name}, nil // comments not needed for builtins
}
case *ast.FuncDecl:
switch obj.(type) {
case *types.Func:
return &HoverInformation{source: obj, comment: node.Doc}, nil
case *types.Builtin:
return &HoverInformation{source: node.Type, comment: node.Doc}, nil
}
}
return &HoverInformation{source: obj}, nil
}
func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type) (*HoverInformation, error) {
if _, ok := typ.(*types.Named); ok {
switch typ.Underlying().(type) {
case *types.Interface, *types.Struct:
return formatGenDecl(node, obj, typ.Underlying())
}
}
var spec ast.Spec
for _, s := range node.Specs {
if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() {
spec = s
break
}
}
if spec == nil {
return nil, errors.Errorf("no spec for node %v at position %v", node, obj.Pos())
}
// If we have a field or method.
switch obj.(type) {
case *types.Var, *types.Const, *types.Func:
return formatVar(spec, obj, node), nil
}
// Handle types.
switch spec := spec.(type) {
case *ast.TypeSpec:
if len(node.Specs) > 1 {
// If multiple types are declared in the same block.
return &HoverInformation{source: spec.Type, comment: spec.Doc}, nil
} else {
return &HoverInformation{source: spec, comment: node.Doc}, nil
}
case *ast.ValueSpec:
return &HoverInformation{source: spec, comment: spec.Doc}, nil
case *ast.ImportSpec:
return &HoverInformation{source: spec, comment: spec.Doc}, nil
}
return nil, errors.Errorf("unable to format spec %v (%T)", spec, spec)
}
func formatVar(node ast.Spec, obj types.Object, decl *ast.GenDecl) *HoverInformation {
var fieldList *ast.FieldList
if spec, ok := node.(*ast.TypeSpec); ok {
switch t := spec.Type.(type) {
case *ast.StructType:
fieldList = t.Fields
case *ast.InterfaceType:
fieldList = t.Methods
}
}
// If we have a struct or interface declaration,
// we need to match the object to the corresponding field or method.
if fieldList != nil {
for i := 0; i < len(fieldList.List); i++ {
field := fieldList.List[i]
if field.Pos() <= obj.Pos() && obj.Pos() <= field.End() {
if field.Doc.Text() != "" {
return &HoverInformation{source: obj, comment: field.Doc}
} else if field.Comment.Text() != "" {
return &HoverInformation{source: obj, comment: field.Comment}
}
}
}
}
// If we have a package level variable that does have a
// comment group attached to it but not in the ast.spec.
if decl.Doc.Text() != "" {
return &HoverInformation{source: obj, comment: decl.Doc}
}
// If we weren't able to find documentation for the object.
return &HoverInformation{source: obj}
}