JSONOutput &Out) override;
void onGoToDefinition(TextDocumentPositionParams Params, StringRef ID,
JSONOutput &Out) override;
+ void onSwitchSourceHeader(TextDocumentIdentifier Params, StringRef ID,
+ JSONOutput &Out) override;
private:
ClangdLSPServer &LangServer;
R"(,"result":[)" + Locations + R"(]})");
}
+void ClangdLSPServer::LSPProtocolCallbacks::onSwitchSourceHeader(
+ TextDocumentIdentifier Params, StringRef ID, JSONOutput &Out) {
+ llvm::Optional<Path> 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<StringRef> ResourceDir)
return make_tagged(std::move(Result), TaggedFS.Tag);
}
+llvm::Optional<Path> 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<StringRef> 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<void> ClangdServer::scheduleReparseAndDiags(
PathRef File, VersionedDraft Contents, std::shared_ptr<CppFile> Resources,
Tagged<IntrusiveRefCntPtr<vfs::FileSystem>> TaggedFS) {
/// Get definition of symbol at a specified \p Line and \p Column in \p File.
Tagged<std::vector<Location>> 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<Path> switchSourceHeader(PathRef Path);
+
/// Run formatting for \p Rng inside \p File.
std::vector<tooling::Replacement> formatRange(PathRef File, Range Rng);
/// Run formatting for the whole \p File.
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,
Dispatcher.registerHandler(
"textDocument/definition",
llvm::make_unique<GotoDefinitionHandler>(Out, Callbacks));
+ Dispatcher.registerHandler(
+ "textDocument/switchSourceHeader",
+ llvm::make_unique<SwitchSourceHeaderHandler>(Out, Callbacks));
}
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,
//
//===----------------------------------------------------------------------===//
+#include "ClangdLSPServer.h"
#include "ClangdServer.h"
#include "Logger.h"
#include "clang/Basic/VirtualFileSystem.h"
}
}
+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<Path> 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: