[clangd] LSP extension to switch between source/header file

Summary:
Small extension to LSP to allow clients to use clangd to switch between C header files and source files.
Final version will use the completed clangd indexer to use the index of symbols to be able to switch from header to source file when the file names don't match.

Reviewers: malaperle, krasimir, bkramer, ilya-biryukov

Reviewed By: ilya-biryukov

Subscribers: ilya-biryukov, cfe-commits, arphaman

Patch by: William Enright

Differential Revision: https://reviews.llvm.org/D36150

llvm-svn: 314377
This commit is contained in:
Marc-Andre Laperle 2017-09-28 03:14:40 +00:00
parent e9165f8720
commit 6571b3edd0
6 changed files with 181 additions and 0 deletions

View File

@ -72,6 +72,8 @@ public:
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 @@ void ClangdLSPServer::LSPProtocolCallbacks::onGoToDefinition(
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)

View File

@ -296,6 +296,66 @@ Tagged<std::vector<Location>> ClangdServer::findDefinitions(PathRef File,
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) {

View File

@ -248,6 +248,10 @@ public:
/// 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.

View File

@ -210,6 +210,22 @@ private:
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 @@ 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));
}

View File

@ -49,6 +49,8 @@ public:
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,

View File

@ -7,6 +7,7 @@
//
//===----------------------------------------------------------------------===//
#include "ClangdLSPServer.h"
#include "ClangdServer.h"
#include "Logger.h"
#include "clang/Basic/VirtualFileSystem.h"
@ -899,6 +900,84 @@ int d;
}
}
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: