x/tools/internal/lsp: add support for document symbols

Updates golang/go#30915

Change-Id: I9a447f7748eb9894fb6f4072febec132b2ed91d7
Reviewed-on: https://go-review.googlesource.com/c/tools/+/168338
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index 3c3e6e2..842ae35 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -125,6 +125,7 @@
 				DefinitionProvider:              true,
 				DocumentFormattingProvider:      true,
 				DocumentRangeFormattingProvider: true,
+				DocumentSymbolProvider:          true,
 				HoverProvider:                   true,
 				SignatureHelpProvider: &protocol.SignatureHelpOptions{
 					TriggerCharacters: []string{"(", ","},
@@ -426,8 +427,13 @@
 	return nil, notImplemented("DocumentHighlight")
 }
 
-func (s *server) DocumentSymbol(context.Context, *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
-	return nil, notImplemented("DocumentSymbol")
+func (s *server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) {
+	f, m, err := newColumnMap(ctx, s.view, span.URI(params.TextDocument.URI))
+	if err != nil {
+		return nil, err
+	}
+	symbols := source.DocumentSymbols(ctx, f)
+	return toProtocolDocumentSymbols(m, symbols), nil
 }
 
 func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
diff --git a/internal/lsp/source/symbols.go b/internal/lsp/source/symbols.go
new file mode 100644
index 0000000..3a0b5ce
--- /dev/null
+++ b/internal/lsp/source/symbols.go
@@ -0,0 +1,129 @@
+// 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 source
+
+import (
+	"bytes"
+	"context"
+	"go/ast"
+	"go/format"
+	"go/token"
+
+	"golang.org/x/tools/internal/span"
+)
+
+type SymbolKind int
+
+const (
+	PackageSymbol SymbolKind = iota
+	StructSymbol
+	VariableSymbol
+	ConstantSymbol
+	FunctionSymbol
+	MethodSymbol
+)
+
+type Symbol struct {
+	Name     string
+	Detail   string
+	Span     span.Span
+	Kind     SymbolKind
+	Children []Symbol
+}
+
+func DocumentSymbols(ctx context.Context, f File) []Symbol {
+	var symbols []Symbol
+	fset := f.GetFileSet(ctx)
+	astFile := f.GetAST(ctx)
+	for _, decl := range astFile.Decls {
+		switch decl := decl.(type) {
+		case *ast.FuncDecl:
+			symbols = append(symbols, funcSymbol(decl, fset))
+		case *ast.GenDecl:
+			for _, spec := range decl.Specs {
+				switch spec := spec.(type) {
+				case *ast.ImportSpec:
+					symbols = append(symbols, importSymbol(spec, fset))
+				case *ast.TypeSpec:
+					symbols = append(symbols, typeSymbol(spec, fset))
+				case *ast.ValueSpec:
+					for _, name := range spec.Names {
+						symbols = append(symbols, varSymbol(decl, name, fset))
+					}
+				}
+			}
+		}
+	}
+	return symbols
+}
+
+func funcSymbol(decl *ast.FuncDecl, fset *token.FileSet) Symbol {
+	s := Symbol{
+		Name: decl.Name.String(),
+		Kind: FunctionSymbol,
+	}
+
+	if decl.Recv != nil {
+		s.Kind = MethodSymbol
+	}
+
+	span, err := nodeSpan(decl, fset)
+	if err == nil {
+		s.Span = span
+	}
+	buf := &bytes.Buffer{}
+	if err := format.Node(buf, fset, decl); err == nil {
+		s.Detail = buf.String()
+	}
+	return s
+}
+
+func importSymbol(spec *ast.ImportSpec, fset *token.FileSet) Symbol {
+	s := Symbol{
+		Name:   spec.Path.Value,
+		Kind:   PackageSymbol,
+		Detail: "import " + spec.Path.Value,
+	}
+	span, err := nodeSpan(spec, fset)
+	if err == nil {
+		s.Span = span
+	}
+	return s
+}
+
+func typeSymbol(spec *ast.TypeSpec, fset *token.FileSet) Symbol {
+	s := Symbol{
+		Name: spec.Name.String(),
+		Kind: StructSymbol,
+	}
+	span, err := nodeSpan(spec, fset)
+	if err == nil {
+		s.Span = span
+	}
+	return s
+}
+
+func varSymbol(decl *ast.GenDecl, name *ast.Ident, fset *token.FileSet) Symbol {
+	s := Symbol{
+		Name: name.Name,
+		Kind: VariableSymbol,
+	}
+
+	if decl.Tok == token.CONST {
+		s.Kind = ConstantSymbol
+	}
+
+	span, err := nodeSpan(name, fset)
+	if err == nil {
+		s.Span = span
+	}
+
+	return s
+}
+
+func nodeSpan(n ast.Node, fset *token.FileSet) (span.Span, error) {
+	r := span.NewRange(fset, n.Pos(), n.End())
+	return r.Span()
+}
diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go
new file mode 100644
index 0000000..794a79f
--- /dev/null
+++ b/internal/lsp/symbols.go
@@ -0,0 +1,48 @@
+// 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 lsp
+
+import (
+	"golang.org/x/tools/internal/lsp/source"
+
+	"golang.org/x/tools/internal/lsp/protocol"
+)
+
+func toProtocolDocumentSymbols(m *protocol.ColumnMapper, symbols []source.Symbol) []protocol.DocumentSymbol {
+	result := make([]protocol.DocumentSymbol, 0, len(symbols))
+	for _, s := range symbols {
+		ps := protocol.DocumentSymbol{
+			Name:     s.Name,
+			Kind:     toProtocolSymbolKind(s.Kind),
+			Detail:   s.Detail,
+			Children: toProtocolDocumentSymbols(m, s.Children),
+		}
+		if r, err := m.Range(s.Span); err == nil {
+			ps.Range = r
+			ps.SelectionRange = r
+		}
+		result = append(result, ps)
+	}
+	return result
+}
+
+func toProtocolSymbolKind(kind source.SymbolKind) protocol.SymbolKind {
+	switch kind {
+	case source.StructSymbol:
+		return protocol.Struct
+	case source.PackageSymbol:
+		return protocol.Package
+	case source.VariableSymbol:
+		return protocol.Variable
+	case source.ConstantSymbol:
+		return protocol.Constant
+	case source.FunctionSymbol:
+		return protocol.Function
+	case source.MethodSymbol:
+		return protocol.Method
+	default:
+		return 0
+	}
+}