internal/lsp: support comments on hover for typenames, funcs, fields

This change adds support for showing documentation when hovering over any
named type or function. For now, we show the entire comment associated
with the type; in future CLs, we should refine our approach and perhaps
only show the first line or sentence.

Updates golang/go#29151

Change-Id: Ib33284747b19acba67d79fb55c916574c3dd8073
Reviewed-on: https://go-review.googlesource.com/c/tools/+/172958
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2019-04-19 13:09:33 -04:00
parent aa740d4807
commit 9eb0fb1732
3 changed files with 132 additions and 57 deletions

View File

@ -32,7 +32,7 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio
if err != nil {
return nil, err
}
decl, doc, err := ident.Hover(ctx, nil, s.enhancedHover)
hover, err := ident.Hover(ctx, nil, s.enhancedHover, s.preferredContentFormat == protocol.Markdown)
if err != nil {
return nil, err
}
@ -45,7 +45,10 @@ func (s *Server) hover(ctx context.Context, params *protocol.TextDocumentPositio
return nil, err
}
return &protocol.Hover{
Contents: markupContent(decl, doc, s.preferredContentFormat),
Contents: protocol.MarkupContent{
Kind: s.preferredContentFormat,
Value: hover,
},
Range: &rng,
}, nil
}

View File

@ -14,56 +14,123 @@ import (
"go/types"
)
func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier, enhancedHover bool) (string, string, error) {
// formatter returns the a hover value formatted with its documentation.
type formatter func(interface{}, *ast.CommentGroup) (string, error)
func (i *IdentifierInfo) Hover(ctx context.Context, qf types.Qualifier, enhancedHover, markdownSupported bool) (string, error) {
file := i.File.GetAST(ctx)
if q == nil {
if qf == nil {
pkg := i.File.GetPackage(ctx)
q = qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
}
// TODO(rstambler): Remove this configuration when hover behavior is stable.
if enhancedHover {
switch obj := i.Declaration.Object.(type) {
case *types.TypeName:
if node, ok := i.Declaration.Node.(*ast.GenDecl); ok {
if decl, doc, err := formatTypeName(i.File.GetFileSet(ctx), node, obj, q); err == nil {
return decl, doc, nil
} else {
// Swallow errors so we can return a best-effort response using types.TypeString.
i.File.View().Logger().Errorf(ctx, "no hover for TypeName %v: %v", obj.Name(), err)
}
}
return types.TypeString(obj.Type(), q), "", nil
default:
return types.ObjectString(obj, q), "", nil
}
}
return types.ObjectString(i.Declaration.Object, q), "", nil
}
func formatTypeName(fset *token.FileSet, decl *ast.GenDecl, obj *types.TypeName, q types.Qualifier) (string, string, error) {
if types.IsInterface(obj.Type()) {
return "", "", fmt.Errorf("no support for interfaces yet")
}
switch t := obj.Type().(type) {
case *types.Struct:
return formatStructType(fset, decl, t)
case *types.Named:
if under, ok := t.Underlying().(*types.Struct); ok {
return formatStructType(fset, decl, under)
}
}
return "", "", fmt.Errorf("no supported for %v, which is of type %T", obj.Name(), obj.Type())
}
func formatStructType(fset *token.FileSet, decl *ast.GenDecl, typ *types.Struct) (string, string, error) {
if len(decl.Specs) != 1 {
return "", "", fmt.Errorf("expected 1 TypeSpec got %v", len(decl.Specs))
qf = qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
}
b := bytes.NewBuffer(nil)
if err := format.Node(b, fset, decl.Specs[0]); err != nil {
return "", "", err
f := func(x interface{}, c *ast.CommentGroup) (string, error) {
return writeHover(x, i.File.GetFileSet(ctx), b, c, markdownSupported, qf)
}
doc := decl.Doc.Text()
return b.String(), doc, nil
obj := i.Declaration.Object
// TODO(rstambler): Remove this configuration when hover behavior is stable.
if enhancedHover {
switch node := i.Declaration.Node.(type) {
case *ast.GenDecl:
switch obj := obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func:
return formatGenDecl(node, obj, obj.Type(), f)
}
case *ast.FuncDecl:
if _, ok := obj.(*types.Func); ok {
return f(obj, node.Doc)
}
}
}
return f(obj, nil)
}
func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type, f formatter) (string, error) {
if _, ok := typ.(*types.Named); ok {
switch typ.Underlying().(type) {
case *types.Interface, *types.Struct:
return formatGenDecl(node, obj, typ.Underlying(), f)
}
}
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 "", fmt.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, f)
}
// Handle types.
switch spec := spec.(type) {
case *ast.TypeSpec:
// If multiple types are declared in the same block.
if len(node.Specs) > 1 {
return f(spec.Type, spec.Doc)
} else {
return f(spec, node.Doc)
}
case *ast.ValueSpec:
return f(spec, spec.Doc)
case *ast.ImportSpec:
return f(spec, spec.Doc)
}
return "", fmt.Errorf("unable to format spec %v (%T)", spec, spec)
}
func formatVar(node ast.Spec, obj types.Object, f formatter) (string, error) {
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 < fieldList.NumFields(); i++ {
field := fieldList.List[i]
if field.Pos() <= obj.Pos() && obj.Pos() <= field.End() {
if field.Doc.Text() != "" {
return f(obj, field.Doc)
} else if field.Comment.Text() != "" {
return f(obj, field.Comment)
}
}
}
}
// If we weren't able to find documentation for the object.
return f(obj, nil)
}
// writeHover writes the hover for a given node and its documentation.
func writeHover(x interface{}, fset *token.FileSet, b *bytes.Buffer, c *ast.CommentGroup, markdownSupported bool, qf types.Qualifier) (string, error) {
if c != nil {
b.WriteString(c.Text())
b.WriteRune('\n')
}
if markdownSupported {
b.WriteString("```go\n")
}
switch x := x.(type) {
case ast.Node:
if err := format.Node(b, fset, x); err != nil {
return "", err
}
case types.Object:
b.WriteString(types.ObjectString(x, qf))
}
if markdownSupported {
b.WriteString("\n```")
}
return b.String(), nil
}

View File

@ -151,15 +151,20 @@ func objToNode(ctx context.Context, v View, obj types.Object, rng span.Range) (a
if path == nil {
return nil, fmt.Errorf("no path for range %v", rng)
}
// TODO(rstambler): Support other node types.
// For now, we only associate an ast.Node for type declarations.
switch obj.Type().(type) {
case *types.Named, *types.Struct, *types.Interface:
for _, node := range path {
if node, ok := node.(*ast.GenDecl); ok && node.Tok == token.TYPE {
switch node := node.(type) {
case *ast.GenDecl:
// Type names, fields, and methods.
switch obj.(type) {
case *types.TypeName, *types.Var, *types.Const, *types.Func:
return node, nil
}
case *ast.FuncDecl:
// Function signatures.
if _, ok := obj.(*types.Func); ok {
return node, nil
}
}
}
return nil, nil // didn't find a node, but no error
return nil, nil // didn't find a node, but don't fail
}