Index: clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/trunk/clangd/ClangdLSPServer.cpp @@ -72,6 +72,8 @@ JSONOutput &Out) override; void onGoToDefinition(TextDocumentPositionParams Params, StringRef ID, JSONOutput &Out) override; + void onSwitchSourceHeader(TextDocumentIdentifier Params, StringRef ID, + JSONOutput &Out) override; private: ClangdLSPServer &LangServer; @@ -226,6 +228,21 @@ R"(,"result":[)" + Locations + R"(]})"); } +void ClangdLSPServer::LSPProtocolCallbacks::onSwitchSourceHeader( + TextDocumentIdentifier Params, StringRef ID, JSONOutput &Out) { + llvm::Optional Result = + LangServer.Server.switchSourceHeader(Params.uri.file); + std::string ResultUri; + if (Result) + ResultUri = URI::unparse(URI::fromFile(*Result)); + else + ResultUri = "\"\""; + + Out.writeMessage( + R"({"jsonrpc":"2.0","id":)" + ID.str() + + R"(,"result":)" + ResultUri + R"(})"); +} + ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, unsigned AsyncThreadsCount, bool SnippetCompletions, llvm::Optional ResourceDir) Index: clang-tools-extra/trunk/clangd/ClangdServer.h =================================================================== --- clang-tools-extra/trunk/clangd/ClangdServer.h +++ clang-tools-extra/trunk/clangd/ClangdServer.h @@ -248,6 +248,10 @@ /// Get definition of symbol at a specified \p Line and \p Column in \p File. Tagged> findDefinitions(PathRef File, Position Pos); + /// Helper function that returns a path to the corresponding source file when + /// given a header file and vice versa. + llvm::Optional switchSourceHeader(PathRef Path); + /// Run formatting for \p Rng inside \p File. std::vector formatRange(PathRef File, Range Rng); /// Run formatting for the whole \p File. Index: clang-tools-extra/trunk/clangd/ClangdServer.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ClangdServer.cpp +++ clang-tools-extra/trunk/clangd/ClangdServer.cpp @@ -296,6 +296,66 @@ return make_tagged(std::move(Result), TaggedFS.Tag); } +llvm::Optional ClangdServer::switchSourceHeader(PathRef Path) { + + StringRef SourceExtensions[] = {".cpp", ".c", ".cc", ".cxx", + ".c++", ".m", ".mm"}; + StringRef HeaderExtensions[] = {".h", ".hh", ".hpp", ".hxx", ".inc"}; + + StringRef PathExt = llvm::sys::path::extension(Path); + + // Lookup in a list of known extensions. + auto SourceIter = + std::find_if(std::begin(SourceExtensions), std::end(SourceExtensions), + [&PathExt](PathRef SourceExt) { + return SourceExt.equals_lower(PathExt); + }); + bool IsSource = SourceIter != std::end(SourceExtensions); + + auto HeaderIter = + std::find_if(std::begin(HeaderExtensions), std::end(HeaderExtensions), + [&PathExt](PathRef HeaderExt) { + return HeaderExt.equals_lower(PathExt); + }); + + bool IsHeader = HeaderIter != std::end(HeaderExtensions); + + // We can only switch between extensions known extensions. + if (!IsSource && !IsHeader) + return llvm::None; + + // Array to lookup extensions for the switch. An opposite of where original + // extension was found. + ArrayRef NewExts; + if (IsSource) + NewExts = HeaderExtensions; + else + NewExts = SourceExtensions; + + // Storage for the new path. + SmallString<128> NewPath = StringRef(Path); + + // Instance of vfs::FileSystem, used for file existence checks. + auto FS = FSProvider.getTaggedFileSystem(Path).Value; + + // Loop through switched extension candidates. + for (StringRef NewExt : NewExts) { + llvm::sys::path::replace_extension(NewPath, NewExt); + if (FS->exists(NewPath)) + return NewPath.str().str(); // First str() to convert from SmallString to + // StringRef, second to convert from StringRef + // to std::string + + // Also check NewExt in upper-case, just in case. + llvm::sys::path::replace_extension(NewPath, NewExt.upper()); + if (FS->exists(NewPath)) + return NewPath.str().str(); + + } + + return llvm::None; +} + std::future ClangdServer::scheduleReparseAndDiags( PathRef File, VersionedDraft Contents, std::shared_ptr Resources, Tagged> TaggedFS) { Index: clang-tools-extra/trunk/clangd/ProtocolHandlers.h =================================================================== --- clang-tools-extra/trunk/clangd/ProtocolHandlers.h +++ clang-tools-extra/trunk/clangd/ProtocolHandlers.h @@ -49,6 +49,8 @@ JSONOutput &Out) = 0; virtual void onGoToDefinition(TextDocumentPositionParams Params, StringRef ID, JSONOutput &Out) = 0; + virtual void onSwitchSourceHeader(TextDocumentIdentifier Params, StringRef ID, + JSONOutput &Out) = 0; }; void regiterCallbackHandlers(JSONRPCDispatcher &Dispatcher, JSONOutput &Out, Index: clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp =================================================================== --- clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp +++ clang-tools-extra/trunk/clangd/ProtocolHandlers.cpp @@ -210,6 +210,22 @@ ProtocolCallbacks &Callbacks; }; +struct SwitchSourceHeaderHandler : Handler { + SwitchSourceHeaderHandler(JSONOutput &Output, ProtocolCallbacks &Callbacks) + : Handler(Output), Callbacks(Callbacks) {} + + void handleMethod(llvm::yaml::MappingNode *Params, StringRef ID) override { + auto TDPP = TextDocumentIdentifier::parse(Params, Output); + if (!TDPP) + return; + + Callbacks.onSwitchSourceHeader(*TDPP, ID, Output); + } + +private: + ProtocolCallbacks &Callbacks; +}; + } // namespace void clangd::regiterCallbackHandlers(JSONRPCDispatcher &Dispatcher, @@ -246,4 +262,7 @@ Dispatcher.registerHandler( "textDocument/definition", llvm::make_unique(Out, Callbacks)); + Dispatcher.registerHandler( + "textDocument/switchSourceHeader", + llvm::make_unique(Out, Callbacks)); } Index: clang-tools-extra/trunk/unittests/clangd/ClangdTests.cpp =================================================================== --- clang-tools-extra/trunk/unittests/clangd/ClangdTests.cpp +++ clang-tools-extra/trunk/unittests/clangd/ClangdTests.cpp @@ -7,6 +7,7 @@ // //===----------------------------------------------------------------------===// +#include "ClangdLSPServer.h" #include "ClangdServer.h" #include "Logger.h" #include "clang/Basic/VirtualFileSystem.h" @@ -899,6 +900,84 @@ } } +TEST_F(ClangdVFSTest, CheckSourceHeaderSwitch) { + MockFSProvider FS; + ErrorCheckingDiagConsumer DiagConsumer; + MockCompilationDatabase CDB(/*AddFreestandingFlag=*/true); + + ClangdServer Server(CDB, DiagConsumer, FS, getDefaultAsyncThreadsCount(), + /*SnippetCompletions=*/false, EmptyLogger::getInstance()); + + auto SourceContents = R"cpp( + #include "foo.h" + int b = a; + )cpp"; + + auto FooCpp = getVirtualTestFilePath("foo.cpp"); + auto FooH = getVirtualTestFilePath("foo.h"); + auto Invalid = getVirtualTestFilePath("main.cpp"); + + FS.Files[FooCpp] = SourceContents; + FS.Files[FooH] = "int a;"; + FS.Files[Invalid] = "int main() { \n return 0; \n }"; + + llvm::Optional PathResult = Server.switchSourceHeader(FooCpp); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooH); + + PathResult = Server.switchSourceHeader(FooH); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooCpp); + + SourceContents = R"c( + #include "foo.HH" + int b = a; + )c"; + + // Test with header file in capital letters and different extension, source + // file with different extension + auto FooC = getVirtualTestFilePath("bar.c"); + auto FooHH = getVirtualTestFilePath("bar.HH"); + + FS.Files[FooC] = SourceContents; + FS.Files[FooHH] = "int a;"; + + PathResult = Server.switchSourceHeader(FooC); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), FooHH); + + // Test with both capital letters + auto Foo2C = getVirtualTestFilePath("foo2.C"); + auto Foo2HH = getVirtualTestFilePath("foo2.HH"); + FS.Files[Foo2C] = SourceContents; + FS.Files[Foo2HH] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo2C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo2HH); + + // Test with source file as capital letter and .hxx header file + auto Foo3C = getVirtualTestFilePath("foo3.C"); + auto Foo3HXX = getVirtualTestFilePath("foo3.hxx"); + + SourceContents = R"c( + #include "foo3.hxx" + int b = a; + )c"; + + FS.Files[Foo3C] = SourceContents; + FS.Files[Foo3HXX] = "int a;"; + + PathResult = Server.switchSourceHeader(Foo3C); + EXPECT_TRUE(PathResult.hasValue()); + ASSERT_EQ(PathResult.getValue(), Foo3HXX); + + // Test if asking for a corresponding file that doesn't exist returns an empty + // string. + PathResult = Server.switchSourceHeader(Invalid); + EXPECT_FALSE(PathResult.hasValue()); +} + TEST_F(ClangdThreadingTest, NoConcurrentDiagnostics) { class NoConcurrentAccessDiagConsumer : public DiagnosticsConsumer { public: