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:
parent
aa740d4807
commit
9eb0fb1732
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
doc := decl.Doc.Text()
|
||||
return b.String(), doc, 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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue