internal/lsp: use ast.Nodes for hover information

This change associates an ast.Node for some object declarations.
In this case, we only handle type declarations, but future changes will
support other objects as well. This is the first step in adding
documentation on hover.

Updates golang/go#29151

Change-Id: I39ddccf4130ee3b106725286375cd74bc51bcd38
Reviewed-on: https://go-review.googlesource.com/c/tools/+/172661
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/cache/file.go b/internal/lsp/cache/file.go
index 9efcccb..0c9b0b2 100644
--- a/internal/lsp/cache/file.go
+++ b/internal/lsp/cache/file.go
@@ -40,6 +40,11 @@
 	return f.uris[0]
 }
 
+// View returns the view associated with the file.
+func (f *File) View() source.View {
+	return f.view
+}
+
 // GetContent returns the contents of the file, reading it from file system if needed.
 func (f *File) GetContent(ctx context.Context) []byte {
 	f.view.mu.Lock()
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index b97d46d..b3b4840 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -164,6 +164,10 @@
 	if usePlaceholders, ok := c["usePlaceholders"].(bool); ok {
 		s.usePlaceholders = usePlaceholders
 	}
+	// Check if enhancedHover is enabled.
+	if enhancedHover, ok := c["enhancedHover"].(bool); ok {
+		s.enhancedHover = enhancedHover
+	}
 	return nil
 }
 
diff --git a/internal/lsp/hover.go b/internal/lsp/hover.go
index 8a8e218..6391c3c 100644
--- a/internal/lsp/hover.go
+++ b/internal/lsp/hover.go
@@ -6,6 +6,7 @@
 
 import (
 	"context"
+	"fmt"
 
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
@@ -31,7 +32,7 @@
 	if err != nil {
 		return nil, err
 	}
-	content, err := ident.Hover(ctx, nil)
+	decl, doc, err := ident.Hover(ctx, nil, s.enhancedHover)
 	if err != nil {
 		return nil, err
 	}
@@ -44,20 +45,23 @@
 		return nil, err
 	}
 	return &protocol.Hover{
-		Contents: markupContent(content, s.preferredContentFormat),
+		Contents: markupContent(decl, doc, s.preferredContentFormat),
 		Range:    &rng,
 	}, nil
 }
 
-func markupContent(content string, kind protocol.MarkupKind) protocol.MarkupContent {
+func markupContent(decl, doc string, kind protocol.MarkupKind) protocol.MarkupContent {
 	result := protocol.MarkupContent{
 		Kind: kind,
 	}
 	switch kind {
 	case protocol.PlainText:
-		result.Value = content
+		result.Value = decl
 	case protocol.Markdown:
-		result.Value = "```go\n" + content + "\n```"
+		result.Value = "```go\n" + decl + "\n```"
+	}
+	if doc != "" {
+		result.Value = fmt.Sprintf("%s\n%s", doc, result.Value)
 	}
 	return result
 }
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 54bfd1a..bd466e1 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -74,6 +74,7 @@
 	// Configurations.
 	// TODO(rstambler): Separate these into their own struct?
 	usePlaceholders               bool
+	enhancedHover                 bool
 	insertTextFormat              protocol.InsertTextFormat
 	configurationSupported        bool
 	dynamicConfigurationSupported bool
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
new file mode 100644
index 0000000..9d78224
--- /dev/null
+++ b/internal/lsp/source/hover.go
@@ -0,0 +1,69 @@
+// Copyright 201p 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 (
+	"bytes"
+	"context"
+	"fmt"
+	"go/ast"
+	"go/format"
+	"go/token"
+	"go/types"
+)
+
+func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier, enhancedHover bool) (string, string, error) {
+	file := i.File.GetAST(ctx)
+	if q == 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))
+	}
+	b := bytes.NewBuffer(nil)
+	if err := format.Node(b, fset, decl.Specs[0]); err != nil {
+		return "", "", err
+	}
+	doc := decl.Doc.Text()
+	return b.String(), doc, nil
+
+}
diff --git a/internal/lsp/source/definition.go b/internal/lsp/source/identifier.go
similarity index 79%
rename from internal/lsp/source/definition.go
rename to internal/lsp/source/identifier.go
index 2ee7592..5671f62 100644
--- a/internal/lsp/source/definition.go
+++ b/internal/lsp/source/identifier.go
@@ -26,6 +26,7 @@
 	}
 	Declaration struct {
 		Range  span.Range
+		Node   ast.Decl
 		Object types.Object
 	}
 
@@ -49,15 +50,6 @@
 	return result, err
 }
 
-func (i *IdentifierInfo) Hover(ctx context.Context, q types.Qualifier) (string, error) {
-	if q == nil {
-		fAST := i.File.GetAST(ctx)
-		pkg := i.File.GetPackage(ctx)
-		q = qualifier(fAST, pkg.GetTypes(), pkg.GetTypesInfo())
-	}
-	return types.ObjectString(i.Declaration.Object, q), nil
-}
-
 // identifier checks a single position for a potential identifier.
 func identifier(ctx context.Context, v View, f File, pos token.Pos) (*IdentifierInfo, error) {
 	fAST := f.GetAST(ctx)
@@ -105,6 +97,9 @@
 	if result.Declaration.Range, err = objToRange(ctx, v, result.Declaration.Object); err != nil {
 		return nil, err
 	}
+	if result.Declaration.Node, err = objToNode(ctx, v, result.Declaration.Object, result.Declaration.Range); err != nil {
+		return nil, err
+	}
 	typ := pkg.GetTypesInfo().TypeOf(result.ident)
 	if typ == nil {
 		return nil, fmt.Errorf("no type for %s", result.Name)
@@ -140,3 +135,30 @@
 	}
 	return span.NewRange(v.FileSet(), p, p+token.Pos(len(obj.Name()))), nil
 }
+
+func objToNode(ctx context.Context, v View, obj types.Object, rng span.Range) (ast.Decl, error) {
+	s, err := rng.Span()
+	if err != nil {
+		return nil, err
+	}
+	declFile, err := v.GetFile(ctx, s.URI())
+	if err != nil {
+		return nil, err
+	}
+	declAST := declFile.GetAST(ctx)
+	path, _ := astutil.PathEnclosingInterval(declAST, rng.Start, rng.End)
+	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 {
+				return node, nil
+			}
+		}
+	}
+	return nil, nil // didn't find a node, but no error
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 896ac8e..5975f22 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -34,6 +34,7 @@
 // the loading of packages into their own caching systems.
 type File interface {
 	URI() span.URI
+	View() View
 	GetAST(ctx context.Context) *ast.File
 	GetFileSet(ctx context.Context) *token.FileSet
 	GetPackage(ctx context.Context) Package