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>
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index 6391c3c..7d9ae2c 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -32,7 +32,7 @@
 	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,8 +45,11 @@
 		return nil, err
 	}
 	return &protocol.Hover{
-		Contents: markupContent(decl, doc, s.preferredContentFormat),
-		Range:    &rng,
+		Contents: protocol.MarkupContent{
+			Kind:  s.preferredContentFormat,
+			Value: hover,
+		},
+		Range: &rng,
 	}, nil
 }
 
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
index 9d78224..375b2cb 100644
--- a/internal/lsp/source/hover.go
+++ b/internal/lsp/source/hover.go
@@ -14,56 +14,123 @@
 	"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
 }
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index da25c29..ff75e05 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -151,15 +151,20 @@
 	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 {
+	for _, node := range path {
+		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
 }