diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index 10f2936f2b..04822d3307 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -240,6 +240,7 @@ func (c *connection) initialize(ctx context.Context, options func(*source.Option params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{ ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, } + params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport if _, err := c.Server.Initialize(ctx, params); err != nil { return err diff --git a/internal/lsp/cmd/symbols.go b/internal/lsp/cmd/symbols.go index eb3aa02b13..60bebbc324 100644 --- a/internal/lsp/cmd/symbols.go +++ b/internal/lsp/cmd/symbols.go @@ -6,6 +6,7 @@ package cmd import ( "context" + "encoding/json" "flag" "fmt" "sort" @@ -47,33 +48,62 @@ func (r *symbols) Run(ctx context.Context, args ...string) error { URI: protocol.URIFromSpanURI(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)) + s, ok := s.(map[string]interface{}) + if !ok { + continue + } + bytes, err := json.Marshal(s) + if err != nil { + return err + } + if _, ok := s["selectionRange"]; ok { + if err := parseDocumentSymbol(bytes); err != nil { + return err + } + continue + } + if err := parseSymbolInformation(bytes); err != nil { + return err } } - 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", +func parseDocumentSymbol(bytes []byte) error { + var s protocol.DocumentSymbol + if err := json.Unmarshal(bytes, &s); err != nil { + return err + } + fmt.Printf("%s %s %s\n", s.Name, s.Kind, positionToString(s.SelectionRange)) + // 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.Printf("\t%s %s %s\n", c.Name, c.Kind, positionToString(c.SelectionRange)) + } + return nil +} + +func parseSymbolInformation(bytes []byte) error { + var s protocol.SymbolInformation + if err := json.Unmarshal(bytes, &s); err != nil { + return err + } + fmt.Printf("%s %s %s\n", s.Name, s.Kind, positionToString(s.Location.Range)) + return nil +} + +func positionToString(r protocol.Range) string { + return 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/lsp_test.go b/internal/lsp/lsp_test.go index 110b1b7924..90d3864f5b 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -790,14 +790,22 @@ func (r *runner) Symbols(t *testing.T, uri span.URI, expectedSymbols []protocol. URI: protocol.URIFromSpanURI(uri), }, } - symbols, err := r.server.DocumentSymbol(r.ctx, params) + got, err := r.server.DocumentSymbol(r.ctx, params) if err != nil { t.Fatal(err) } - if len(symbols) != len(expectedSymbols) { - t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols)) + if len(got) != len(expectedSymbols) { + t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(got)) return } + symbols := make([]protocol.DocumentSymbol, len(got)) + for i, s := range got { + s, ok := s.(protocol.DocumentSymbol) + if !ok { + t.Fatalf("%v: wanted []DocumentSymbols but got %v", uri, got) + } + symbols[i] = s + } if diff := tests.DiffSymbols(t, uri, expectedSymbols, symbols); diff != "" { t.Error(diff) } diff --git a/internal/lsp/protocol/tsserver.go b/internal/lsp/protocol/tsserver.go index 5b77848ebd..356a2230d2 100644 --- a/internal/lsp/protocol/tsserver.go +++ b/internal/lsp/protocol/tsserver.go @@ -49,7 +49,7 @@ type Server interface { Definition(context.Context, *DefinitionParams) (Definition /*Definition | DefinitionLink[] | null*/, error) References(context.Context, *ReferenceParams) ([]Location /*Location[] | null*/, error) DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight /*DocumentHighlight[] | null*/, error) - DocumentSymbol(context.Context, *DocumentSymbolParams) ([]DocumentSymbol /*SymbolInformation[] | DocumentSymbol[] | null*/, error) + DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/, error) CodeAction(context.Context, *CodeActionParams) ([]CodeAction /*(Command | CodeAction)[] | null*/, error) Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation /*SymbolInformation[] | null*/, error) CodeLens(context.Context, *CodeLensParams) ([]CodeLens /*CodeLens[] | null*/, error) @@ -832,8 +832,8 @@ func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *Docume return result, nil } -func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]DocumentSymbol /*SymbolInformation[] | DocumentSymbol[] | null*/, error) { - var result []DocumentSymbol /*SymbolInformation[] | DocumentSymbol[] | null*/ +func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/, error) { + var result []interface{} /*SymbolInformation[] | DocumentSymbol[] | null*/ if err := s.Conn.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { return nil, err } diff --git a/internal/lsp/server_gen.go b/internal/lsp/server_gen.go index 4205ad569e..e02467e180 100644 --- a/internal/lsp/server_gen.go +++ b/internal/lsp/server_gen.go @@ -72,7 +72,7 @@ func (s *Server) DocumentLink(ctx context.Context, params *protocol.DocumentLink return s.documentLink(ctx, params) } -func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) { +func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { return s.documentSymbol(ctx, params) } diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index cf0783a569..e262dbd988 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -47,12 +47,13 @@ import ( func DefaultOptions() Options { return Options{ ClientOptions: ClientOptions{ - InsertTextFormat: protocol.PlainTextTextFormat, - PreferredContentFormat: protocol.Markdown, - ConfigurationSupported: true, - DynamicConfigurationSupported: true, - DynamicWatchedFilesSupported: true, - LineFoldingOnly: false, + InsertTextFormat: protocol.PlainTextTextFormat, + PreferredContentFormat: protocol.Markdown, + ConfigurationSupported: true, + DynamicConfigurationSupported: true, + DynamicWatchedFilesSupported: true, + LineFoldingOnly: false, + HierarchicalDocumentSymbolSupport: true, }, ServerOptions: ServerOptions{ SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{ @@ -104,12 +105,13 @@ type Options struct { } type ClientOptions struct { - InsertTextFormat protocol.InsertTextFormat - ConfigurationSupported bool - DynamicConfigurationSupported bool - DynamicWatchedFilesSupported bool - PreferredContentFormat protocol.MarkupKind - LineFoldingOnly bool + InsertTextFormat protocol.InsertTextFormat + ConfigurationSupported bool + DynamicConfigurationSupported bool + DynamicWatchedFilesSupported bool + PreferredContentFormat protocol.MarkupKind + LineFoldingOnly bool + HierarchicalDocumentSymbolSupport bool } type ServerOptions struct { @@ -273,6 +275,8 @@ func (o *Options) ForClientCapabilities(caps protocol.ClientCapabilities) { // Check if the client supports only line folding. fr := caps.TextDocument.FoldingRange o.LineFoldingOnly = fr.LineFoldingOnly + // Check if the client supports hierarchical document symbols. + o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport } func (o *Options) set(name string, value interface{}) OptionResult { diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go index b9f0b75de3..3fc007e76d 100644 --- a/internal/lsp/symbols.go +++ b/internal/lsp/symbols.go @@ -14,18 +14,38 @@ import ( "golang.org/x/tools/internal/telemetry/trace" ) -func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]protocol.DocumentSymbol, error) { +func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { ctx, done := trace.StartSpan(ctx, "lsp.Server.documentSymbol") defer done() snapshot, fh, ok, err := s.beginFileRequest(params.TextDocument.URI, source.Go) if !ok { - return []protocol.DocumentSymbol{}, err + return []interface{}{}, err } - symbols, err := source.DocumentSymbols(ctx, snapshot, fh) + docSymbols, err := source.DocumentSymbols(ctx, snapshot, fh) if err != nil { log.Error(ctx, "DocumentSymbols failed", err, telemetry.URI.Of(fh.Identity().URI)) - return []protocol.DocumentSymbol{}, nil + return []interface{}{}, nil + } + // Convert the symbols to an interface array. + // TODO: Remove this once the lsp deprecates SymbolInformation. + symbols := make([]interface{}, len(docSymbols)) + for i, s := range docSymbols { + if snapshot.View().Options().HierarchicalDocumentSymbolSupport { + symbols[i] = s + continue + } + // If the client does not support hierarchical document symbols, then + // we need to be backwards compatible for now and return SymbolInformation. + symbols[i] = protocol.SymbolInformation{ + Name: s.Name, + Kind: s.Kind, + Deprecated: s.Deprecated, + Location: protocol.Location{ + URI: params.TextDocument.URI, + Range: s.Range, + }, + } } return symbols, nil } diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index 36fefe94cb..9e796818eb 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -225,6 +225,7 @@ func DefaultOptions() source.Options { o.HoverKind = source.SynopsisDocumentation o.InsertTextFormat = protocol.SnippetTextFormat o.CompletionBudget = time.Minute + o.HierarchicalDocumentSymbolSupport = true return o }