malaperle created this revision. Herald added subscribers: cfe-commits, MaskRay, ioeric, jkorous-apple, mgrang, ilya-biryukov, mgorny, klimek.
This is a basic implementation of the "workspace/symbol" request which is used to find symbols by a string query. Since this is similar to code completion in terms of result, this implementation reuses the "fuzzyFind" in order to get matches. The index model was augmented to include more symbols like class members so that those can also be found by fuzzyFind and therefore workspace/symbol. Because fuzzyFind can now returns more results, a new option flag is introduced to optionally narrow down results to only completion-matches; this is how code completion can still behave the way it did before. For now, results are sorted alphanumerically and improvements to scoring could be done in the future. Another further improvement would be to include symbols in anonymous namespaces. Signed-off-by: Marc-Andre Laperle <marc-andre.lape...@ericsson.com> Repository: rCTE Clang Tools Extra https://reviews.llvm.org/D44882 Files: clangd/CMakeLists.txt clangd/ClangdLSPServer.cpp clangd/ClangdLSPServer.h clangd/ClangdServer.cpp clangd/ClangdServer.h clangd/CodeComplete.cpp clangd/Protocol.cpp clangd/Protocol.h clangd/ProtocolHandlers.cpp clangd/ProtocolHandlers.h clangd/SourceCode.cpp clangd/SourceCode.h clangd/WorkspaceSymbols.cpp clangd/WorkspaceSymbols.h clangd/XRefs.cpp clangd/index/Index.h clangd/index/MemIndex.cpp clangd/index/SymbolCollector.cpp clangd/index/SymbolYAML.cpp clangd/tool/ClangdMain.cpp test/clangd/initialize-params-invalid.test test/clangd/initialize-params.test unittests/clangd/CMakeLists.txt unittests/clangd/CodeCompleteTests.cpp unittests/clangd/FileIndexTests.cpp unittests/clangd/SymbolCollectorTests.cpp unittests/clangd/SyncAPI.cpp unittests/clangd/SyncAPI.h unittests/clangd/WorkspaceSymbolsTests.cpp
Index: unittests/clangd/WorkspaceSymbolsTests.cpp =================================================================== --- /dev/null +++ unittests/clangd/WorkspaceSymbolsTests.cpp @@ -0,0 +1,373 @@ +//===-- WorkspaceSymbolsTests.cpp ---------------------*- C++ -*-----------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "Annotations.h" +#include "ClangdServer.h" +#include "SyncAPI.h" +#include "TestFS.h" +#include "WorkspaceSymbols.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace clang { +namespace clangd { + +void PrintTo(const SymbolInformation &I, std::ostream *O) { + llvm::raw_os_ostream OS(*O); + OS << I.containerName << I.name << " - " << toJSON(I); +} + +namespace { + +using ::testing::AllOf; +using ::testing::AnyOf; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +class IgnoreDiagnostics : public DiagnosticsConsumer { + void onDiagnosticsReady(PathRef File, + std::vector<Diag> Diagnostics) override {} +}; + +// GMock helpers for matching SymbolInfos items. +MATCHER_P(Named, Name, "") { return arg.name == Name; } +MATCHER_P(InContainer, ContainerName, "") { + return arg.containerName == ContainerName; +} +MATCHER_P(WithKind, Kind, "") { return arg.kind == Kind; } + +class WorkspaceSymbolsTest : public ::testing::Test { +protected: + std::unique_ptr<MockFSProvider> FSProvider; + std::unique_ptr<MockCompilationDatabase> CDB; + std::unique_ptr<IgnoreDiagnostics> DiagConsumer; + std::unique_ptr<ClangdServer> Server; + std::unique_ptr<WorkspaceSymbolOptions> Opts; + + virtual void SetUp() override { + FSProvider = llvm::make_unique<MockFSProvider>(); + CDB = llvm::make_unique<MockCompilationDatabase>(); + DiagConsumer = llvm::make_unique<IgnoreDiagnostics>(); + auto ServerOpts = ClangdServer::optsForTest(); + ServerOpts.BuildDynamicSymbolIndex = true; + Server = llvm::make_unique<ClangdServer>(*CDB, *FSProvider, *DiagConsumer, + ServerOpts); + Opts = llvm::make_unique<WorkspaceSymbolOptions>(); + } + + std::vector<SymbolInformation> getSymbols(StringRef Query) { + EXPECT_TRUE(Server->blockUntilIdleForTest()) << "Waiting for preamble"; + auto SymbolInfos = runWorkspaceSymbols(*Server, Query, *Opts); + EXPECT_TRUE(bool(SymbolInfos)) << "workspaceSymbols returned an error"; + return *SymbolInfos; + } + + void addFile(StringRef FileName, StringRef Contents) { + auto Path = testPath(FileName); + FSProvider->Files[Path] = Contents; + Server->addDocument(Path, Contents); + } +}; + +} // namespace + +TEST_F(WorkspaceSymbolsTest, NoMacro) { + addFile("foo.cpp", R"cpp( + #define MACRO X + )cpp"); + + // Macros are not in the index. + EXPECT_THAT(getSymbols("macro"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, NoLocals) { + addFile("foo.cpp", R"cpp( + void test() { + struct LocalClass {}; + int local_var; + })cpp"); + EXPECT_THAT(getSymbols("local_var"), IsEmpty()); + EXPECT_THAT(getSymbols("LocalClass"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, NoParams) { + addFile("foo.cpp", R"cpp( + void test(int FirstParam, int SecondParam) { + })cpp"); + EXPECT_THAT(getSymbols("FirstParam"), IsEmpty()); + EXPECT_THAT(getSymbols("SecondParam"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, ClassWithMembers) { + addFile("foo.cpp", R"cpp( + struct ClassWithMembers { + int method(); + protected: + int field; + private: + int private_field; + };)cpp"); + EXPECT_THAT( + getSymbols("method"), + ElementsAre(AllOf(Named("method"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("ClassWithMembers::method"), + ElementsAre(AllOf(Named("method"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("ClassWithMembers::met"), + ElementsAre(AllOf(Named("method"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("field"), + ElementsAre( + AllOf(Named("field"), InContainer("ClassWithMembers::")), + AllOf(Named("private_field"), InContainer("ClassWithMembers::")))); + EXPECT_THAT( + getSymbols("ClassWithMembers::"), + ElementsAre( + AllOf(Named("field"), InContainer("ClassWithMembers::")), + AllOf(Named("method"), InContainer("ClassWithMembers::")), + AllOf(Named("private_field"), InContainer("ClassWithMembers::")))); + EXPECT_THAT(getSymbols("ClassWithMembers:"), IsEmpty()); + EXPECT_THAT(getSymbols("ClassWithMembers"), + ElementsAre(AllOf(Named("ClassWithMembers"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, ClassInNamespaceWithMembers) { + addFile("foo.cpp", R"cpp( + namespace ns1 { + struct ClassWithMembers { + int method(); + }; + })cpp"); + EXPECT_THAT(getSymbols("ns1::ClassWithMembers::method"), + ElementsAre(AllOf(Named("method"), + InContainer("ns1::ClassWithMembers::")))); +} + +TEST_F(WorkspaceSymbolsTest, Globals) { + addFile("foo.cpp", R"cpp( + int global_var; + + int global_func(); + + struct GlobalClass {};)cpp"); + EXPECT_THAT(getSymbols("global"), + ElementsAre(AllOf(Named("GlobalClass"), InContainer("")), + AllOf(Named("global_func"), InContainer("")), + AllOf(Named("global_var"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, Unnamed) { + addFile("foo.cpp", R"cpp( + struct { + int InUnnamed; + } UnnamedStruct;)cpp"); + EXPECT_THAT(getSymbols("UnnamedStruct"), ElementsAre(Named("UnnamedStruct"))); + EXPECT_THAT(getSymbols("InUnnamed"), + ElementsAre(AllOf(Named("InUnnamed"), + InContainer("(anonymous struct)::")))); +} + +TEST_F(WorkspaceSymbolsTest, InMainFile) { + addFile("foo.cpp", R"cpp( + int test() { + } + )cpp"); + EXPECT_THAT(getSymbols("test"), + ElementsAre(AllOf(Named("test"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, AnonymousNamespace) { + addFile("foo.cpp", R"cpp( + namespace { + void test() {} + } + )cpp"); + + EXPECT_THAT(getSymbols("test"), IsEmpty() + // FIXME: This should work once SymbolCollector collects symbols + // in anonymous namespaces. + // ElementsAre(AllOf(Named("test"), InContainer(""))) + ); +} + +TEST_F(WorkspaceSymbolsTest, MultiFile) { + addFile("foo.cpp", R"cpp( + int foo() { + } + )cpp"); + addFile("foo2.cpp", R"cpp( + int foo2() { + } + )cpp"); + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AllOf(Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer("")))); +} + +TEST_F(WorkspaceSymbolsTest, GlobalNamespaceQueries) { + addFile("foo.cpp", R"cpp( + int foo() { + } + class Foo { + int a; + }; + )cpp"); + EXPECT_THAT(getSymbols("::"), + ElementsAre(AllOf(Named("Foo"), InContainer("")), + AllOf(Named("a"), InContainer("Foo::")), + AllOf(Named("foo"), InContainer("")))); + EXPECT_THAT(getSymbols(":"), IsEmpty()); + EXPECT_THAT(getSymbols(""), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, WithLimit) { + addFile("foo.cpp", R"cpp( + int foo; + int foo2; + )cpp"); + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AllOf(Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer("")))); + + Opts->Limit = 1; + EXPECT_THAT(getSymbols("foo"), + ElementsAre(AnyOf((Named("foo"), InContainer("")), + AllOf(Named("foo2"), InContainer(""))))); +} + +TEST_F(WorkspaceSymbolsTest, Enums) { + addFile("foo.cpp", R"cpp( + enum { + Red + }; + enum Color1 { + Green + }; + enum class Color2 { + Yellow + }; + namespace ns { + enum { + Black + }; + } + )cpp"); + EXPECT_THAT(getSymbols("Red"), + ElementsAre(AllOf(Named("Red"), InContainer("")))); + EXPECT_THAT(getSymbols("Color1"), + ElementsAre(AllOf(Named("Color1"), InContainer("")))); + EXPECT_THAT(getSymbols("Green"), + ElementsAre(AllOf(Named("Green"), InContainer("")))); + EXPECT_THAT(getSymbols("Color2"), + ElementsAre(AllOf(Named("Color2"), InContainer("")))); + EXPECT_THAT(getSymbols("Yellow"), + ElementsAre(AllOf(Named("Yellow"), InContainer("Color2::")))); + EXPECT_THAT(getSymbols("Color2::Yellow"), + ElementsAre(AllOf(Named("Yellow"), InContainer("Color2::")))); + EXPECT_THAT(getSymbols("ns"), + ElementsAre(AllOf(Named("ns"), InContainer("")))); + EXPECT_THAT(getSymbols("Black"), + ElementsAre(AllOf(Named("Black"), InContainer("ns::")))); + EXPECT_THAT(getSymbols("ns::Black"), + ElementsAre(AllOf(Named("Black"), InContainer("ns::")))); +} + +TEST_F(WorkspaceSymbolsTest, Union) { + addFile("foo.cpp", R"cpp( + union U { + int x; + bool y; + }; + )cpp"); + EXPECT_THAT(getSymbols("U"), ElementsAre(AllOf(Named("U"), InContainer("")))); + EXPECT_THAT(getSymbols("x"), + ElementsAre(AllOf(Named("x"), InContainer("U::")))); + EXPECT_THAT(getSymbols("y"), + ElementsAre(AllOf(Named("y"), InContainer("U::")))); +} + +TEST_F(WorkspaceSymbolsTest, InlineNamespace) { + addFile("foo.cpp", R"cpp( + namespace na { + inline namespace nb { + class Foo {}; + } + } + namespace na { + // This is still inlined. + namespace nb { + class Bar {}; + } + } + )cpp"); + EXPECT_THAT(getSymbols("na"), + ElementsAre(AllOf(Named("na"), InContainer("")))); + EXPECT_THAT(getSymbols("nb"), + ElementsAre(AllOf(Named("nb"), InContainer("na::")))); + EXPECT_THAT(getSymbols("Foo"), + ElementsAre(AllOf(Named("Foo"), InContainer("na::")))); + EXPECT_THAT(getSymbols("na::Foo"), + ElementsAre(AllOf(Named("Foo"), InContainer("na::")))); + // It would be good if it was possible to query with the inline namespace as + // well. + // EXPECT_THAT(getSymbols("na::nb::Foo"), ElementsAre(AllOf(Named("Foo"), + // InContainer("na::Foo")))); + EXPECT_THAT(getSymbols("Bar"), + ElementsAre(AllOf(Named("Bar"), InContainer("na::")))); + EXPECT_THAT(getSymbols("na::Bar"), + ElementsAre(AllOf(Named("Bar"), InContainer("na::")))); + EXPECT_THAT(getSymbols("nb::Bar"), IsEmpty()); +} + +TEST_F(WorkspaceSymbolsTest, SymbolKindCapabilities) { + addFile("foo.cpp", R"cpp( + struct Foo {}; + class Foo2 {}; + enum { + FOO_VAL + } + )cpp"); + + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("Foo2"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("FOO_VAL"), InContainer(""), + WithKind(SymbolKind::Enum)))); + + using SymbolKindType = std::underlying_type<SymbolKind>::type; + std::vector<SymbolKind> BaseKinds; + for (SymbolKindType I = 0; I < SymbolKindType(SymbolKind::Array); ++I) + BaseKinds.push_back(SymbolKind(I)); + + Opts->supportedSymbolKinds = BaseKinds; + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("Foo2"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("FOO_VAL"), InContainer(""), + WithKind(SymbolKind::Enum)))); + + Opts->supportedSymbolKinds->push_back(SymbolKind::Struct); + Opts->supportedSymbolKinds->push_back(SymbolKind::EnumMember); + EXPECT_THAT( + getSymbols("Foo"), + UnorderedElementsAre( + AllOf(Named("Foo"), InContainer(""), WithKind(SymbolKind::Struct)), + AllOf(Named("Foo2"), InContainer(""), WithKind(SymbolKind::Class)), + AllOf(Named("FOO_VAL"), InContainer(""), + WithKind(SymbolKind::EnumMember)))); +} + +} // namespace clangd +} // namespace clang Index: unittests/clangd/SyncAPI.h =================================================================== --- unittests/clangd/SyncAPI.h +++ unittests/clangd/SyncAPI.h @@ -41,6 +41,10 @@ std::string runDumpAST(ClangdServer &Server, PathRef File); +llvm::Expected<std::vector<SymbolInformation>> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, + const WorkspaceSymbolOptions &Opts); + } // namespace clangd } // namespace clang Index: unittests/clangd/SyncAPI.cpp =================================================================== --- unittests/clangd/SyncAPI.cpp +++ unittests/clangd/SyncAPI.cpp @@ -110,5 +110,13 @@ return std::move(*Result); } +llvm::Expected<std::vector<SymbolInformation>> +runWorkspaceSymbols(ClangdServer &Server, StringRef Query, + const WorkspaceSymbolOptions &Opts) { + llvm::Optional<llvm::Expected<std::vector<SymbolInformation>>> Result; + Server.onWorkspaceSymbol(Query, Opts, capture(Result)); + return std::move(*Result); +} + } // namespace clangd } // namespace clang Index: unittests/clangd/SymbolCollectorTests.cpp =================================================================== --- unittests/clangd/SymbolCollectorTests.cpp +++ unittests/clangd/SymbolCollectorTests.cpp @@ -60,6 +60,9 @@ arg.Definition.EndOffset == Offsets.second; } MATCHER_P(Refs, R, "") { return int(arg.References) == R; } +MATCHER_P(ForCompletion, ForCompletion, "") { + return arg.ForCompletion == ForCompletion; +} namespace clang { namespace clangd { @@ -191,25 +194,31 @@ } // namespace foo )"; runSymbolCollector(Header, /*Main=*/""); - EXPECT_THAT(Symbols, - UnorderedElementsAreArray( - {QName("Foo"), QName("f1"), QName("f2"), QName("KInt"), - QName("kStr"), QName("foo"), QName("foo::bar"), - QName("foo::int32"), QName("foo::int32_t"), QName("foo::v1"), - QName("foo::bar::v2"), QName("foo::baz")})); + EXPECT_THAT( + Symbols, + UnorderedElementsAreArray( + {QName("Foo"), QName("Foo::f"), QName("f1"), QName("f2"), + QName("KInt"), QName("kStr"), QName("foo"), QName("foo::bar"), + QName("foo::int32"), QName("foo::int32_t"), QName("foo::v1"), + QName("foo::bar::v2"), QName("foo::baz")})); } +template <class T> struct Tmpl { T TmplField = 0; }; + TEST_F(SymbolCollectorTest, Template) { Annotations Header(R"( // Template is indexed, specialization and instantiation is not. - template <class T> struct [[Tmpl]] {T x = 0;}; + template <class T> struct [[Tmpl]] {T $xdecl[[x]] = 0;}; template <> struct Tmpl<int> {}; extern template struct Tmpl<float>; template struct Tmpl<double>; )"); runSymbolCollector(Header.code(), /*Main=*/""); - EXPECT_THAT(Symbols, UnorderedElementsAreArray({AllOf( - QName("Tmpl"), DeclRange(Header.offsetRange()))})); + EXPECT_THAT( + Symbols, + UnorderedElementsAreArray( + {AllOf(QName("Tmpl"), DeclRange(Header.offsetRange())), + AllOf(QName("Tmpl::x"), DeclRange(Header.offsetRange("xdecl")))})); } TEST_F(SymbolCollectorTest, Locations) { @@ -228,7 +237,7 @@ void $printdef[[print]]() {} // Declared/defined in main only. - int Y; + int $ydecl[[Y]]; )cpp"); runSymbolCollector(Header.code(), Main.code()); EXPECT_THAT( @@ -240,7 +249,8 @@ DefRange(Main.offsetRange("clsdef"))), AllOf(QName("print"), DeclRange(Header.offsetRange("printdecl")), DefRange(Main.offsetRange("printdef"))), - AllOf(QName("Z"), DeclRange(Header.offsetRange("zdecl"))))); + AllOf(QName("Z"), DeclRange(Header.offsetRange("zdecl"))), + AllOf(QName("Y"), DeclRange(Main.offsetRange("ydecl"))))); } TEST_F(SymbolCollectorTest, References) { @@ -262,10 +272,11 @@ CollectorOpts.CountReferences = true; runSymbolCollector(Header, Main); EXPECT_THAT(Symbols, - UnorderedElementsAre(AllOf(QName("W"), Refs(1)), - AllOf(QName("X"), Refs(1)), - AllOf(QName("Y"), Refs(0)), - AllOf(QName("Z"), Refs(0)), QName("y"))); + UnorderedElementsAre( + AllOf(QName("W"), Refs(1)), AllOf(QName("X"), Refs(1)), + AllOf(QName("Y"), Refs(0)), AllOf(QName("Z"), Refs(0)), + QName("y"), QName("w"), QName("w2"), QName("x"), QName("V"), + QName("v"))); } TEST_F(SymbolCollectorTest, SymbolRelativeNoFallback) { @@ -320,29 +331,31 @@ Green }; enum class Color2 { - Yellow // ignore + Yellow }; namespace ns { enum { Black }; } )"; runSymbolCollector(Header, /*Main=*/""); - EXPECT_THAT(Symbols, UnorderedElementsAre(QName("Red"), QName("Color"), - QName("Green"), QName("Color2"), - QName("ns"), QName("ns::Black"))); + EXPECT_THAT(Symbols, + UnorderedElementsAre(QName("Red"), QName("Color"), QName("Green"), + QName("Color2"), QName("Color2::Yellow"), + QName("ns"), QName("ns::Black"))); } -TEST_F(SymbolCollectorTest, IgnoreNamelessSymbols) { +TEST_F(SymbolCollectorTest, NamelessSymbols) { const std::string Header = R"( struct { int a; } Foo; )"; runSymbolCollector(Header, /*Main=*/""); - EXPECT_THAT(Symbols, - UnorderedElementsAre(QName("Foo"))); + EXPECT_THAT(Symbols, UnorderedElementsAre( + QName("Foo"), AllOf(QName("(anonymous struct)::a"), + ForCompletion(false)))); } TEST_F(SymbolCollectorTest, SymbolFormedFromMacro) { @@ -384,7 +397,7 @@ DeclURI(TestHeaderURI)))); } -TEST_F(SymbolCollectorTest, IgnoreSymbolsInMainFile) { +TEST_F(SymbolCollectorTest, SymbolsInMainFile) { const std::string Header = R"( class Foo {}; void f1(); @@ -394,15 +407,16 @@ namespace { void ff() {} // ignore } - void main_f() {} // ignore + void main_f() {} // not for completion void f1() {} )"; runSymbolCollector(Header, Main); - EXPECT_THAT(Symbols, - UnorderedElementsAre(QName("Foo"), QName("f1"), QName("f2"))); + EXPECT_THAT(Symbols, UnorderedElementsAre( + QName("Foo"), QName("f1"), QName("f2"), + AllOf(QName("main_f"), ForCompletion(false)))); } -TEST_F(SymbolCollectorTest, IgnoreClassMembers) { +TEST_F(SymbolCollectorTest, ClassMembers) { const std::string Header = R"( class Foo { void f() {} @@ -417,7 +431,13 @@ void Foo::ssf() {} )"; runSymbolCollector(Header, Main); - EXPECT_THAT(Symbols, UnorderedElementsAre(QName("Foo"))); + EXPECT_THAT(Symbols, + UnorderedElementsAre( + QName("Foo"), AllOf(QName("Foo::f"), ForCompletion(false)), + AllOf(QName("Foo::g"), ForCompletion(false)), + AllOf(QName("Foo::sf"), ForCompletion(false)), + AllOf(QName("Foo::ssf"), ForCompletion(false)), + AllOf(QName("Foo::x"), ForCompletion(false)))); } TEST_F(SymbolCollectorTest, Scopes) { @@ -512,6 +532,7 @@ StartOffset: 0 EndOffset: 1 FileURI: file:///path/foo.h +ForCompletion: true CompletionLabel: 'Foo1-label' CompletionFilterText: 'filter' CompletionPlainInsertText: 'plain' @@ -532,6 +553,7 @@ StartOffset: 10 EndOffset: 12 FileURI: file:///path/bar.h +ForCompletion: true CompletionLabel: 'Foo2-label' CompletionFilterText: 'filter' CompletionPlainInsertText: 'plain' @@ -545,9 +567,10 @@ QName("clang::Foo1"), Labeled("Foo1-label"), Doc("Foo doc"), Detail("int"), DeclURI("file:///path/foo.h")))); auto Symbols2 = SymbolsFromYAML(YAML2); - EXPECT_THAT(Symbols2, UnorderedElementsAre(AllOf( - QName("clang::Foo2"), Labeled("Foo2-label"), - Not(HasDetail()), DeclURI("file:///path/bar.h")))); + EXPECT_THAT(Symbols2, + UnorderedElementsAre(AllOf( + QName("clang::Foo2"), Labeled("Foo2-label"), Not(HasDetail()), + DeclURI("file:///path/bar.h"), ForCompletion(true)))); std::string ConcatenatedYAML; { @@ -638,23 +661,30 @@ // Canonical declarations. class $cdecl[[C]] {}; struct $sdecl[[S]] {}; - union $udecl[[U]] {int x; bool y;}; + union $udecl[[U]] {int $xdecl[[x]]; bool $ydecl[[y]];}; )"); runSymbolCollector(Header.code(), /*Main=*/""); - EXPECT_THAT(Symbols, - UnorderedElementsAre( - AllOf(QName("C"), DeclURI(TestHeaderURI), - DeclRange(Header.offsetRange("cdecl")), - IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), - DefRange(Header.offsetRange("cdecl"))), - AllOf(QName("S"), DeclURI(TestHeaderURI), - DeclRange(Header.offsetRange("sdecl")), - IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), - DefRange(Header.offsetRange("sdecl"))), - AllOf(QName("U"), DeclURI(TestHeaderURI), - DeclRange(Header.offsetRange("udecl")), - IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), - DefRange(Header.offsetRange("udecl"))))); + EXPECT_THAT( + Symbols, + UnorderedElementsAre( + AllOf(QName("C"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("cdecl")), + IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("cdecl"))), + AllOf(QName("S"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("sdecl")), + IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("sdecl"))), + AllOf(QName("U"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("udecl")), + IncludeHeader(TestHeaderURI), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("udecl"))), + AllOf(QName("U::x"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("xdecl")), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("xdecl"))), + AllOf(QName("U::y"), DeclURI(TestHeaderURI), + DeclRange(Header.offsetRange("ydecl")), DefURI(TestHeaderURI), + DefRange(Header.offsetRange("ydecl"))))); } TEST_F(SymbolCollectorTest, ClassForwardDeclarationIsCanonical) { Index: unittests/clangd/FileIndexTests.cpp =================================================================== --- unittests/clangd/FileIndexTests.cpp +++ unittests/clangd/FileIndexTests.cpp @@ -170,15 +170,16 @@ EXPECT_THAT(match(M, FuzzyFindRequest()), UnorderedElementsAre()); } -TEST(FileIndexTest, IgnoreClassMembers) { +TEST(FileIndexTest, ClassMembers) { FileIndex M; M.update("f1", build("f1", "class X { static int m1; int m2; static void f(); };") .getPointer()); FuzzyFindRequest Req; Req.Query = ""; - EXPECT_THAT(match(M, Req), UnorderedElementsAre("X")); + EXPECT_THAT(match(M, Req), + UnorderedElementsAre("X", "X::m1", "X::m2", "X::f")); } TEST(FileIndexTest, NoIncludeCollected) { Index: unittests/clangd/CodeCompleteTests.cpp =================================================================== --- unittests/clangd/CodeCompleteTests.cpp +++ unittests/clangd/CodeCompleteTests.cpp @@ -57,6 +57,7 @@ using ::testing::Each; using ::testing::ElementsAre; using ::testing::Field; +using ::testing::IsEmpty; using ::testing::Not; using ::testing::UnorderedElementsAre; @@ -156,6 +157,7 @@ } USR += Regex("^.*$").sub(USRFormat, Sym.Name); // e.g. func -> @F@func# Sym.ID = SymbolID(USR); + Sym.ForCompletion = true; Sym.CompletionPlainInsertText = Sym.Name; Sym.CompletionSnippetInsertText = Sym.Name; Sym.CompletionLabel = Sym.Name; @@ -645,6 +647,109 @@ EXPECT_THAT(Results.items, Not(Contains(Labeled("param_in_bar")))); } +TEST(CompletionTest, Enums) { + EXPECT_THAT(completions(R"cpp( + enum class Color2 { + Yellow + }; + void foo() { + Color2::^ + })cpp") + .items, + Has("Yellow", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + enum { + Red + }; + void foo() { + Re^ + })cpp") + .items, + Has("Red", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + enum Color { + Green + }; + void foo() { + Gr^ + })cpp") + .items, + Has("Green", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + namespace ns { + enum { + Black + }; + } + void foo() { + ns::B^ + })cpp") + .items, + Has("Black", CompletionItemKind::Value)); + EXPECT_THAT(completions(R"cpp( + void foo() { + ns::B^ + })cpp") + .items, + IsEmpty()); +} + +TEST(CompletionTest, AnonymousNamespace) { + + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + auto Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + auto File = testPath("bar.cpp"); + Server.addDocument(File, R"( + namespace { + void inAnymous() { + } + } // namespace + )"); + + File = testPath("bar2.cpp"); + Annotations Test(R"( + void bar() { + inAnym^ + } + )"); + + Server.addDocument(File, Test.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto Results = cantFail(runCodeComplete(Server, File, Test.point(), {})); + EXPECT_THAT(Results.items, IsEmpty()); +} + +TEST(CompletionTest, InMainFile) { + + MockFSProvider FS; + MockCompilationDatabase CDB; + IgnoreDiagnostics DiagConsumer; + auto Opts = ClangdServer::optsForTest(); + Opts.BuildDynamicSymbolIndex = true; + ClangdServer Server(CDB, FS, DiagConsumer, Opts); + auto File = testPath("main.cpp"); + Server.addDocument(File, R"( + void funcInMain() { + } + )"); + + File = testPath("bar2.cpp"); + Annotations Test(R"( + void bar() { + funcInMa^ + } + )"); + + Server.addDocument(File, Test.code()); + ASSERT_TRUE(Server.blockUntilIdleForTest()) << "Waiting for preamble"; + auto Results = cantFail(runCodeComplete(Server, File, Test.point(), {})); + EXPECT_THAT(Results.items, IsEmpty()); +} + SignatureHelp signatures(StringRef Text) { MockFSProvider FS; MockCompilationDatabase CDB; Index: unittests/clangd/CMakeLists.txt =================================================================== --- unittests/clangd/CMakeLists.txt +++ unittests/clangd/CMakeLists.txt @@ -28,6 +28,7 @@ TraceTests.cpp TUSchedulerTests.cpp URITests.cpp + WorkspaceSymbolsTests.cpp XRefsTests.cpp ) Index: test/clangd/initialize-params.test =================================================================== --- test/clangd/initialize-params.test +++ test/clangd/initialize-params.test @@ -36,7 +36,8 @@ # CHECK-NEXT: "," # CHECK-NEXT: ] # CHECK-NEXT: }, -# CHECK-NEXT: "textDocumentSync": 1 +# CHECK-NEXT: "textDocumentSync": 1, +# CHECK-NEXT: "workspaceSymbolProvider": true # CHECK-NEXT: } # CHECK-NEXT: } --- Index: test/clangd/initialize-params-invalid.test =================================================================== --- test/clangd/initialize-params-invalid.test +++ test/clangd/initialize-params-invalid.test @@ -36,7 +36,8 @@ # CHECK-NEXT: "," # CHECK-NEXT: ] # CHECK-NEXT: }, -# CHECK-NEXT: "textDocumentSync": 1 +# CHECK-NEXT: "textDocumentSync": 1, +# CHECK-NEXT: "workspaceSymbolProvider": true # CHECK-NEXT: } # CHECK-NEXT: } --- Index: clangd/tool/ClangdMain.cpp =================================================================== --- clangd/tool/ClangdMain.cpp +++ clangd/tool/ClangdMain.cpp @@ -102,6 +102,13 @@ "0 means no limit."), llvm::cl::init(100)); +static llvm::cl::opt<int> LimitWorkspaceSymbolResult( + "workspace-symbol-limit", + llvm::cl::desc( + "Limit the number of workspace symbol results returned by clangd. " + "0 means no limit."), + llvm::cl::init(100)); + static llvm::cl::opt<bool> RunSynchronously( "run-synchronously", llvm::cl::desc("Parse on main thread. If set, -j is ignored"), @@ -118,11 +125,11 @@ "Mirror all LSP input to the specified file. Useful for debugging."), llvm::cl::init(""), llvm::cl::Hidden); -static llvm::cl::opt<bool> EnableIndexBasedCompletion( - "enable-index-based-completion", - llvm::cl::desc( - "Enable index-based global code completion. " - "Clang uses an index built from symbols in opened files"), +static llvm::cl::opt<bool> EnableIndexBasedFeatures( + "enable-index-based-features", + llvm::cl::desc("Enable index-based features such as global code completion " + "and searching for symbols." + "Clang uses an index built from symbols in opened files"), llvm::cl::init(true)); static llvm::cl::opt<Path> YamlSymbolFile( @@ -220,9 +227,9 @@ } if (!ResourceDir.empty()) Opts.ResourceDir = ResourceDir; - Opts.BuildDynamicSymbolIndex = EnableIndexBasedCompletion; + Opts.BuildDynamicSymbolIndex = EnableIndexBasedFeatures; std::unique_ptr<SymbolIndex> StaticIdx; - if (EnableIndexBasedCompletion && !YamlSymbolFile.empty()) { + if (EnableIndexBasedFeatures && !YamlSymbolFile.empty()) { StaticIdx = BuildStaticIndex(YamlSymbolFile); Opts.StaticIndex = StaticIdx.get(); } @@ -232,8 +239,11 @@ CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; CCOpts.Limit = LimitCompletionResult; + clangd::WorkspaceSymbolOptions WorkspaceSymOpts; + WorkspaceSymOpts.Limit = LimitWorkspaceSymbolResult; // Initialize and run ClangdLSPServer. - ClangdLSPServer LSPServer(Out, CCOpts, CompileCommandsDirPath, Opts); + ClangdLSPServer LSPServer(Out, CCOpts, WorkspaceSymOpts, + CompileCommandsDirPath, Opts); constexpr int NoShutdownRequestErrorCode = 1; llvm::set_thread_name("clangd.main"); // Change stdin to binary to not lose \r\n on windows. Index: clangd/index/SymbolYAML.cpp =================================================================== --- clangd/index/SymbolYAML.cpp +++ clangd/index/SymbolYAML.cpp @@ -101,6 +101,7 @@ SymbolLocation()); IO.mapOptional("Definition", Sym.Definition, SymbolLocation()); IO.mapOptional("References", Sym.References, 0u); + IO.mapOptional("ForCompletion", Sym.ForCompletion, false); IO.mapRequired("CompletionLabel", Sym.CompletionLabel); IO.mapRequired("CompletionFilterText", Sym.CompletionFilterText); IO.mapRequired("CompletionPlainInsertText", Sym.CompletionPlainInsertText); Index: clangd/index/SymbolCollector.cpp =================================================================== --- clangd/index/SymbolCollector.cpp +++ clangd/index/SymbolCollector.cpp @@ -11,6 +11,7 @@ #include "../AST.h" #include "../CodeCompletionStrings.h" #include "../Logger.h" +#include "../SourceCode.h" #include "../URI.h" #include "CanonicalIncludes.h" #include "clang/AST/DeclCXX.h" @@ -80,25 +81,8 @@ return llvm::None; } -// "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no qualifier. -std::pair<llvm::StringRef, llvm::StringRef> -splitQualifiedName(llvm::StringRef QName) { - assert(!QName.startswith("::") && "Qualified names should not start with ::"); - size_t Pos = QName.rfind("::"); - if (Pos == llvm::StringRef::npos) - return {StringRef(), QName}; - return {QName.substr(0, Pos + 2), QName.substr(Pos + 2)}; -} - bool shouldFilterDecl(const NamedDecl *ND, ASTContext *ASTCtx, const SymbolCollector::Options &Opts) { - using namespace clang::ast_matchers; - if (ND->isImplicit()) - return true; - // Skip anonymous declarations, e.g (anonymous enum/class/struct). - if (ND->getDeclName().isEmpty()) - return true; - // FIXME: figure out a way to handle internal linkage symbols (e.g. static // variables, function) defined in the .cc files. Also we skip the symbols // in anonymous namespace as the qualifier names of these symbols are like @@ -110,27 +94,41 @@ if (ND->isInAnonymousNamespace()) return true; + using namespace clang::ast_matchers; + if (ND->isImplicit()) + return true; + // Skip anonymous declarations, e.g (anonymous enum/class/struct). + if (ND->getDeclName().isEmpty()) + return true; + + // Don't index template specializations. + auto IsSpecialization = + anyOf(functionDecl(isExplicitTemplateSpecialization()), + cxxRecordDecl(isExplicitTemplateSpecialization()), + varDecl(isExplicitTemplateSpecialization())); + if (!match(decl(IsSpecialization), *ND, *ASTCtx).empty()) + return true; + + return false; +} + +bool isForCompletion(const NamedDecl *ND, ASTContext *ASTCtx) { + using namespace clang::ast_matchers; // We only want: // * symbols in namespaces or translation unit scopes (e.g. no class // members) // * enum constants in unscoped enum decl (e.g. "red" in "enum {red};") auto InTopLevelScope = hasDeclContext( anyOf(namespaceDecl(), translationUnitDecl(), linkageSpecDecl())); - // Don't index template specializations. - auto IsSpecialization = - anyOf(functionDecl(isExplicitTemplateSpecialization()), - cxxRecordDecl(isExplicitTemplateSpecialization()), - varDecl(isExplicitTemplateSpecialization())); if (match(decl(allOf(unless(isExpansionInMainFile()), anyOf(InTopLevelScope, hasDeclContext(enumDecl(InTopLevelScope, - unless(isScoped())))), - unless(IsSpecialization))), + unless(isScoped())))))), *ND, *ASTCtx) .empty()) - return true; + return false; - return false; + return true; } // We only collect #include paths for symbols that are suitable for global code @@ -299,6 +297,8 @@ Symbol S; S.ID = std::move(ID); + assert(!StringRef(QName).startswith("::") && + "Qualified names should not start with '::' here."); std::tie(S.Scope, S.Name) = splitQualifiedName(QName); S.SymInfo = index::getSymbolInfo(&ND); std::string FileURI; @@ -314,34 +314,39 @@ *ASTCtx, *PP, CodeCompletionContext::CCC_Name, *CompletionAllocator, *CompletionTUInfo, /*IncludeBriefComments*/ true); - std::string Label; - std::string SnippetInsertText; - std::string IgnoredLabel; - std::string PlainInsertText; - getLabelAndInsertText(*CCS, &Label, &SnippetInsertText, - /*EnableSnippets=*/true); - getLabelAndInsertText(*CCS, &IgnoredLabel, &PlainInsertText, - /*EnableSnippets=*/false); - std::string FilterText = getFilterText(*CCS); - std::string Documentation = getDocumentation(*CCS); - std::string CompletionDetail = getDetail(*CCS); + std::string Documentation = getDocumentation(*CCS); std::string Include; if (Opts.CollectIncludePath && shouldCollectIncludePath(S.SymInfo.Kind)) { // Use the expansion location to get the #include header since this is // where the symbol is exposed. if (auto Header = getIncludeHeader( QName, SM, SM.getExpansionLoc(ND.getLocation()), Opts)) Include = std::move(*Header); } - S.CompletionFilterText = FilterText; - S.CompletionLabel = Label; - S.CompletionPlainInsertText = PlainInsertText; - S.CompletionSnippetInsertText = SnippetInsertText; + Symbol::Details Detail; Detail.Documentation = Documentation; - Detail.CompletionDetail = CompletionDetail; Detail.IncludeHeader = Include; + + std::string Label, SnippetInsertText, IgnoredLabel, PlainInsertText, + CompletionDetail, FilterText; + if (isForCompletion(&ND, ASTCtx)) { + S.ForCompletion = true; + getLabelAndInsertText(*CCS, &Label, &SnippetInsertText, + /*EnableSnippets=*/true); + getLabelAndInsertText(*CCS, &IgnoredLabel, &PlainInsertText, + /*EnableSnippets=*/false); + CompletionDetail = getDetail(*CCS); + FilterText = getFilterText(*CCS); + + Detail.CompletionDetail = CompletionDetail; + S.CompletionFilterText = FilterText; + S.CompletionLabel = Label; + S.CompletionPlainInsertText = PlainInsertText; + S.CompletionSnippetInsertText = SnippetInsertText; + } + S.Detail = &Detail; Symbols.insert(S); Index: clangd/index/MemIndex.cpp =================================================================== --- clangd/index/MemIndex.cpp +++ clangd/index/MemIndex.cpp @@ -45,6 +45,8 @@ // Exact match against all possible scopes. if (!Req.Scopes.empty() && !llvm::is_contained(Req.Scopes, Sym->Scope)) continue; + if (Req.CompletionMatchesOnly && !Sym->ForCompletion) + continue; if (auto Score = Filter.match(Sym->Name)) { Top.emplace(-*Score, Sym); Index: clangd/index/Index.h =================================================================== --- clangd/index/Index.h +++ clangd/index/Index.h @@ -139,8 +139,11 @@ // file. This number is only meaningful if aggregated in an index. unsigned References = 0; + // Whether or not the symbol should be considered for completion. + bool ForCompletion = false; + /// A brief description of the symbol that can be displayed in the completion - /// candidate list. For example, "Foo(X x, Y y) const" is a labal for a + /// candidate list. For example, "Foo(X x, Y y) const" is a label for a /// function. llvm::StringRef CompletionLabel; /// The piece of text that the user is expected to type to match the @@ -250,6 +253,8 @@ /// \brief The number of top candidates to return. The index may choose to /// return more than this, e.g. if it doesn't know which candidates are best. size_t MaxCandidateCount = UINT_MAX; + /// A flag to restrict the results to completion matches. + bool CompletionMatchesOnly = false; }; struct LookupRequest { Index: clangd/XRefs.cpp =================================================================== --- clangd/XRefs.cpp +++ clangd/XRefs.cpp @@ -143,45 +143,14 @@ } }; -llvm::Optional<Location> -makeLocation(ParsedAST &AST, const SourceRange &ValSourceRange) { - const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); - const LangOptions &LangOpts = AST.getASTContext().getLangOpts(); - SourceLocation LocStart = ValSourceRange.getBegin(); - - const FileEntry *F = - SourceMgr.getFileEntryForID(SourceMgr.getFileID(LocStart)); - if (!F) - return llvm::None; - SourceLocation LocEnd = Lexer::getLocForEndOfToken(ValSourceRange.getEnd(), 0, - SourceMgr, LangOpts); - Position Begin = sourceLocToPosition(SourceMgr, LocStart); - Position End = sourceLocToPosition(SourceMgr, LocEnd); - Range R = {Begin, End}; - Location L; - - SmallString<64> FilePath = F->tryGetRealPathName(); - if (FilePath.empty()) - FilePath = F->getName(); - if (!llvm::sys::path::is_absolute(FilePath)) { - if (!SourceMgr.getFileManager().makeAbsolutePath(FilePath)) { - log("Could not turn relative path to absolute: " + FilePath); - return llvm::None; - } - } - - L.uri = URIForFile(FilePath.str()); - L.range = R; - return L; -} - } // namespace std::vector<Location> findDefinitions(ParsedAST &AST, Position Pos) { const SourceManager &SourceMgr = AST.getASTContext().getSourceManager(); const FileEntry *FE = SourceMgr.getFileEntryForID(SourceMgr.getMainFileID()); if (!FE) return {}; + const LangOptions &LangOpts = AST.getASTContext().getLangOpts(); SourceLocation SourceLocationBeg = getBeginningOfIdentifier(AST, Pos, FE); @@ -213,14 +182,16 @@ for (auto D : Decls) { auto Loc = findNameLoc(D); - auto L = makeLocation(AST, SourceRange(Loc, Loc)); + auto EndLoc = Lexer::getLocForEndOfToken(Loc, 0, SourceMgr, LangOpts); + auto L = sourceRangeToLocation(SourceMgr, SourceRange(Loc, EndLoc)); if (L) Result.push_back(*L); } for (auto Item : MacroInfos) { auto Loc = Item.Info->getDefinitionLoc(); - auto L = makeLocation(AST, SourceRange(Loc, Loc)); + auto EndLoc = Lexer::getLocForEndOfToken(Loc, 0, SourceMgr, LangOpts); + auto L = sourceRangeToLocation(SourceMgr, SourceRange(Loc, EndLoc)); if (L) Result.push_back(*L); } Index: clangd/WorkspaceSymbols.h =================================================================== --- /dev/null +++ clangd/WorkspaceSymbols.h @@ -0,0 +1,42 @@ +//===--- WorkspaceSymbols.h --------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// Workspace symbols provides a list of symbols in the workspace matching a +// string query. +// +//===----------------------------------------------------------------------===// +#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_WORKSPACESYMBOL_H +#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_WORKSPACESYMBOL_H + +#include "Protocol.h" +#include "clang/Basic/VirtualFileSystem.h" +#include "llvm/ADT/IntrusiveRefCntPtr.h" +#include "llvm/ADT/StringRef.h" + +namespace clang { +namespace clangd { +class SymbolIndex; + +struct WorkspaceSymbolOptions { + /// Limit the number of results returned (0 means no limit). + size_t Limit = 0; + + /// Symbol kinds supported by the requester. + llvm::Optional<std::vector<SymbolKind>> supportedSymbolKinds; +}; + +llvm::Expected<std::vector<SymbolInformation>> +getWorkspaceSymbols(llvm::StringRef Query, const WorkspaceSymbolOptions &Opts, + const SymbolIndex *const Index, + llvm::IntrusiveRefCntPtr<vfs::FileSystem> VFS); + +} // namespace clangd +} // namespace clang + +#endif Index: clangd/WorkspaceSymbols.cpp =================================================================== --- /dev/null +++ clangd/WorkspaceSymbols.cpp @@ -0,0 +1,171 @@ +//===--- WorkspaceSymbols.cpp ------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#include "WorkspaceSymbols.h" + +#include "Logger.h" +#include "SourceCode.h" +#include "index/Index.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Index/IndexSymbol.h" +#include "llvm/Support/FormatVariadic.h" + +namespace clang { +namespace clangd { + +namespace { + +SymbolKind adjustKindToCapability( + SymbolKind Kind, + const llvm::Optional<std::vector<SymbolKind>> &supportedSymbolKinds) { + // All clients should support those. + if (Kind >= SymbolKind::File && Kind <= SymbolKind::Array) + return Kind; + + if (supportedSymbolKinds && + std::find(supportedSymbolKinds->begin(), supportedSymbolKinds->end(), + Kind) != supportedSymbolKinds->end()) + return Kind; + + // Provide some fall backs for common kinds that are close enough. + if (Kind == SymbolKind::Struct) + return SymbolKind::Class; + if (Kind == SymbolKind::EnumMember) + return SymbolKind::Enum; + + if (!supportedSymbolKinds) { + // Provide some sensible default when all fails. + return SymbolKind::Variable; + } + return Kind; +} + +// Convert a index::SymbolKind to clangd::SymbolKind (LSP) +// Note, some are not perfect matches and should be improved when this LSP +// issue is addressed: +// https://github.com/Microsoft/language-server-protocol/issues/344 +SymbolKind indexSymbolKindToSymbolKind(index::SymbolKind Kind) { + switch (Kind) { + case index::SymbolKind::Unknown: + return SymbolKind::Variable; + case index::SymbolKind::Module: + return SymbolKind::Module; + case index::SymbolKind::Namespace: + return SymbolKind::Namespace; + case index::SymbolKind::NamespaceAlias: + return SymbolKind::Namespace; + case index::SymbolKind::Macro: + return SymbolKind::String; + case index::SymbolKind::Enum: + return SymbolKind::Enum; + case index::SymbolKind::Struct: + return SymbolKind::Struct; + case index::SymbolKind::Class: + return SymbolKind::Class; + case index::SymbolKind::Protocol: + return SymbolKind::Interface; + case index::SymbolKind::Extension: + return SymbolKind::Interface; + case index::SymbolKind::Union: + return SymbolKind::Class; + case index::SymbolKind::TypeAlias: + return SymbolKind::Class; + case index::SymbolKind::Function: + return SymbolKind::Function; + case index::SymbolKind::Variable: + return SymbolKind::Variable; + case index::SymbolKind::Field: + return SymbolKind::Field; + case index::SymbolKind::EnumConstant: + return SymbolKind::EnumMember; + case index::SymbolKind::InstanceMethod: + case index::SymbolKind::ClassMethod: + case index::SymbolKind::StaticMethod: + return SymbolKind::Method; + case index::SymbolKind::InstanceProperty: + case index::SymbolKind::ClassProperty: + case index::SymbolKind::StaticProperty: + return SymbolKind::Property; + case index::SymbolKind::Constructor: + case index::SymbolKind::Destructor: + return SymbolKind::Method; + case index::SymbolKind::ConversionFunction: + return SymbolKind::Function; + case index::SymbolKind::Parameter: + return SymbolKind::Variable; + case index::SymbolKind::Using: + return SymbolKind::Namespace; + } + llvm_unreachable("invalid symbol kind"); +} +} // namespace + +llvm::Expected<std::vector<SymbolInformation>> +getWorkspaceSymbols(StringRef Query, const WorkspaceSymbolOptions &Opts, + const SymbolIndex *const Index, + IntrusiveRefCntPtr<vfs::FileSystem> VFS) { + std::vector<SymbolInformation> Result; + if (Query.empty() || !Index) + return Result; + + auto Names = splitQualifiedName(Query); + + // We'll use a temporary SourceManager to do the offset -> line/col mapping. + // We don't have any context from which this query was launched (working dir), + // so use defaults here. + FileSystemOptions FileOpts; + FileManager FM(FileOpts, VFS); + IntrusiveRefCntPtr<DiagnosticsEngine> DE( + CompilerInstance::createDiagnostics(new DiagnosticOptions)); + SourceManager TempSM(*DE, FM); + + // Global scope is represented by "" in FuzzyFind. + if (Names.first.startswith("::")) + Names.first = Names.first.substr(2); + + FuzzyFindRequest Req; + Req.Query = Names.second; + if (!Names.first.empty()) + Req.Scopes.push_back(Names.first); + if (Opts.Limit) + Req.MaxCandidateCount = Opts.Limit; + Index->fuzzyFind(Req, [&TempSM, &Result, &Opts](const Symbol &Sym) { + auto &CD = Sym.Definition ? Sym.Definition : Sym.CanonicalDeclaration; + auto Uri = URI::parse(CD.FileURI); + if (!Uri) { + log(llvm::formatv( + "Workspace symbol: Could not parse URI '{0}' for symbol '{1}'.", + CD.FileURI, Sym.Name)); + return; + } + auto Path = URI::resolve(*Uri); + if (!Path) { + log(llvm::formatv("Workspace symbol: Could not resolve path for URI " + "'{0}' for symbol '{1}'.", + (*Uri).toString(), Sym.Name.str())); + return; + } + auto L = + offsetRangeToLocation(TempSM, *Path, CD.StartOffset, CD.StartOffset); + if (L) { + SymbolKind SK = + adjustKindToCapability(indexSymbolKindToSymbolKind(Sym.SymInfo.Kind), + Opts.supportedSymbolKinds); + Result.push_back({Sym.Name, SK, *L, Sym.Scope}); + } + }); + std::sort(Result.begin(), Result.end(), + [](const SymbolInformation &A, const SymbolInformation &B) { + return A.name < B.name; + }); + return Result; +} + +} // namespace clangd +} // namespace clang Index: clangd/SourceCode.h =================================================================== --- clangd/SourceCode.h +++ clangd/SourceCode.h @@ -49,6 +49,22 @@ // Note that clang also uses closed source ranges, which this can't handle! Range halfOpenToRange(const SourceManager &SM, CharSourceRange R); +/// Turn a SourceRange into a Location. +llvm::Optional<Location> +sourceRangeToLocation(const SourceManager &SourceMgr, + const SourceRange &ValSourceRange); + +/// Turn a pair of offsets into a Location. +llvm::Optional<Location> offsetRangeToLocation(SourceManager &SourceMgr, + StringRef File, + size_t OffsetStart, + size_t OffsetEnd); + +/// From "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no +/// qualifier. +std::pair<llvm::StringRef, llvm::StringRef> +splitQualifiedName(llvm::StringRef QName); + } // namespace clangd } // namespace clang #endif Index: clangd/SourceCode.cpp =================================================================== --- clangd/SourceCode.cpp +++ clangd/SourceCode.cpp @@ -7,10 +7,12 @@ // //===----------------------------------------------------------------------===// #include "SourceCode.h" +#include "Logger.h" #include "clang/Basic/SourceManager.h" #include "llvm/Support/Errc.h" #include "llvm/Support/Error.h" +#include "llvm/Support/Path.h" namespace clang { namespace clangd { @@ -76,5 +78,62 @@ return {Begin, End}; } +llvm::Optional<Location> +sourceRangeToLocation(const SourceManager &SourceMgr, + const SourceRange &ValSourceRange) { + SourceLocation LocStart = ValSourceRange.getBegin(); + const FileEntry *FE = + SourceMgr.getFileEntryForID(SourceMgr.getFileID(LocStart)); + if (!FE) + return llvm::None; + SourceLocation LocEnd = ValSourceRange.getEnd(); + Position Begin = sourceLocToPosition(SourceMgr, LocStart); + Position End = sourceLocToPosition(SourceMgr, LocEnd); + Range R = {Begin, End}; + Location L; + + SmallString<64> FilePath = FE->tryGetRealPathName(); + if (FilePath.empty()) + FilePath = FE->getName(); + if (!llvm::sys::path::is_absolute(FilePath)) { + if (!SourceMgr.getFileManager().makeAbsolutePath(FilePath)) { + log("Could not turn relative path to absolute: " + FilePath); + return llvm::None; + } + } + + L.uri = URIForFile(FilePath.str()); + L.range = R; + return L; +} + +llvm::Optional<Location> offsetRangeToLocation(SourceManager &SourceMgr, + StringRef File, + size_t OffsetStart, + size_t OffsetEnd) { + const FileEntry *FE = SourceMgr.getFileManager().getFile(File); + if (!FE) + return llvm::None; + + FileID FID = SourceMgr.getOrCreateFileID(FE, SrcMgr::C_User); + + SourceLocation LocStart = SourceMgr.getComposedLoc(FID, OffsetStart); + SourceLocation LocEnd = SourceMgr.getComposedLoc(FID, OffsetEnd); + if (LocStart.isInvalid() || LocEnd.isInvalid()) + return llvm::None; + + return sourceRangeToLocation(SourceMgr, {LocStart, LocEnd}); +} + +/// From "a::b::c", return {"a::b::", "c"}. Scope is empty if there's no +/// qualifier. +std::pair<llvm::StringRef, llvm::StringRef> +splitQualifiedName(llvm::StringRef QName) { + size_t Pos = QName.rfind("::"); + if (Pos == llvm::StringRef::npos) + return {StringRef(), QName}; + return {QName.substr(0, Pos + 2), QName.substr(Pos + 2)}; +} + } // namespace clangd } // namespace clang Index: clangd/ProtocolHandlers.h =================================================================== --- clangd/ProtocolHandlers.h +++ clangd/ProtocolHandlers.h @@ -49,6 +49,7 @@ virtual void onSwitchSourceHeader(TextDocumentIdentifier &Params) = 0; virtual void onFileEvent(DidChangeWatchedFilesParams &Params) = 0; virtual void onCommand(ExecuteCommandParams &Params) = 0; + virtual void onWorkspaceSymbol(WorkspaceSymbolParams &Params) = 0; virtual void onRename(RenameParams &Parames) = 0; virtual void onDocumentHighlight(TextDocumentPositionParams &Params) = 0; virtual void onHover(TextDocumentPositionParams &Params) = 0; Index: clangd/ProtocolHandlers.cpp =================================================================== --- clangd/ProtocolHandlers.cpp +++ clangd/ProtocolHandlers.cpp @@ -72,4 +72,5 @@ &ProtocolCallbacks::onDocumentHighlight); Register("workspace/didChangeConfiguration", &ProtocolCallbacks::onChangeConfiguration); + Register("workspace/symbol", &ProtocolCallbacks::onWorkspaceSymbol); } Index: clangd/Protocol.h =================================================================== --- clangd/Protocol.h +++ clangd/Protocol.h @@ -223,6 +223,59 @@ }; bool fromJSON(const json::Expr &, CompletionClientCapabilities &); +/// A symbol kind. +enum class SymbolKind { + File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26 +}; +bool fromJSON(const json::Expr &, SymbolKind &); + +struct SymbolKindCapabilities { + /// The SymbolKinds that the client supports. If not set, the client only + /// supports <= SymbolKind::Array and will not fall back to a valid default + /// value. + llvm::Optional<std::vector<SymbolKind>> valueSet; +}; +bool fromJSON(const json::Expr &, SymbolKindCapabilities &); + +struct WorkspaceSymbolCapabilities { + /// Capabilities SymbolKind. + llvm::Optional<SymbolKindCapabilities> symbolKind; +}; +bool fromJSON(const json::Expr &, WorkspaceSymbolCapabilities &); + +// FIXME: most of the capabilities are missing from this struct. Only the ones +// used by clangd are currently there. +struct WorkspaceClientCapabilities { + /// Capabilities specific to `workspace/symbol`. + llvm::Optional<WorkspaceSymbolCapabilities> symbol; +}; +bool fromJSON(const json::Expr &, WorkspaceClientCapabilities &); + // FIXME: most of the capabilities are missing from this struct. Only the ones // used by clangd are currently there. struct TextDocumentClientCapabilities { @@ -233,8 +286,7 @@ struct ClientCapabilities { // Workspace specific client capabilities. - // NOTE: not used by clangd at the moment. - // WorkspaceClientCapabilities workspace; + llvm::Optional<WorkspaceClientCapabilities> workspace; // Text document specific client capabilities. TextDocumentClientCapabilities textDocument; @@ -505,6 +557,30 @@ json::Expr toJSON(const Command &C); +/// Represents information about programming constructs like variables, classes, +/// interfaces etc. +struct SymbolInformation { + /// The name of this symbol. + std::string name; + + /// The kind of this symbol. + SymbolKind kind; + + /// The location of this symbol. + Location location; + + /// The name of the symbol containing this symbol. + std::string containerName; +}; +json::Expr toJSON(const SymbolInformation &); + +/// The parameters of a Workspace Symbol Request. +struct WorkspaceSymbolParams { + /// A non-empty query string + std::string query; +}; +bool fromJSON(const json::Expr &, WorkspaceSymbolParams &); + struct ApplyWorkspaceEditParams { WorkspaceEdit edit; }; Index: clangd/Protocol.cpp =================================================================== --- clangd/Protocol.cpp +++ clangd/Protocol.cpp @@ -176,6 +176,41 @@ return true; } +bool fromJSON(const json::Expr &E, SymbolKind &Out) { + if (auto T = E.asInteger()) { + if (*T < static_cast<int>(SymbolKind::File) || + *T > static_cast<int>(SymbolKind::TypeParameter)) + return false; + Out = static_cast<SymbolKind>(*T); + return true; + } + return false; +} + +bool fromJSON(const json::Expr &Params, SymbolKindCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("valueSet", R.valueSet); + return true; +} + +bool fromJSON(const json::Expr &Params, WorkspaceSymbolCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("symbolKind", R.symbolKind); + return true; +} + +bool fromJSON(const json::Expr &Params, WorkspaceClientCapabilities &R) { + json::ObjectMapper O(Params); + if (!O) + return false; + O.map("symbol", R.symbol); + return true; +} + bool fromJSON(const json::Expr &Params, TextDocumentClientCapabilities &R) { json::ObjectMapper O(Params); if (!O) @@ -350,6 +385,20 @@ return false; // Unrecognized command. } +json::Expr toJSON(const SymbolInformation &P) { + return json::obj{ + {"name", P.name}, + {"kind", static_cast<int>(P.kind)}, + {"location", P.location}, + {"containerName", P.containerName}, + }; +} + +bool fromJSON(const json::Expr &Params, WorkspaceSymbolParams &R) { + json::ObjectMapper O(Params); + return O && O.map("query", R.query); +} + json::Expr toJSON(const Command &C) { auto Cmd = json::obj{{"title", C.title}, {"command", C.command}}; if (C.workspaceEdit) Index: clangd/CodeComplete.cpp =================================================================== --- clangd/CodeComplete.cpp +++ clangd/CodeComplete.cpp @@ -919,6 +919,7 @@ if (Opts.Limit) Req.MaxCandidateCount = Opts.Limit; Req.Query = Filter->pattern(); + Req.CompletionMatchesOnly = true; Req.Scopes = getQueryScopes(Recorder->CCContext, Recorder->CCSema->getSourceManager()); log(llvm::formatv("Code complete: fuzzyFind(\"{0}\", scopes=[{1}])", Index: clangd/ClangdServer.h =================================================================== --- clangd/ClangdServer.h +++ clangd/ClangdServer.h @@ -33,6 +33,7 @@ class PCHContainerOperations; namespace clangd { +struct WorkspaceSymbolOptions; class DiagnosticsConsumer { public: @@ -157,6 +158,11 @@ /// Get code hover for a given position. void findHover(PathRef File, Position Pos, Callback<Hover> CB); + void onWorkspaceSymbol( + StringRef Query, const clangd::WorkspaceSymbolOptions &Opts, + UniqueFunction<void(llvm::Expected<std::vector<SymbolInformation>>)> + Callback); + /// Run formatting for \p Rng inside \p File with content \p Code. llvm::Expected<tooling::Replacements> formatRange(StringRef Code, PathRef File, Range Rng); Index: clangd/ClangdServer.cpp =================================================================== --- clangd/ClangdServer.cpp +++ clangd/ClangdServer.cpp @@ -11,6 +11,7 @@ #include "CodeComplete.h" #include "Headers.h" #include "SourceCode.h" +#include "WorkspaceSymbols.h" #include "XRefs.h" #include "index/Merge.h" #include "clang/Format/Format.h" @@ -492,6 +493,14 @@ // invalidating other caches. } +void ClangdServer::onWorkspaceSymbol( + StringRef Query, const clangd::WorkspaceSymbolOptions &Opts, + UniqueFunction<void(llvm::Expected<std::vector<SymbolInformation>>)> + Callback) { + Callback(clangd::getWorkspaceSymbols(Query, Opts, Index, + FSProvider.getFileSystem())); +} + std::vector<std::pair<Path, std::size_t>> ClangdServer::getUsedBytesPerFile() const { return WorkScheduler.getUsedBytesPerFile(); Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -16,6 +16,7 @@ #include "Path.h" #include "Protocol.h" #include "ProtocolHandlers.h" +#include "WorkspaceSymbols.h" #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/Optional.h" @@ -33,6 +34,7 @@ /// loaded only from \p CompileCommandsDir. Otherwise, clangd will look /// for compile_commands.json in all parent directories of each file. ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::WorkspaceSymbolOptions &WorkspaceSymOpts, llvm::Optional<Path> CompileCommandsDir, const ClangdServer::Options &Opts); @@ -69,6 +71,7 @@ void onDocumentHighlight(TextDocumentPositionParams &Params) override; void onFileEvent(DidChangeWatchedFilesParams &Params) override; void onCommand(ExecuteCommandParams &Params) override; + void onWorkspaceSymbol(WorkspaceSymbolParams &Params) override; void onRename(RenameParams &Parames) override; void onHover(TextDocumentPositionParams &Params) override; void onChangeConfiguration(DidChangeConfigurationParams &Params) override; @@ -89,6 +92,8 @@ /// Language Server client. /// It's used to break out of the LSP parsing loop. bool IsDone = false; + /// Indicates whether or not the index is available. + bool SymbolIndexAvailable = false; std::mutex FixItsMutex; typedef std::map<clangd::Diagnostic, std::vector<Fix>, LSPDiagnosticCompare> @@ -102,6 +107,8 @@ RealFileSystemProvider FSProvider; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; + /// Options used for workspace symbol. + clangd::WorkspaceSymbolOptions WorkspaceSymbolOpts; // Store of the current versions of the open documents. DraftStore DraftMgr; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -96,6 +96,10 @@ CCOpts.EnableSnippets = Params.capabilities.textDocument.completion.completionItem.snippetSupport; + if (Params.capabilities.workspace && Params.capabilities.workspace->symbol && + Params.capabilities.workspace->symbol->symbolKind) + WorkspaceSymbolOpts.supportedSymbolKinds = + Params.capabilities.workspace->symbol->symbolKind->valueSet; reply(json::obj{ {{"capabilities", @@ -122,6 +126,7 @@ {"documentHighlightProvider", true}, {"hoverProvider", true}, {"renameProvider", true}, + {"workspaceSymbolProvider", SymbolIndexAvailable}, {"executeCommandProvider", json::obj{ {"commands", @@ -240,6 +245,17 @@ } } +void ClangdLSPServer::onWorkspaceSymbol(WorkspaceSymbolParams &Params) { + Server.onWorkspaceSymbol( + Params.query, WorkspaceSymbolOpts, + [](llvm::Expected<std::vector<SymbolInformation>> Items) { + if (!Items) + return replyError(ErrorCode::InvalidParams, + llvm::toString(Items.takeError())); + reply(json::ary(*Items)); + }); +} + void ClangdLSPServer::onRename(RenameParams &Params) { Path File = Params.textDocument.uri.file(); llvm::Optional<std::string> Code = DraftMgr.getDraft(File); @@ -412,11 +428,13 @@ } } -ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, - const clangd::CodeCompleteOptions &CCOpts, - llvm::Optional<Path> CompileCommandsDir, - const ClangdServer::Options &Opts) - : Out(Out), CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts), +ClangdLSPServer::ClangdLSPServer( + JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::WorkspaceSymbolOptions &WorkspaceSymOpts, + llvm::Optional<Path> CompileCommandsDir, const ClangdServer::Options &Opts) + : Out(Out), SymbolIndexAvailable(Opts.BuildDynamicSymbolIndex), + CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts), + WorkspaceSymbolOpts(WorkspaceSymOpts), Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} bool ClangdLSPServer::run(std::istream &In, JSONStreamStyle InputStyle) { Index: clangd/CMakeLists.txt =================================================================== --- clangd/CMakeLists.txt +++ clangd/CMakeLists.txt @@ -27,6 +27,7 @@ Trace.cpp TUScheduler.cpp URI.cpp + WorkspaceSymbols.cpp XRefs.cpp index/CanonicalIncludes.cpp index/FileIndex.cpp
_______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org http://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits