[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:
parent
e9165f8720
commit
6571b3edd0
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue