tools/gopls: add cmd support for symbols

This change adds command line support for symbols.
Symbols are formatted as '{name} {type} {range}', with
children being preceded by a \t.

Example:

$ gopls symbols ~/tmp/foo/main.go
$
$ x Variable 7:5-7:6
$ y Constant 9:7-9:8
$ Quux Struct 29:6-29:10
$ 	Do Method 37:16-37:18
$ 	X Field 30:2-30:3
$ 	Y Field 30:5-30:6

Updates golang/go#32875

Change-Id: I1272fce733fb12b67e3d6fb948f5bf3de4ca2ca1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/203609
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 0d00c38..c320a5e 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -150,6 +150,7 @@
 		&rename{app: app},
 		&signature{app: app},
 		&suggestedfix{app: app},
+		&symbols{app: app},
 		&version{app: app},
 	}
 }
diff --git a/internal/lsp/cmd/symbols.go b/internal/lsp/cmd/symbols.go
new file mode 100644
index 0000000..6c2b34d
--- /dev/null
+++ b/internal/lsp/cmd/symbols.go
@@ -0,0 +1,80 @@
+// Copyright 2019 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 cmd
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"sort"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+	"golang.org/x/tools/internal/tool"
+)
+
+// references implements the references verb for gopls
+type symbols struct {
+	app *Application
+}
+
+func (r *symbols) Name() string      { return "symbols" }
+func (r *symbols) Usage() string     { return "<file>" }
+func (r *symbols) ShortHelp() string { return "display selected file's symbols" }
+func (r *symbols) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), `
+Example:
+  $ gopls symbols helper/helper.go
+`)
+	f.PrintDefaults()
+}
+func (r *symbols) Run(ctx context.Context, args ...string) error {
+	if len(args) != 1 {
+		return tool.CommandLineErrorf("symbols expects 1 argument (position)")
+	}
+
+	conn, err := r.app.connect(ctx)
+	if err != nil {
+		return err
+	}
+	defer conn.terminate(ctx)
+
+	from := span.Parse(args[0])
+	p := protocol.DocumentSymbolParams{
+		TextDocument: protocol.TextDocumentIdentifier{
+			URI: string(from.URI()),
+		},
+	}
+
+	symbols, err := conn.DocumentSymbol(ctx, &p)
+	if err != nil {
+		return err
+	}
+
+	for _, s := range symbols {
+		fmt.Println(symbolToString(s))
+		// Sort children for consistency
+		sort.Slice(s.Children, func(i, j int) bool {
+			return s.Children[i].Name < s.Children[j].Name
+		})
+		for _, c := range s.Children {
+			fmt.Println("\t" + symbolToString(c))
+		}
+	}
+
+	return nil
+}
+
+func symbolToString(symbol protocol.DocumentSymbol) string {
+	r := symbol.SelectionRange
+	// convert ranges to user friendly 1-based positions
+	position := fmt.Sprintf("%v:%v-%v:%v",
+		r.Start.Line+1,
+		r.Start.Character+1,
+		r.End.Line+1,
+		r.End.Character+1,
+	)
+	return fmt.Sprintf("%s %s %s", symbol.Name, symbol.Kind, position)
+}
diff --git a/internal/lsp/cmd/test/cmdtest.go b/internal/lsp/cmd/test/cmdtest.go
index adced2a..b4d2bfc 100644
--- a/internal/lsp/cmd/test/cmdtest.go
+++ b/internal/lsp/cmd/test/cmdtest.go
@@ -16,7 +16,6 @@
 	"testing"
 
 	"golang.org/x/tools/go/packages/packagestest"
-	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/lsp/tests"
 	"golang.org/x/tools/internal/span"
@@ -78,10 +77,6 @@
 	//TODO: add command line prepare rename tests when it works
 }
 
-func (r *runner) Symbol(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
-	//TODO: add command line symbol tests when it works
-}
-
 func (r *runner) Implementation(t *testing.T, spn span.Span, imp tests.Implementations) {
 	//TODO: add implements tests when it works
 }
diff --git a/internal/lsp/cmd/test/symbols.go b/internal/lsp/cmd/test/symbols.go
new file mode 100644
index 0000000..05f00ab
--- /dev/null
+++ b/internal/lsp/cmd/test/symbols.go
@@ -0,0 +1,33 @@
+// Copyright 2019 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 cmdtest
+
+import (
+	"testing"
+
+	"fmt"
+
+	"golang.org/x/tools/internal/lsp/cmd"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/span"
+	"golang.org/x/tools/internal/tool"
+)
+
+func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
+	filename := uri.Filename()
+	app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Config.Env, r.options)
+	got := CaptureStdOut(t, func() {
+		err := tool.Run(r.ctx, app, append([]string{"-remote=internal", "symbols"}, filename))
+		if err != nil {
+			fmt.Println(err)
+		}
+	})
+	expect := string(r.data.Golden("symbols", filename, func() ([]byte, error) {
+		return []byte(got), nil
+	}))
+	if expect != got {
+		t.Errorf("symbols failed for %s expected:\n%s\ngot:\n%s", filename, expect, got)
+	}
+}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 7e3a555..36be0fd 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -654,7 +654,7 @@
 	return res
 }
 
-func (r *runner) Symbol(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
+func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
 	params := &protocol.DocumentSymbolParams{
 		TextDocument: protocol.TextDocumentIdentifier{
 			URI: string(uri),
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index c4ab1bb..d3590e5 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -770,7 +770,7 @@
 	}
 }
 
-func (r *runner) Symbol(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
+func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol.DocumentSymbol) {
 	ctx := r.ctx
 	f, err := r.view.GetFile(ctx, uri)
 	if err != nil {
diff --git a/internal/lsp/testdata/symbols/main.go.golden b/internal/lsp/testdata/symbols/main.go.golden
new file mode 100644
index 0000000..22cc38a
--- /dev/null
+++ b/internal/lsp/testdata/symbols/main.go.golden
@@ -0,0 +1,29 @@
+-- symbols --
+x Variable 7:5-7:6
+y Constant 9:7-9:8
+Number Number 11:6-11:12
+Alias String 13:6-13:11
+NumberAlias Number 15:6-15:17
+Boolean Boolean 18:2-18:9
+BoolAlias Boolean 19:2-19:11
+Foo Struct 22:6-22:9
+	Bar Field 25:2-25:5
+	Baz Method 33:14-33:17
+	Quux Field 23:2-23:6
+	W Field 24:2-24:3
+	baz Field 26:2-26:5
+Quux Struct 29:6-29:10
+	Do Method 37:16-37:18
+	X Field 30:2-30:3
+	Y Field 30:5-30:6
+main Function 39:6-39:10
+Stringer Interface 43:6-43:14
+	String Method 44:2-44:8
+ABer Interface 47:6-47:10
+	A Method 49:2-49:3
+	B Method 48:2-48:3
+WithEmbeddeds Interface 52:6-52:19
+	ABer Interface 54:2-54:6
+	Do Method 53:2-53:4
+	io.Writer Interface 55:2-55:11
+
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 64225f8..4b47ff6 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -116,7 +116,7 @@
 	References(*testing.T, span.Span, []span.Span)
 	Rename(*testing.T, span.Span, string)
 	PrepareRename(*testing.T, span.Span, *source.PrepareItem)
-	Symbol(*testing.T, span.URI, []protocol.DocumentSymbol)
+	Symbols(*testing.T, span.URI, []protocol.DocumentSymbol)
 	SignatureHelp(*testing.T, span.Span, *source.SignatureInformation)
 	Link(*testing.T, span.URI, []Link)
 }
@@ -534,7 +534,7 @@
 		for uri, expectedSymbols := range data.Symbols {
 			t.Run(uriName(uri), func(t *testing.T) {
 				t.Helper()
-				tests.Symbol(t, uri, expectedSymbols)
+				tests.Symbols(t, uri, expectedSymbols)
 			})
 		}
 	})