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 @@ -514,6 +514,7 @@ Params.capabilities.HierarchicalDocumentSymbol; SupportFileStatus = Params.initializationOptions.FileStatus; HoverContentFormat = Params.capabilities.HoverContentFormat; + Opts.LineFoldingOnly = Params.capabilities.LineFoldingOnly; SupportsOffsetsInSignatureHelp = Params.capabilities.OffsetsInSignatureHelp; if (Params.capabilities.WorkDoneProgress) BackgroundIndexProgressState = BackgroundIndexProgress::Empty; 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 @@ -164,6 +164,9 @@ /// Enable preview of FoldingRanges feature. bool FoldingRanges = false; + // Whether the client supports folding only complete lines. + bool LineFoldingOnly = false; + FeatureModuleSet *FeatureModules = nullptr; /// If true, use the dirty buffer contents when building Preambles. bool UseDirtyHeaders = false; @@ -429,6 +432,9 @@ bool UseDirtyHeaders = false; + // Whether the client supports folding only complete lines. + bool LineFoldingOnly = false; + bool PreambleParseForwardingFunctions = false; // GUARDED_BY(CachedCompletionFuzzyFindRequestMutex) 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 @@ -177,6 +177,7 @@ DynamicIdx(Opts.BuildDynamicSymbolIndex ? new FileIndex() : nullptr), ClangTidyProvider(Opts.ClangTidyProvider), UseDirtyHeaders(Opts.UseDirtyHeaders), + LineFoldingOnly(Opts.LineFoldingOnly), PreambleParseForwardingFunctions(Opts.PreambleParseForwardingFunctions), WorkspaceRoot(Opts.WorkspaceRoot), Transient(Opts.ImplicitCancellation ? TUScheduler::InvalidateOnUpdate @@ -855,8 +856,9 @@ 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 = [LineFoldingOnly = LineFoldingOnly, CB = std::move(CB), + Code = std::move(*Code)]() mutable { + CB(clangd::getFoldingRanges(Code, LineFoldingOnly)); }; // 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 @@ -437,6 +437,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,10 @@ } } } + 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 LineFoldingOnly); } // 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 LineFoldingOnly) { auto OrigStream = pseudo::lex(Code, clang::pseudo::genericLangOpts()); auto DirectiveStructure = pseudo::DirectiveTree::parse(OrigStream); @@ -192,15 +192,17 @@ 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; FR.kind = Kind.str(); - return FR; + Result.push_back(FR); }; auto OriginalToken = [&](const pseudo::Token &T) { return OrigStream.tokens()[T.OriginalIndex]; @@ -211,8 +213,11 @@ auto StartPosition = [&](const pseudo::Token &T) { return offsetToPosition(Code, StartOffset(T)); }; + auto EndOffset = [&](const pseudo::Token &T) { + return StartOffset(T) + OriginalToken(T).Length; + }; auto EndPosition = [&](const pseudo::Token &T) { - return offsetToPosition(Code, StartOffset(T) + OriginalToken(T).Length); + return offsetToPosition(Code, EndOffset(T)); }; auto Tokens = ParseableStream.tokens(); // Brackets. @@ -223,26 +228,43 @@ if (Tok.Line < Paired->Line) { Position Start = offsetToPosition(Code, 1 + StartOffset(Tok)); Position End = StartPosition(*Paired); - Result.push_back(ToFoldingRange(Start, End, FoldingRange::REGION_KIND)); + if (LineFoldingOnly) + End.line--; + AddFoldingRange(Start, End, FoldingRange::REGION_KIND); } } } + auto IsBlockComment = [&](const pseudo::Token &T) { + assert(T.Kind == tok::comment); + return OriginalToken(T).Length >= 2 && + Code.substr(StartOffset(T), 2) == "/*"; + }; // Multi-line comments. - for (const auto *T = Tokens.begin(); T != Tokens.end();) { + for (auto *T = Tokens.begin(); T != Tokens.end();) { if (T->Kind != tok::comment) { T++; continue; } - Position Start = StartPosition(*T); - Position LastCommentEnd = EndPosition(*T); + pseudo::Token *FirstComment = T; + // Show starting sentinals (// and /*) of the comment. + Position Start = offsetToPosition(Code, 2 + StartOffset(*FirstComment)); + pseudo::Token *LastComment = T; + Position End = EndPosition(*T); while (T != Tokens.end() && T->Kind == tok::comment && - StartPosition(*T).line <= LastCommentEnd.line + 1) { - LastCommentEnd = EndPosition(*T); + StartPosition(*T).line <= End.line + 1) { + End = EndPosition(*T); + LastComment = T; T++; } - if (Start.line < LastCommentEnd.line) - Result.push_back( - ToFoldingRange(Start, LastCommentEnd, FoldingRange::COMMENT_KIND)); + if (IsBlockComment(*FirstComment)) { + if (LineFoldingOnly) + // Show last line of a block comment. + End.line--; + if (IsBlockComment(*LastComment)) + // Show ending sentinal "*/" of the block comment. + End.character -= 2; + } + AddFoldingRange(Start, End, 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() {\ @@ -336,36 +336,123 @@ ]]}; )cpp", R"cpp( - [[/* Multi + /*[[ Multi * line * comment - */]] + ]]*/ )cpp", R"cpp( - [[// Comment + //[[ Comment // 1]] - [[// Comment + //[[ Comment // 2]] // No folding for single line comment. - [[/* comment 3 - */]] + /*[[ comment 3 + ]]*/ - [[/* comment 4 - */]] + /*[[ comment 4 + ]]*/ + + /*[[ foo */ + /* bar ]]*/ + + /*[[ foo */ + // baz + /* bar ]]*/ + + /*[[ foo */ + /* bar*/ + // baz]] + + //[[ foo + /* bar */]] )cpp", }; 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) {[[ + a++;]] + } + )cpp", + R"cpp( + // Always exclude last line for brackets. + void func(int a) {[[ + if(a == 1) {[[ + a++;]] + } else if (a == 2){[[ + a--;]] + } else { // No folding for 2 line bracketed ranges. + }]] + } + )cpp", + R"cpp( + /*[[ comment + * comment]] + */ + + /* No folding for this comment. + */ + + // No folding for this comment. + + //[[ 2 single line comment. + // 2 single line comment.]] + + //[[ >=2 line comments. + // >=2 line comments. + // >=2 line comments.]] + + //[[ foo\ + bar\ + baz]] + + /*[[ foo */ + /* bar */]] + /* baz */ + + /*[[ foo */ + /* bar]] + * This does not fold me */ + + //[[ foo + /* bar */]] + )cpp", + // FIXME: Support folding template arguments. + // R"cpp( + // template <[[typename foo, class bar]]> struct baz {}; + // )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( + StripColumns(gatherFoldingRanges(llvm::cantFail( + getFoldingRanges(T.code().str(), /*LineFoldingsOnly=*/true)))), + UnorderedElementsAreArray(StripColumns(T.ranges()))) + << Test; + } +} } // namespace } // namespace clangd } // namespace clang