internal/fetch/dochtml: render shortened func signatures in side nav

This change introduces a new method on Renderer, ShortSynopsis. It
is meant to be used in templates when rendering methods and functions
within the side navigation UI component.

Due to the limited horizontal space available, it omits the “func”
keyword, type information, and return values.

An example of functions and methods from the time package rendered
using this new method is below.

Functions
    After(d)
    Sleep(d)
    Tick(d)
Types
    type Duration
        ParseDuration(s)
        Since(t)
        Until(t)
        (d) Hours()
        (d) Microseconds()
        (d) Milliseconds()
        (d) Minutes()
        (d) Nanoseconds()
        (d) Round(m)
        (d) Seconds()
        (d) String()
        (d) Truncate(m)
    type Location
        FixedZone(name, offset)
        LoadLocation(name)
        LoadLocationFromTZData(name, data)
        (l) String()
    type Month
        (m) String()
    type ParseError
        (e) Error()

Updates b/148095016

Change-Id: I663eaafdc0baa3619e449ccd0d8ee8d601974392
Reviewed-on: https://team-review.git.corp.google.com/c/golang/discovery/+/763562
CI-Result: Cloud Build <devtools-proctor-result-processor@system.gserviceaccount.com>
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/internal/fetch/dochtml/dochtml.go b/internal/fetch/dochtml/dochtml.go
index dd1806b..117f069 100644
--- a/internal/fetch/dochtml/dochtml.go
+++ b/internal/fetch/dochtml/dochtml.go
@@ -100,12 +100,13 @@
 		Remain: opt.Limit,
 	}
 	err := template.Must(htmlPackage.Clone()).Funcs(map[string]interface{}{
-		"render_synopsis": r.Synopsis,
-		"render_doc":      r.DocHTML,
-		"render_decl":     r.DeclHTML,
-		"render_code":     r.CodeHTML,
-		"source_link":     sourceLink,
-		"play_url":        playURLFunc,
+		"render_short_synopsis": r.ShortSynopsis,
+		"render_synopsis":       r.Synopsis,
+		"render_doc":            r.DocHTML,
+		"render_decl":           r.DeclHTML,
+		"render_code":           r.CodeHTML,
+		"source_link":           sourceLink,
+		"play_url":              playURLFunc,
 	}).Execute(buf, struct {
 		RootURL string
 		*doc.Package
diff --git a/internal/fetch/dochtml/internal/render/render.go b/internal/fetch/dochtml/internal/render/render.go
index c1670cb..7c24f6c 100644
--- a/internal/fetch/dochtml/internal/render/render.go
+++ b/internal/fetch/dochtml/internal/render/render.go
@@ -92,10 +92,18 @@
 	}
 }
 
+const maxSynopsisNodeDepth = 10
+
+// ShortSynopsis returns a very short, one-line summary of the given input node.
+// It currently only supports *ast.FuncDecl nodes and will return a non-nil
+// error otherwise.
+func (r *Renderer) ShortSynopsis(n ast.Node) (string, error) {
+	return shortOneLineNodeDepth(r.fset, n, 0)
+}
+
 // Synopsis returns a one-line summary of the given input node.
 func (r *Renderer) Synopsis(n ast.Node) string {
-	const maxDepth = 10
-	return oneLineNodeDepth(r.fset, n, maxDepth)
+	return oneLineNodeDepth(r.fset, n, 0)
 }
 
 // DocHTML formats documentation text as HTML.
diff --git a/internal/fetch/dochtml/internal/render/short_synopsis.go b/internal/fetch/dochtml/internal/render/short_synopsis.go
new file mode 100644
index 0000000..e364cd9
--- /dev/null
+++ b/internal/fetch/dochtml/internal/render/short_synopsis.go
@@ -0,0 +1,89 @@
+// Copyright 2020 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 render
+
+import (
+	"fmt"
+	"go/ast"
+	"go/token"
+	"strings"
+)
+
+// shortOneLineNodeDepth returns a heavily-truncated, one-line summary of the
+// given node. It will only accept an *ast.FuncDecl when not called recursively.
+// The depth specifies the current depth when traversing the AST and the
+// function will stop traversing once it reaches maxSynopsisNodeDepth.
+func shortOneLineNodeDepth(fset *token.FileSet, node ast.Node, depth int) (string, error) {
+	if _, ok := node.(*ast.FuncDecl); !ok && depth == 0 {
+		return "", fmt.Errorf("only *ast.FuncDecl nodes are supported at top level")
+	}
+
+	const dotDotDot = "..."
+	if depth == maxSynopsisNodeDepth {
+		return dotDotDot, nil
+	}
+	depth++
+
+	switch n := node.(type) {
+	case *ast.FuncDecl:
+		// Formats func declarations.
+		name := n.Name.Name
+		recv, err := shortOneLineNodeDepth(fset, n.Recv, depth)
+		if err != nil {
+			return "", err
+		}
+		if len(recv) > 0 {
+			recv = "(" + recv + ") "
+		}
+		fnc, err := shortOneLineNodeDepth(fset, n.Type, depth)
+		if err != nil {
+			return "", err
+		}
+		fnc = strings.TrimPrefix(fnc, "func")
+		return recv + name + fnc, nil
+
+	case *ast.FuncType:
+		var params []string
+		if n.Params != nil {
+			for _, field := range n.Params.List {
+				f, err := shortOneLineField(fset, field, depth)
+				if err != nil {
+					return "", err
+				}
+				params = append(params, f)
+			}
+		}
+		return fmt.Sprintf("func(%s)", joinStrings(params)), nil
+
+	case *ast.FieldList:
+		if n == nil || len(n.List) == 0 {
+			return "", nil
+		}
+		if len(n.List) == 1 {
+			return shortOneLineField(fset, n.List[0], depth)
+		}
+		return dotDotDot, nil
+
+	default:
+		return "", nil
+	}
+}
+
+// shortOneLineField returns a heavily-truncated, one-line summary of the field.
+// Notably, it omits the field types in its result.
+func shortOneLineField(fset *token.FileSet, field *ast.Field, depth int) (string, error) {
+	if len(field.Names) == 0 {
+		return shortOneLineNodeDepth(fset, field.Type, depth)
+	}
+	var names []string
+	for _, name := range field.Names {
+		names = append(names, name.Name)
+	}
+	s, err := shortOneLineNodeDepth(fset, field.Type, depth)
+	if err != nil {
+		return "", err
+	}
+	return joinStrings(names) + s, nil
+}
diff --git a/internal/fetch/dochtml/internal/render/short_synopsis_test.go b/internal/fetch/dochtml/internal/render/short_synopsis_test.go
new file mode 100644
index 0000000..2e6a2a3
--- /dev/null
+++ b/internal/fetch/dochtml/internal/render/short_synopsis_test.go
@@ -0,0 +1,71 @@
+// Copyright 2020 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 render
+
+import (
+	"go/parser"
+	"go/token"
+	"testing"
+)
+
+func TestShortOneLineNode(t *testing.T) {
+	src := `
+		package insane
+
+		func (p *private) Method1() string { return "" }
+
+		func Foo(ctx Context, s struct {
+			Fizz struct {
+				Field int
+			}
+			Buzz interface {
+				Method() int
+			}
+		}) (_ private) {
+			return
+		}
+
+		func (s *Struct2) Method() {}
+
+		func NewStruct2() *Struct2 {
+			return nil
+		}
+
+		func NArgs(a, b string) (a, b string) { return }
+
+		type t struct{}`
+
+	want := []struct {
+		result string
+		err    bool
+	}{
+		{result: `(p) Method1()`},
+		{result: `Foo(ctx, s)`},
+		{result: `(s) Method()`},
+		{result: `NewStruct2()`},
+		{result: `NArgs(a, b)`},
+		{err: true},
+	}
+
+	// Parse src but stop after processing the imports.
+	fset := token.NewFileSet() // positions are relative to fset
+	f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
+	if err != nil {
+		t.Fatal(err)
+	}
+	renderer := &Renderer{fset: fset}
+	for i, d := range f.Decls {
+		got, err := renderer.ShortSynopsis(d)
+		if err != nil && !want[i].err {
+			t.Errorf("test %d, ShortSynopsis(): got unexpected error: %v", i, err)
+		}
+		if err == nil && want[i].err {
+			t.Errorf("test %d, ShortSynopsis(): got nil error, want non-nil error", i)
+		}
+		if got != want[i].result {
+			t.Errorf("test %d, ShortSynopsis():\ngot  %s\nwant %s", i, got, want[i].result)
+		}
+	}
+}
diff --git a/internal/fetch/dochtml/internal/render/synopsis.go b/internal/fetch/dochtml/internal/render/synopsis.go
index 712fe2a..629ec3d 100644
--- a/internal/fetch/dochtml/internal/render/synopsis.go
+++ b/internal/fetch/dochtml/internal/render/synopsis.go
@@ -14,13 +14,14 @@
 )
 
 // oneLineNodeDepth returns a one-line summary of the given input node.
-// The depth specifies the maximum depth when traversing the AST.
+// The depth specifies the current depth when traversing the AST and the
+// function will stop traversing once depth reaches maxSynopsisNodeDepth.
 func oneLineNodeDepth(fset *token.FileSet, node ast.Node, depth int) string {
 	const dotDotDot = "..."
-	if depth == 0 {
+	if depth == maxSynopsisNodeDepth {
 		return dotDotDot
 	}
-	depth--
+	depth++
 
 	switch n := node.(type) {
 	case nil:
diff --git a/internal/fetch/dochtml/internal/render/synopsis_test.go b/internal/fetch/dochtml/internal/render/synopsis_test.go
index 9afbef3..29a1470 100644
--- a/internal/fetch/dochtml/internal/render/synopsis_test.go
+++ b/internal/fetch/dochtml/internal/render/synopsis_test.go
@@ -170,7 +170,7 @@
 
 	// Print the imports from the file's AST.
 	for i, d := range f.Decls {
-		got := oneLineNodeDepth(fset, d, 10)
+		got := oneLineNodeDepth(fset, d, 0)
 		if got != want[i] {
 			t.Errorf("test %d, oneLineNode():\ngot  %s\nwant %s", i, got, want[i])
 		}
diff --git a/internal/fetch/dochtml/template.go b/internal/fetch/dochtml/template.go
index df0ab04..8ec7589 100644
--- a/internal/fetch/dochtml/template.go
+++ b/internal/fetch/dochtml/template.go
@@ -25,12 +25,13 @@
 			}
 			return a
 		},
-		"render_synopsis": (*render.Renderer)(nil).Synopsis,
-		"render_doc":      (*render.Renderer)(nil).DocHTML,
-		"render_decl":     (*render.Renderer)(nil).DeclHTML,
-		"render_code":     (*render.Renderer)(nil).CodeHTML,
-		"source_link":     func() string { return "" },
-		"play_url":        func(*doc.Example) string { return "" },
+		"render_short_synopsis": (*render.Renderer)(nil).ShortSynopsis,
+		"render_synopsis":       (*render.Renderer)(nil).Synopsis,
+		"render_doc":            (*render.Renderer)(nil).DocHTML,
+		"render_decl":           (*render.Renderer)(nil).DeclHTML,
+		"render_code":           (*render.Renderer)(nil).CodeHTML,
+		"source_link":           func() string { return "" },
+		"play_url":              func(*doc.Example) string { return "" },
 	},
 ).Parse(`{{- "" -}}
 {{- if or .Doc .Consts .Vars .Funcs .Types .Examples.List -}}
@@ -60,26 +61,44 @@
 
 	<li class="Documentation-tocItem Documentation-tocItem--funcsAndTypes">
 		<details class="TypesAndFuncs" open>
-			<summary class="TypesAndFuncs-summary">types and functions</summary>
+			<summary class="TypesAndFuncs-summary">Functions</summary>
 			<ul class="TypesAndFuncs-list">
 				{{- range .Funcs -}}
-				<li class="TypesAndFuncs-item">
-					<a href="#{{.Name}}">func {{.Name}}</a>
-				</li>{{"\n"}}
+					<li class="TypesAndFuncs-item">
+						<a href="#{{.Name}}" title="{{render_short_synopsis .Decl}}">{{render_short_synopsis .Decl}}</a>
+					</li>
 				{{- end -}}
-
+			</ul>
+		</details>
+	</li>
+	<li class="Documentation-tocItem Documentation-tocItem--funcsAndTypes">
+		<details class="TypesAndFuncs" open>
+			<summary class="TypesAndFuncs-summary">Types</summary>
+			<ul class="TypesAndFuncs-list">
 				{{- range .Types -}}
 					{{- $tname := .Name -}}
 					<li class="TypesAndFuncs-item"><a href="#{{$tname}}">type {{$tname}}</a></li>{{"\n"}}
 					{{- with .Funcs -}}
-						<li class="TypesAndFuncs-item TypesAndFuncs-item--noBorder"><ul>{{"\n" -}}
-						{{range .}}<li class="TypesAndFuncs-item"><a href="#{{.Name}}">func {{.Name}}</a></li>{{"\n"}}{{end}}
-						</ul></li>{{"\n" -}}
+						<li class="TypesAndFuncs-item TypesAndFuncs-item--noBorder">
+						  <ul>
+								{{range .}}
+									<li class="TypesAndFuncs-item">
+										<a href="#{{.Name}}" title="{{render_short_synopsis .Decl}}">{{render_short_synopsis .Decl}}</a>
+									</li>
+								{{end}}
+							</ul>
+						</li>
 					{{- end -}}
 					{{- with .Methods -}}
-						<li class="TypesAndFuncs-item TypesAndFuncs-item--noBorder"><ul>{{"\n" -}}
-						{{range .}}<li class="TypesAndFuncs-item"><a href="#{{$tname}}.{{.Name}}">func ({{.Recv}}) {{.Name}}</a></li>{{"\n"}}{{end}}
-						</ul></li>{{"\n" -}}
+						<li class="TypesAndFuncs-item TypesAndFuncs-item--noBorder">
+						  <ul>
+								{{range .}}
+									<li class="TypesAndFuncs-item">
+										<a href="#{{$tname}}.{{.Name}}" title="{{render_short_synopsis .Decl}}">{{render_short_synopsis .Decl}}</a>
+									</li>
+								{{end}}
+							</ul>
+						</li>
 					{{- end -}}
 				{{- end -}}
 			</ul>