diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index ee44f9c9..46f604a4 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -14,6 +14,7 @@ import ( "os/exec" "path/filepath" "runtime" + "sort" "strings" "testing" @@ -43,6 +44,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { const expectedDefinitionsCount = 16 const expectedTypeDefinitionsCount = 2 const expectedHighlightsCount = 2 + const expectedSymbolsCount = 1 files := packagestest.MustCopyFileTree(dir) for fragment, operation := range files { @@ -87,6 +89,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { expectedDefinitions := make(definitions) expectedTypeDefinitions := make(definitions) expectedHighlights := make(highlights) + expectedSymbols := make(symbols) // Collect any data that needs to be used by subsequent tests. if err := exported.Expect(map[string]interface{}{ @@ -97,6 +100,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { "godef": expectedDefinitions.collect, "typdef": expectedTypeDefinitions.collect, "highlight": expectedHighlights.collect, + "symbol": expectedSymbols.collect, }); err != nil { t.Fatal(err) } @@ -168,6 +172,16 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) { } expectedHighlights.test(t, s) }) + + t.Run("Symbols", func(t *testing.T) { + t.Helper() + if goVersion111 { // TODO(rstambler): Remove this when we no longer support Go 1.10. + if len(expectedSymbols) != expectedSymbolsCount { + t.Errorf("got %v symbols expected %v", len(expectedSymbols), expectedSymbolsCount) + } + } + expectedSymbols.test(t, s) + }) } type diagnostics map[span.URI][]protocol.Diagnostic @@ -176,6 +190,7 @@ type completions map[token.Position][]token.Pos type formats map[string]string type definitions map[protocol.Location]protocol.Location type highlights map[string][]protocol.Location +type symbols map[span.URI][]protocol.DocumentSymbol func (d diagnostics) test(t *testing.T, v source.View) int { count := 0 @@ -503,6 +518,70 @@ func (h highlights) test(t *testing.T, s *server) { } } +func (s symbols) collect(e *packagestest.Exported, fset *token.FileSet, name string, rng span.Range, kind int64) { + f := fset.File(rng.Start) + if f == nil { + return + } + + content, err := e.FileContents(f.Name()) + if err != nil { + return + } + + spn, err := rng.Span() + if err != nil { + return + } + + m := protocol.NewColumnMapper(spn.URI(), fset, f, content) + prng, err := m.Range(spn) + if err != nil { + return + } + + s[spn.URI()] = append(s[spn.URI()], protocol.DocumentSymbol{ + Name: name, + Kind: protocol.SymbolKind(kind), + SelectionRange: prng, + }) +} + +func (s symbols) test(t *testing.T, server *server) { + for uri, expectedSymbols := range s { + params := &protocol.DocumentSymbolParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: string(uri), + }, + } + symbols, err := server.DocumentSymbol(context.Background(), params) + if err != nil { + t.Fatal(err) + } + + if len(symbols) != len(expectedSymbols) { + t.Errorf("want %d symbols in %v, got %d", len(expectedSymbols), uri, len(symbols)) + continue + } + + sort.Slice(symbols, func(i, j int) bool { return symbols[i].Name < symbols[j].Name }) + sort.Slice(expectedSymbols, func(i, j int) bool { return expectedSymbols[i].Name < expectedSymbols[j].Name }) + for i, w := range expectedSymbols { + g := symbols[i] + if w.Name != g.Name { + t.Errorf("%s: want symbol %q, got %q", uri, w.Name, g.Name) + continue + } + if w.Kind != g.Kind { + t.Errorf("%s: want kind %v for %s, got %v", uri, w.Kind, w.Name, g.Kind) + } + if w.SelectionRange != g.SelectionRange { + t.Errorf("%s: want selection range %v for %s, got %v", uri, w.SelectionRange, w.Name, g.SelectionRange) + } + } + } +} + func testLocation(e *packagestest.Exported, fset *token.FileSet, rng packagestest.Range) (span.Span, *protocol.ColumnMapper) { spn, err := span.NewRange(fset, rng.Start, rng.End).Span() if err != nil { diff --git a/internal/lsp/source/symbols.go b/internal/lsp/source/symbols.go index e07f70dd..d645e330 100644 --- a/internal/lsp/source/symbols.go +++ b/internal/lsp/source/symbols.go @@ -23,6 +23,7 @@ const ( ConstantSymbol FunctionSymbol MethodSymbol + InterfaceSymbol ) type Symbol struct { @@ -100,6 +101,9 @@ func typeSymbol(spec *ast.TypeSpec, obj types.Object, fset *token.FileSet, q typ Name: obj.Name(), Kind: StructSymbol, } + if types.IsInterface(obj.Type()) { + s.Kind = InterfaceSymbol + } if span, err := nodeSpan(spec, fset); err == nil { s.Span = span } diff --git a/internal/lsp/symbols.go b/internal/lsp/symbols.go index 0e0c8a1f..ae15e08c 100644 --- a/internal/lsp/symbols.go +++ b/internal/lsp/symbols.go @@ -43,6 +43,8 @@ func toProtocolSymbolKind(kind source.SymbolKind) protocol.SymbolKind { return protocol.Function case source.MethodSymbol: return protocol.Method + case source.InterfaceSymbol: + return protocol.Interface default: return 0 } diff --git a/internal/lsp/testdata/symbols/main.go b/internal/lsp/testdata/symbols/main.go new file mode 100644 index 00000000..df11cb36 --- /dev/null +++ b/internal/lsp/testdata/symbols/main.go @@ -0,0 +1,27 @@ +package main + +var x = 42 //@symbol("x", "x", 13) + +const y = 43 //@symbol("y", "y", 14) + +type Foo struct { //@symbol("Foo", "Foo", 23) + Quux + Bar int + baz string +} + +type Quux struct { //@symbol("Quux", "Quux", 23) + X float64 +} + +func (f Foo) Baz() string { //@symbol("Baz", "Baz", 6) + return f.baz +} + +func main() { //@symbol("main", "main", 12) + +} + +type Stringer interface { //@symbol("Stringer", "Stringer", 11) + String() string +}