diff --git a/clang-tools-extra/clangd/ClangdLSPServer.h b/clang-tools-extra/clangd/ClangdLSPServer.h --- a/clang-tools-extra/clangd/ClangdLSPServer.h +++ b/clang-tools-extra/clangd/ClangdLSPServer.h @@ -87,6 +87,8 @@ // otherwise. void onDocumentSymbol(const DocumentSymbolParams &, Callback); + void onFoldingRange(const FoldingRangeParams &, + Callback>); void onCodeAction(const CodeActionParams &, Callback); void onCompletion(const CompletionParams &, Callback); void onSignatureHelp(const TextDocumentPositionParams &, diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -637,6 +637,8 @@ ->insert( {"semanticHighlighting", llvm::json::Object{{"scopes", buildHighlightScopeLookupTable()}}}); + if (ClangdServerOpts.FoldingRanges) + Result.getObject("capabilities")->insert({"foldingRangeProvider", true}); Reply(std::move(Result)); } @@ -922,7 +924,6 @@ static std::vector flattenSymbolHierarchy(llvm::ArrayRef Symbols, const URIForFile &FileURI) { - std::vector Results; std::function Process = [&](const DocumentSymbol &S, llvm::Optional ParentName) { @@ -961,6 +962,12 @@ }); } +void ClangdLSPServer::onFoldingRange( + const FoldingRangeParams &Params, + Callback> Reply) { + Server->foldingRanges(Params.textDocument.uri.file(), std::move(Reply)); +} + static llvm::Optional asCommand(const CodeAction &Action) { Command Cmd; if (Action.command && Action.edit) @@ -1388,6 +1395,8 @@ MsgHandler->bind("textDocument/documentLink", &ClangdLSPServer::onDocumentLink); MsgHandler->bind("textDocument/semanticTokens", &ClangdLSPServer::onSemanticTokens); MsgHandler->bind("textDocument/semanticTokens/edits", &ClangdLSPServer::onSemanticTokensEdits); + if (Opts.FoldingRanges) + MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange); // clang-format on } diff --git a/clang-tools-extra/clangd/ClangdServer.h b/clang-tools-extra/clangd/ClangdServer.h --- a/clang-tools-extra/clangd/ClangdServer.h +++ b/clang-tools-extra/clangd/ClangdServer.h @@ -153,6 +153,9 @@ /// Enable notification-based semantic highlighting. bool TheiaSemanticHighlighting = false; + /// Enable preview of FoldingRanges feature. + bool FoldingRanges = false; + /// Returns true if the tweak should be enabled. std::function TweakFilter = [](const Tweak &T) { return !T.hidden(); // only enable non-hidden tweaks. @@ -242,6 +245,9 @@ void documentSymbols(StringRef File, Callback> CB); + /// Retrieve ranges that can be used to fold code within the specified file. + void foldingRanges(StringRef File, Callback> CB); + /// Retrieve locations for symbol references. void findReferences(PathRef File, Position Pos, uint32_t Limit, Callback CB); diff --git a/clang-tools-extra/clangd/ClangdServer.cpp b/clang-tools-extra/clangd/ClangdServer.cpp --- a/clang-tools-extra/clangd/ClangdServer.cpp +++ b/clang-tools-extra/clangd/ClangdServer.cpp @@ -655,6 +655,18 @@ TUScheduler::InvalidateOnUpdate); } +void ClangdServer::foldingRanges(llvm::StringRef File, + Callback> CB) { + auto Action = + [CB = std::move(CB)](llvm::Expected InpAST) mutable { + if (!InpAST) + return CB(InpAST.takeError()); + CB(clangd::getFoldingRanges(InpAST->AST)); + }; + WorkScheduler.runWithAST("foldingRanges", File, std::move(Action), + TUScheduler::InvalidateOnUpdate); +} + void ClangdServer::findReferences(PathRef File, Position Pos, uint32_t Limit, Callback CB) { auto Action = [Pos, Limit, CB = std::move(CB), diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -1510,6 +1510,23 @@ }; llvm::json::Value toJSON(const DocumentLink &DocumentLink); +// FIXME(kirillbobyrev): Add FoldingRangeClientCapabilities so we can support +// per-line-folding editors. +struct FoldingRangeParams { + TextDocumentIdentifier textDocument; +}; +bool fromJSON(const llvm::json::Value &, FoldingRangeParams &); + +/// Stores information about a region of code that can be folded. +struct FoldingRange { + unsigned startLine = 0; + llvm::Optional startCharacter; + unsigned endLine = 0; + llvm::Optional endCharacter; + llvm::Optional kind; +}; +llvm::json::Value toJSON(const FoldingRange &Range); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -1241,5 +1241,24 @@ }; } +bool fromJSON(const llvm::json::Value &Params, FoldingRangeParams &R) { + llvm::json::ObjectMapper O(Params); + return O && O.map("textDocument", R.textDocument); +} + +llvm::json::Value toJSON(const FoldingRange &Range) { + llvm::json::Object Result{ + {"startLine", Range.startLine}, + {"endLine", Range.endLine}, + }; + if (Range.startCharacter) + Result["startCharacter"] = *Range.startCharacter; + if (Range.endCharacter) + Result["endCharacter"] = *Range.endCharacter; + if (Range.kind) + Result["kind"] = *Range.kind; + return Result; +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/SemanticSelection.h b/clang-tools-extra/clangd/SemanticSelection.h --- a/clang-tools-extra/clangd/SemanticSelection.h +++ b/clang-tools-extra/clangd/SemanticSelection.h @@ -25,6 +25,10 @@ /// If pos is not in any interesting range, return [Pos, Pos). llvm::Expected getSemanticRanges(ParsedAST &AST, Position Pos); +/// Returns a list of ranges whose contents might be collapsible in an editor. +/// This should include large scopes, preprocessor blocks etc. +llvm::Expected> getFoldingRanges(ParsedAST &AST); + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/SemanticSelection.cpp b/clang-tools-extra/clangd/SemanticSelection.cpp --- a/clang-tools-extra/clangd/SemanticSelection.cpp +++ b/clang-tools-extra/clangd/SemanticSelection.cpp @@ -6,6 +6,7 @@ // //===----------------------------------------------------------------------===// #include "SemanticSelection.h" +#include "FindSymbols.h" #include "ParsedAST.h" #include "Protocol.h" #include "Selection.h" @@ -18,6 +19,7 @@ namespace clang { namespace clangd { namespace { + // Adds Range \p R to the Result if it is distinct from the last added Range. // Assumes that only consecutive ranges can coincide. void addIfDistinct(const Range &R, std::vector &Result) { @@ -25,6 +27,20 @@ Result.push_back(R); } } + +// Recursively collects FoldingRange from a symbol and its children. +void collectFoldingRanges(DocumentSymbol Symbol, + std::vector &Result) { + FoldingRange Range; + Range.startLine = Symbol.range.start.line; + Range.startCharacter = Symbol.range.start.character; + Range.endLine = Symbol.range.end.line; + Range.endCharacter = Symbol.range.end.character; + Result.push_back(Range); + for (const auto &Child : Symbol.children) + collectFoldingRanges(Child, Result); +} + } // namespace llvm::Expected getSemanticRanges(ParsedAST &AST, Position Pos) { @@ -81,5 +97,24 @@ return std::move(Head); } +// FIXME(kirillbobyrev): Collect comments, PP conditional regions, includes and +// other code regions (e.g. public/private/protected sections of classes, +// control flow statement bodies). +// Related issue: +// https://github.com/clangd/clangd/issues/310 +llvm::Expected> getFoldingRanges(ParsedAST &AST) { + // FIXME(kirillbobyrev): getDocumentSymbols() is conveniently available but + // limited (e.g. doesn't yield blocks inside functions and provides ranges for + // nodes themselves instead of their contents which is less useful). Replace + // this with a more general RecursiveASTVisitor implementation instead. + auto DocumentSymbols = getDocumentSymbols(AST); + if (!DocumentSymbols) + return DocumentSymbols.takeError(); + std::vector Result; + for (const auto &Symbol : *DocumentSymbols) + collectFoldingRanges(Symbol, Result); + return Result; +} + } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/tool/ClangdMain.cpp b/clang-tools-extra/clangd/tool/ClangdMain.cpp --- a/clang-tools-extra/clangd/tool/ClangdMain.cpp +++ b/clang-tools-extra/clangd/tool/ClangdMain.cpp @@ -296,6 +296,14 @@ Hidden, }; +opt FoldingRanges{ + "folding-ranges", + cat(Features), + desc("Enable preview of FoldingRanges feature"), + init(false), + Hidden, +}; + opt WorkerThreadsCount{ "j", cat(Misc), @@ -659,6 +667,7 @@ Opts.AsyncThreadsCount = WorkerThreadsCount; Opts.BuildRecoveryAST = RecoveryAST; Opts.PreserveRecoveryASTType = RecoveryASTType; + Opts.FoldingRanges = FoldingRanges; clangd::CodeCompleteOptions CCOpts; CCOpts.IncludeIneligibleResults = IncludeIneligibleResults; diff --git a/clang-tools-extra/clangd/unittests/SemanticSelectionTests.cpp b/clang-tools-extra/clangd/unittests/SemanticSelectionTests.cpp --- a/clang-tools-extra/clangd/unittests/SemanticSelectionTests.cpp +++ b/clang-tools-extra/clangd/unittests/SemanticSelectionTests.cpp @@ -17,15 +17,19 @@ #include "TestTU.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/SourceManager.h" +#include "llvm/ADT/ArrayRef.h" #include "llvm/Support/Error.h" #include "gmock/gmock.h" #include "gtest/gtest.h" #include + namespace clang { namespace clangd { namespace { + using ::testing::ElementsAre; using ::testing::ElementsAreArray; +using ::testing::UnorderedElementsAreArray; // front() is SR.range, back() is outermost range. std::vector gatherRanges(const SelectionRange &SR) { @@ -35,6 +39,22 @@ return Ranges; } +std::vector +gatherFoldingRanges(llvm::ArrayRef FoldingRanges) { + std::vector Ranges; + Range NextRange; + for (const auto &R : FoldingRanges) { + NextRange.start.line = R.startLine; + EXPECT_TRUE(R.startCharacter); + NextRange.start.character = *R.startCharacter; + NextRange.end.line = R.endLine; + EXPECT_TRUE(R.endCharacter); + NextRange.end.character = *R.endCharacter; + Ranges.push_back(NextRange); + } + return Ranges; +} + TEST(SemanticSelection, All) { const char *Tests[] = { R"cpp( // Single statement in a function body. @@ -118,16 +138,16 @@ )cpp", R"cpp( // Inside struct. struct A { static int a(); }; - [[struct B { + [[struct B { [[static int b() [[{ [[return [[[[1^1]] + 2]]]]; }]]]] }]]; )cpp", // Namespaces. - R"cpp( - [[namespace nsa { - [[namespace nsb { + R"cpp( + [[namespace nsa { + [[namespace nsb { static int ccc(); [[void func() [[{ // int x = nsa::nsb::ccc(); @@ -181,6 +201,41 @@ EXPECT_THAT(gatherRanges(Ranges->back()), ElementsAre(SourceAnnotations.range("empty"))); } + +TEST(FoldingRanges, All) { + const char *Tests[] = { + R"cpp( + int [[global_variable]]; + + [[void func() { + int v = 100; + }]] + )cpp", + R"cpp( + [[class Foo { + public: + [[Foo() { + int X = 1; + }]] + + private: + [[int getBar() { + return 42; + }]] + + [[void getFooBar() { }]] + }]]; + )cpp", + }; + for (const char *Test : Tests) { + auto T = Annotations(Test); + auto AST = TestTU::withCode(T.code()).build(); + EXPECT_THAT(gatherFoldingRanges(llvm::cantFail(getFoldingRanges(AST))), + UnorderedElementsAreArray(T.ranges())) + << Test; + } +} + } // namespace } // namespace clangd } // namespace clang