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 @@ -259,6 +259,8 @@ MarkupKind HoverContentFormat = MarkupKind::PlainText; /// Whether the client supports offsets for parameter info labels. bool SupportsOffsetsInSignatureHelp = false; + // Whether the client supports folding only complete lines. + bool LineFoldingsOnly = false; std::mutex BackgroundIndexProgressMutex; enum class BackgroundIndexProgress { // Client doesn't support reporting progress. No transitions possible. 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 @@ -512,6 +512,7 @@ Params.capabilities.HierarchicalDocumentSymbol; SupportFileStatus = Params.initializationOptions.FileStatus; HoverContentFormat = Params.capabilities.HoverContentFormat; + LineFoldingsOnly = Params.capabilities.LineFoldingOnly; SupportsOffsetsInSignatureHelp = Params.capabilities.OffsetsInSignatureHelp; if (Params.capabilities.WorkDoneProgress) BackgroundIndexProgressState = BackgroundIndexProgress::Empty; @@ -939,7 +940,8 @@ void ClangdLSPServer::onFoldingRange( const FoldingRangeParams &Params, Callback> Reply) { - Server->foldingRanges(Params.textDocument.uri.file(), std::move(Reply)); + Server->foldingRanges(Params.textDocument.uri.file(), LineFoldingsOnly, + std::move(Reply)); } static llvm::Optional asCommand(const CodeAction &Action) { 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 @@ -281,7 +281,8 @@ Callback> CB); /// Retrieve ranges that can be used to fold code within the specified file. - void foldingRanges(StringRef File, Callback> CB); + void foldingRanges(StringRef File, bool LineFoldingsOnly, + Callback> CB); /// Retrieve implementations for virtual method. void findImplementations(PathRef File, Position Pos, 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 @@ -832,15 +832,16 @@ Transient); } -void ClangdServer::foldingRanges(llvm::StringRef File, +void ClangdServer::foldingRanges(llvm::StringRef File, bool LineFoldingsOnly, Callback> CB) { auto Code = getDraft(File); if (!Code) return CB(llvm::make_error( "trying to compute folding ranges for non-added document", ErrorCode::InvalidParams)); - auto Action = [CB = std::move(CB), Code = std::move(*Code)]() mutable { - CB(clangd::getFoldingRanges(Code)); + auto Action = [LineFoldingsOnly, CB = std::move(CB), + Code = std::move(*Code)]() mutable { + CB(clangd::getFoldingRanges(Code, LineFoldingsOnly)); }; // We want to make sure folding ranges are always available for all the open // files, hence prefer runQuick to not wait for operations on other files. 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 @@ -434,6 +434,12 @@ /// textDocument.signatureHelp bool HasSignatureHelp = false; + /// Client signals that it only supports folding complete lines. + /// Client will ignore specified `startCharacter` and `endCharacter` + /// properties in a FoldingRange. + /// textDocument.foldingRange.lineFoldingOnly + bool LineFoldingOnly = false; + /// Client supports processing label offsets instead of a simple label string. /// textDocument.signatureHelp.signatureInformation.parameterInformation.labelOffsetSupport bool OffsetsInSignatureHelp = false; 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 @@ -391,6 +391,11 @@ } } } + if (auto *Folding = TextDocument->getObject("foldingRange")) { + if (auto LineFolding = Folding->getBoolean("lineFoldingOnly")) { + R.LineFoldingOnly = *LineFolding; + } + } if (auto *Rename = TextDocument->getObject("rename")) { if (auto RenameSupport = Rename->getBoolean("prepareSupport")) R.RenamePrepareSupport = *RenameSupport; 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 @@ -33,7 +33,7 @@ /// Returns a list of ranges whose contents might be collapsible in an editor. /// This version uses the pseudoparser which does not require the AST. llvm::Expected> -getFoldingRanges(const std::string &Code); +getFoldingRanges(const std::string &Code, bool LineFoldingsOnly); } // 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 @@ -179,7 +179,7 @@ // statement bodies). // Related issue: https://github.com/clangd/clangd/issues/310 llvm::Expected> -getFoldingRanges(const std::string &Code) { +getFoldingRanges(const std::string &Code, bool LineFoldingsOnly) { auto OrigStream = pseudo::lex(Code, clang::pseudo::genericLangOpts()); auto DirectiveStructure = pseudo::DirectiveTree::parse(OrigStream); @@ -192,15 +192,21 @@ pseudo::pairBrackets(ParseableStream); std::vector Result; - auto ToFoldingRange = [](Position Start, Position End, - llvm::StringLiteral Kind) { + auto AddFoldingRange = [&](Position Start, Position End, + llvm::StringLiteral Kind) { + if (Start.line >= End.line) + return; FoldingRange FR; FR.startLine = Start.line; - FR.startCharacter = Start.character; FR.endLine = End.line; - FR.endCharacter = End.character; + if (LineFoldingsOnly) + FR.startCharacter = FR.endCharacter = 0; + else { + FR.startCharacter = Start.character; + FR.endCharacter = End.character; + } FR.kind = Kind.str(); - return FR; + Result.push_back(FR); }; auto OriginalToken = [&](const pseudo::Token &T) { return OrigStream.tokens()[T.OriginalIndex]; @@ -221,9 +227,16 @@ // Process only token at the start of the range. Avoid ranges on a single // line. if (Tok.Line < Paired->Line) { - Position Start = offsetToPosition(Code, 1 + StartOffset(Tok)); + const Position Start = offsetToPosition(Code, 1 + StartOffset(Tok)); Position End = StartPosition(*Paired); - Result.push_back(ToFoldingRange(Start, End, FoldingRange::REGION_KIND)); + + if (LineFoldingsOnly && + Paired->OriginalIndex + 1 < OrigStream.tokens().size() && + End.line == + StartPosition(OrigStream.tokens()[Paired->OriginalIndex + 1]) + .line) + End.line--; + AddFoldingRange(Start, End, FoldingRange::REGION_KIND); } } } @@ -233,16 +246,14 @@ T++; continue; } - Position Start = StartPosition(*T); + const Position Start = StartPosition(*T); Position LastCommentEnd = EndPosition(*T); while (T != Tokens.end() && T->Kind == tok::comment && StartPosition(*T).line <= LastCommentEnd.line + 1) { LastCommentEnd = EndPosition(*T); T++; } - if (Start.line < LastCommentEnd.line) - Result.push_back( - ToFoldingRange(Start, LastCommentEnd, FoldingRange::COMMENT_KIND)); + AddFoldingRange(Start, LastCommentEnd, FoldingRange::COMMENT_KIND); } return Result; } 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 @@ -196,7 +196,7 @@ ElementsAre(SourceAnnotations.range("empty"))); } -TEST(FoldingRanges, All) { +TEST(FoldingRanges, ASTAll) { const char *Tests[] = { R"cpp( #define FOO int foo() {\ @@ -265,7 +265,7 @@ } } -TEST(FoldingRangesPseudoParser, All) { +TEST(FoldingRanges, PseudoParserWithoutLineFoldings) { const char *Tests[] = { R"cpp( #define FOO int foo() {\ @@ -359,13 +359,44 @@ }; for (const char *Test : Tests) { auto T = Annotations(Test); - EXPECT_THAT( - gatherFoldingRanges(llvm::cantFail(getFoldingRanges(T.code().str()))), - UnorderedElementsAreArray(T.ranges())) + EXPECT_THAT(gatherFoldingRanges(llvm::cantFail(getFoldingRanges( + T.code().str(), /*LineFoldingsOnly=*/false))), + UnorderedElementsAreArray(T.ranges())) << Test; } } +TEST(FoldingRanges, PseudoParserLineFoldingsOnly) { + const char *Tests[] = { + R"cpp( + void func(int a) {[[ + if(a == 1) {[[ + a++;]] + } else {[[ + a--; + ]]} + if(a == 1) {[[ + a++;]] + } // A token on the end line. + ]]} + )cpp", + }; + auto StripColumns = [](const std::vector &Ranges) { + std::vector Res; + for (Range R : Ranges) { + R.start.character = R.end.character = 0; + Res.push_back(R); + } + return Res; + }; + for (const char *Test : Tests) { + auto T = Annotations(Test); + EXPECT_THAT(gatherFoldingRanges(llvm::cantFail(getFoldingRanges( + T.code().str(), /*LineFoldingsOnly=*/true))), + UnorderedElementsAreArray(StripColumns(T.ranges()))) + << Test; + } +} } // namespace } // namespace clangd } // namespace clang