From a4b83eb1dc91f88a28354c4db25edb288291b293 Mon Sep 17 00:00:00 2001 From: ykiko Date: Wed, 6 Aug 2025 11:41:55 +0800 Subject: [PATCH] Enable `FoldingRange` (#169) --- include/Feature/FoldingRange.h | 4 +- include/Protocol/Feature/FoldingRange.h | 66 +++++++++++- include/Protocol/Initialize.h | 2 +- include/Server/Server.h | 2 + src/Feature/FoldingRange.cpp | 128 ++++++++++++------------ src/Index/FeatureIndex.cpp | 2 +- src/Server/Feature.cpp | 41 ++++++++ src/Server/Lifecycle.cpp | 5 +- src/Server/Server.cpp | 1 + tests/unit/Feature/FoldingRange.cpp | 28 +++--- 10 files changed, 195 insertions(+), 84 deletions(-) diff --git a/include/Feature/FoldingRange.h b/include/Feature/FoldingRange.h index a12f16ab..115696d0 100644 --- a/include/Feature/FoldingRange.h +++ b/include/Feature/FoldingRange.h @@ -47,10 +47,10 @@ struct FoldingRange { using FoldingRanges = std::vector; /// Generate folding range for interested file only. -FoldingRanges foldingRanges(CompilationUnit& unit); +FoldingRanges folding_ranges(CompilationUnit& unit); /// Generate folding range for all files. -index::Shared indexFoldingRange(CompilationUnit& unit); +index::Shared index_folding_range(CompilationUnit& unit); } // namespace clice::feature diff --git a/include/Protocol/Feature/FoldingRange.h b/include/Protocol/Feature/FoldingRange.h index 54792979..61581a3d 100644 --- a/include/Protocol/Feature/FoldingRange.h +++ b/include/Protocol/Feature/FoldingRange.h @@ -4,8 +4,72 @@ namespace clice::proto { -struct FoldingRangeClientCapabilities {}; +struct FoldingRangeClientCapabilities { + /// The maximum number of folding ranges that the client prefers to receive + /// per document. The value serves as a hint, servers are free to follow the + /// limit. + optional rangeLimit; + + /// If set, the client signals that it only supports folding complete lines. + /// If set, client will ignore specified `startCharacter` and `endCharacter` + /// properties in a FoldingRange. + bool lineFoldingOnly = false; + + /// Specific options for the folding range kind. + struct { + /// The folding range kind values the client supports. When this + /// property exists the client also guarantees that it will + /// handle values outside its set gracefully and falls back + /// to a default value when unknown. + array valueSet; + } foldingRangeKind; + + /// Specific options for the folding range. + struct { + /// If set, the client signals that it supports setting collapsedText on + /// folding ranges to display custom labels instead of the default text. + bool collapsedText = false; + } foldingRange; +}; using FoldingRangeOptions = bool; +struct FoldingRangeParams { + /// The text document. + TextDocumentIdentifier textDocument; +}; + +using FoldingRangeKind = string; + +struct FoldingRange { + /// The zero-based start line of the range to fold. The folded area starts + /// after the line's last character. To be valid, the end must be zero or + /// larger and smaller than the number of lines in the document. + uinteger startLine; + + /// The zero-based character offset from where the folded range starts. If + /// not defined, defaults to the length of the start line. + uinteger startCharacter; + + /// The zero-based end line of the range to fold. The folded area ends with + /// the line's last character. To be valid, the end must be zero or larger + /// and smaller than the number of lines in the document. + uinteger endLine; + + /// The zero-based character offset before the folded range ends. If not + /// defined, defaults to the length of the end line. + uinteger endCharacter; + + /// Describes the kind of the folding range such as `comment` or `region`. + /// The kind is used to categorize folding ranges and used by commands like + /// 'Fold all comments'. See [FoldingRangeKind](#FoldingRangeKind) for an + /// enumeration of standardized kinds. + FoldingRangeKind kind; + + /// The text that the client should show when the specified range is + /// collapsed. If not defined or not supported by the client, a default + /// will be chosen by the client. + string collapsedText; +}; + } // namespace clice::proto diff --git a/include/Protocol/Initialize.h b/include/Protocol/Initialize.h index b8b2e281..0dd913ab 100644 --- a/include/Protocol/Initialize.h +++ b/include/Protocol/Initialize.h @@ -136,7 +136,7 @@ struct ServerCapabilities { /// FIXME: RenameOptions renameProvider; /// The server provides folding provider support. - /// FIXME: FoldingRangeOptions foldingRangeProvider; + FoldingRangeOptions foldingRangeProvider; /// The server provides execute command support. /// FIXME: ExecuteCommandOptions executeCommandProvider; diff --git a/include/Server/Server.h b/include/Server/Server.h index 614bd7aa..bbce3d2b 100644 --- a/include/Server/Server.h +++ b/include/Server/Server.h @@ -111,6 +111,8 @@ private: async::Task on_document_link(proto::DocumentLinkParams params); + async::Task on_folding_range(proto::FoldingRangeParams params); + async::Task on_semantic_token(proto::SemanticTokensParams params); private: diff --git a/src/Feature/FoldingRange.cpp b/src/Feature/FoldingRange.cpp index 4ed5e6c1..648bd9b6 100644 --- a/src/Feature/FoldingRange.cpp +++ b/src/Feature/FoldingRange.cpp @@ -28,7 +28,7 @@ public: /// Collect namespace. clang::SourceRange range(shrink.front().location(), decl->getRBraceLoc()); - addRange(range, FoldingRangeKind::Namespace, "{...}"); + add_range(range, FoldingRangeKind::Namespace, "{...}"); return true; } @@ -44,7 +44,7 @@ public: : decl->isClass() ? FoldingRangeKind::Class : decl->isUnion() ? FoldingRangeKind::Union : FoldingRangeKind::Enum; - addRange(decl->getBraceRange(), kind, "{...}"); + add_range(decl->getBraceRange(), kind, "{...}"); /// Collect public/protected/private blocks for a non-lambda struct/class. if(auto RD = llvm::dyn_cast(decl)) { @@ -56,7 +56,7 @@ public: for(auto* decl: RD->decls()) { if(auto* AS = llvm::dyn_cast(decl)) { if(last) { - addRange( + add_range( clang::SourceRange(last->getColonLoc(), AS->getAccessSpecifierLoc()), FoldingRangeKind::AccessSpecifier, ""); @@ -66,9 +66,9 @@ public: } if(last) { - addRange(clang::SourceRange(last->getColonLoc(), RD->getBraceRange().getEnd()), - FoldingRangeKind::AccessSpecifier, - ""); + add_range(clang::SourceRange(last->getColonLoc(), RD->getBraceRange().getEnd()), + FoldingRangeKind::AccessSpecifier, + ""); } } @@ -78,12 +78,12 @@ public: bool VisitFunctionDecl(const clang::FunctionDecl* decl) { /// If it's a forward declaration, try to collect the parameter list. if(!decl->doesThisDeclarationHaveABody()) { - collectParameterList(decl->getSourceRange()); + collect_parameter_list(decl->getSourceRange()); } else { - collectParameterList(decl->getBeginLoc(), decl->getBody()->getBeginLoc()); + collect_parameter_list(decl->getBeginLoc(), decl->getBody()->getBeginLoc()); /// Collect function body. - addRange(decl->getBody()->getSourceRange(), FoldingRangeKind::FunctionBody, "{...}"); + add_range(decl->getBody()->getSourceRange(), FoldingRangeKind::FunctionBody, "{...}"); } return true; @@ -92,15 +92,15 @@ public: bool VisitLambdaExpr(const clang::LambdaExpr* lambda) { auto introduceRange = lambda->getIntroducerRange(); /// Collect lambda capture list. - addRange(lambda->getIntroducerRange(), FoldingRangeKind::LambdaCapture, "[...]"); + add_range(lambda->getIntroducerRange(), FoldingRangeKind::LambdaCapture, "[...]"); /// Collect explicit parameter list. if(lambda->hasExplicitParameters()) { - collectParameterList(introduceRange.getEnd(), - lambda->getCompoundStmtBody()->getBeginLoc()); + collect_parameter_list(introduceRange.getEnd(), + lambda->getCompoundStmtBody()->getBeginLoc()); } - collectCompoundStmt(lambda->getBody()); + collect_compound_stmt(lambda->getBody()); return true; } @@ -116,9 +116,9 @@ public: if(kind == clang::tok::r_paren) depth += 1; else if(kind == clang::tok::l_paren && --depth == 0) { - addRange({tokens.back().location(), rightParen}, - FoldingRangeKind::FunctionCall, - "(...)"); + add_range({tokens.back().location(), rightParen}, + FoldingRangeKind::FunctionCall, + "(...)"); break; } tokens = tokens.drop_back(); @@ -129,42 +129,42 @@ public: bool VisitCXXConstructExpr(const clang::CXXConstructExpr* stmt) { if(auto range = stmt->getParenOrBraceRange(); range.isValid()) { - addRange({range.getBegin().getLocWithOffset(1), range.getEnd()}, - FoldingRangeKind::FunctionCall, - "(...)"); + add_range({range.getBegin().getLocWithOffset(1), range.getEnd()}, + FoldingRangeKind::FunctionCall, + "(...)"); } return true; } bool VisitInitListExpr(const clang::InitListExpr* expr) { - addRange({expr->getLBraceLoc(), expr->getRBraceLoc()}, - FoldingRangeKind::Initializer, - "{...}"); + add_range({expr->getLBraceLoc(), expr->getRBraceLoc()}, + FoldingRangeKind::Initializer, + "{...}"); return true; } - auto buildForFile(CompilationUnit& unit) { + auto build_for_file(CompilationUnit& unit) { TraverseTranslationUnitDecl(unit.tu()); collectDrectives(unit.directives()[unit.interested_file()]); std::ranges::sort(result, refl::less); return std::move(result); } - auto buildForIndex(CompilationUnit& unit) { + auto build_for_index(CompilationUnit& unit) { TraverseTranslationUnitDecl(unit.tu()); for(auto& [fid, directive]: unit.directives()) { collectDrectives(directive); } - for(auto& [fid, ranges]: indexResult) { + for(auto& [fid, ranges]: index_result) { std::ranges::sort(ranges, refl::less); } - return std::move(indexResult); + return std::move(index_result); } private: - void addRange(clang::SourceRange range, FoldingRangeKind kind, std::string text) { + void add_range(clang::SourceRange range, FoldingRangeKind kind, std::string text) { /// In normal AST, the range must be valid. But unfortunately, the range /// may be invalid in incomplete AST, so we need to check it. if(range.isInvalid()) { @@ -180,61 +180,61 @@ private: return; } - auto [fid, localRange] = unit.decompose_range(clang::SourceRange(begin, end)); - auto [beginOffset, endOffset] = localRange; + auto [fid, local_range] = unit.decompose_range(clang::SourceRange(begin, end)); + auto [begin_offset, end_offset] = local_range; - bool isSameLine = true; + bool is_same_line = true; auto content = unit.file_content(fid); - for(auto i = beginOffset; i < endOffset; ++i) { + for(auto i = begin_offset; i < end_offset; ++i) { if(content[i] == '\n') { - isSameLine = false; + is_same_line = false; break; } } /// TODO: Currently, we only support folding range in different lines. - if(isSameLine) { + if(is_same_line) { return; } - auto& ranges = interestedOnly ? result : indexResult[fid]; - ranges.emplace_back(localRange, kind, std::move(text)); + auto& ranges = interestedOnly ? result : index_result[fid]; + ranges.emplace_back(local_range, kind, std::move(text)); } - void collectParameterList(clang::SourceLocation left, clang::SourceLocation right) { - collectParameterList(clang::SourceRange(left, right)); + void collect_parameter_list(clang::SourceLocation left, clang::SourceLocation right) { + collect_parameter_list(clang::SourceRange(left, right)); } /// Collect function parameter list between '(' and ')'. - void collectParameterList(clang::SourceRange bounds) { + void collect_parameter_list(clang::SourceRange bounds) { auto tokens = unit.expanded_tokens(bounds); - auto leftParen = tokens.drop_until([](const auto& tk) { // + auto left_paren = tokens.drop_until([](const auto& tk) { // return tk.kind() == clang::tok::l_paren; }); - if(leftParen.empty()) + if(left_paren.empty()) return; - auto rightParenIter = - std::find_if(leftParen.rbegin(), leftParen.rend(), [](const auto& tk) { + auto right_paren_iter = + std::find_if(left_paren.rbegin(), left_paren.rend(), [](const auto& tk) { return tk.kind() == clang::tok::r_paren; }); - if(rightParenIter == leftParen.rend()) + if(right_paren_iter == left_paren.rend()) return; - addRange(clang::SourceRange(leftParen.front().location(), rightParenIter->location()), - FoldingRangeKind::FunctionParams, - "(...)"); + add_range(clang::SourceRange(left_paren.front().location(), right_paren_iter->location()), + FoldingRangeKind::FunctionParams, + "(...)"); } - void collectCompoundStmt(const clang::Stmt* stmt) { + void collect_compound_stmt(const clang::Stmt* stmt) { if(auto* CS = llvm::dyn_cast(stmt)) { - addRange({CS->getLBracLoc(), CS->getRBracLoc()}, - FoldingRangeKind::CompoundStmt, - "{...}"); + add_range({CS->getLBracLoc(), CS->getRBracLoc()}, + FoldingRangeKind::CompoundStmt, + "{...}"); for(auto child: stmt->children()) { - collectCompoundStmt(child); + collect_compound_stmt(child); } } } @@ -243,15 +243,15 @@ private: std::remove_reference_t().directives())>; void collectDrectives(const Directive& directive) { - collectConditionMacro(directive.conditions); - collectPragmaRegion(directive.pragmas); + collect_condition_directive(directive.conditions); + collect_pragma_region(directive.pragmas); /// TODO: /// Collect multiline include statement. } /// Collect all condition macro's block as folding range. - void collectConditionMacro(const std::vector& conds) { + void collect_condition_directive(const std::vector& conds) { // All condition directives have been stored in `conds` variable, ordered by presumed line // number increasement, so use a stack to handle the branch structure. @@ -271,9 +271,9 @@ private: case Condition::BranchKind::Else: { if(!stack.empty()) { auto last = stack.pop_back_val(); - addRange({last->condition_range.getEnd(), cond.loc}, - FoldingRangeKind::ConditionDirective, - ""); + add_range({last->condition_range.getEnd(), cond.loc}, + FoldingRangeKind::ConditionDirective, + ""); } stack.push_back(&cond); @@ -302,7 +302,7 @@ private: } /// Collect all condition macro's block as folding range. - void collectPragmaRegion(const std::vector& pragmas) { + void collect_pragma_region(const std::vector& pragmas) { llvm::SmallVector stack; for(auto& pragma: pragmas) { if(pragma.kind == Pragma::Region) { @@ -313,24 +313,24 @@ private: } auto last = stack.pop_back_val(); - addRange(clang::SourceRange(last->loc, pragma.loc), FoldingRangeKind::Region, ""); + add_range(clang::SourceRange(last->loc, pragma.loc), FoldingRangeKind::Region, ""); } } } private: FoldingRanges result; - index::Shared indexResult; + index::Shared index_result; }; } // namespace -FoldingRanges foldingRanges(CompilationUnit& unit) { - return FoldingRangeCollector(unit, true).buildForFile(unit); +FoldingRanges folding_ranges(CompilationUnit& unit) { + return FoldingRangeCollector(unit, true).build_for_file(unit); } -index::Shared indexFoldingRange(CompilationUnit& unit) { - return FoldingRangeCollector(unit, false).buildForIndex(unit); +index::Shared index_folding_range(CompilationUnit& unit) { + return FoldingRangeCollector(unit, false).build_for_index(unit); } // namespace feature } // namespace clice::feature diff --git a/src/Index/FeatureIndex.cpp b/src/Index/FeatureIndex.cpp index 1a033025..bb64d7af 100644 --- a/src/Index/FeatureIndex.cpp +++ b/src/Index/FeatureIndex.cpp @@ -65,7 +65,7 @@ Shared> FeatureIndex::build(CompilationUnit& unit) { indices[fid].tokens = std::move(result); } - for(auto&& [fid, result]: feature::indexFoldingRange(unit)) { + for(auto&& [fid, result]: feature::index_folding_range(unit)) { indices[fid].foldings = std::move(result); } diff --git a/src/Server/Feature.cpp b/src/Server/Feature.cpp index f27f6c8f..6026e462 100644 --- a/src/Server/Feature.cpp +++ b/src/Server/Feature.cpp @@ -1,3 +1,4 @@ +#include "Feature/FoldingRange.h" #include "Server/Server.h" #include "Server/Convert.h" #include "Compiler/Compilation.h" @@ -93,6 +94,46 @@ async::Task Server::on_document_link(proto::DocumentLinkParams para }); } +async::Task Server::on_folding_range(proto::FoldingRangeParams params) { + auto path = mapping.to_path(params.textDocument.uri); + + auto opening_file = &opening_files[path]; + auto guard = co_await opening_file->ast_built_lock.try_lock(); + + opening_file = &opening_files[path]; + auto content = opening_file->content; + auto ast = opening_file->ast; + if(!ast) { + co_return json::Value(nullptr); + } + + co_return co_await async::submit([&, kind = this->kind] { + auto foldings = feature::folding_ranges(*ast); + PositionConverter converter(content, kind); + converter.to_positions(foldings, + [](feature::FoldingRange& folding) { return folding.range; }); + + std::vector result; + + for(auto&& folding: foldings) { + auto [begin_offset, end_offset] = folding.range; + auto [begin_line, begin_char] = converter.lookup(begin_offset); + auto [end_line, end_char] = converter.lookup(end_offset); + + proto::FoldingRange range; + range.startLine = begin_line; + range.startCharacter = begin_char; + range.endLine = end_line; + range.endCharacter = end_char; + range.kind = "region"; + range.collapsedText = folding.text; + result.emplace_back(std::move(range)); + } + + return json::serialize(result); + }); +} + async::Task Server::on_semantic_token(proto::SemanticTokensParams params) { auto path = mapping.to_path(params.textDocument.uri); diff --git a/src/Server/Lifecycle.cpp b/src/Server/Lifecycle.cpp index b95764d0..49df9f9c 100644 --- a/src/Server/Lifecycle.cpp +++ b/src/Server/Lifecycle.cpp @@ -50,7 +50,10 @@ async::Task Server::on_initialize(proto::InitializeParams params) { /// DocumentLink capabilities.documentLinkProvider.resolveProvider = false; - /// Semantic tokens. + /// FoldingRange + capabilities.foldingRangeProvider = true; + + /// Semantic tokens capabilities.semanticTokensProvider.range = false; capabilities.semanticTokensProvider.full = true; for(auto name: SymbolKind::all()) { diff --git a/src/Server/Server.cpp b/src/Server/Server.cpp index c7f4c4ba..93e6ddd0 100644 --- a/src/Server/Server.cpp +++ b/src/Server/Server.cpp @@ -66,6 +66,7 @@ Server::Server() { register_callback<&Server::on_completion>("textDocument/completion"); register_callback<&Server::on_hover>("textDocument/hover"); register_callback<&Server::on_document_link>("textDocument/documentLink"); + register_callback<&Server::on_folding_range>("textDocument/foldingRange"); register_callback<&Server::on_semantic_token>("textDocument/semanticTokens/full"); } diff --git a/tests/unit/Feature/FoldingRange.cpp b/tests/unit/Feature/FoldingRange.cpp index ed4965ec..14a9a247 100644 --- a/tests/unit/Feature/FoldingRange.cpp +++ b/tests/unit/Feature/FoldingRange.cpp @@ -6,12 +6,12 @@ namespace clice::testing { namespace { struct FoldingRange : TestFixture { - std::vector result; + std::vector ranges; void run(llvm::StringRef source) { add_main("main.cpp", source); TestFixture::compile(); - result = feature::foldingRanges(*unit); + ranges = feature::folding_ranges(*unit); } using Self = FoldingRange; @@ -22,10 +22,10 @@ struct FoldingRange : TestFixture { llvm::StringRef end, feature::FoldingRangeKind kind, LocationChain chain = LocationChain()) { - auto& folding = self.result[index]; - auto begin_offset = self["main.cpp", begin]; + auto& folding = self.ranges[index]; + auto begin_offset = self.point(begin, "main.cpp"); EXPECT_EQ(begin_offset, folding.range.begin, chain); - auto end_offset = self["main.cpp", end]; + auto end_offset = self.point(end, "main.cpp"); EXPECT_EQ(end_offset, folding.range.end, chain); } }; @@ -54,7 +54,7 @@ $(7)NS_BEGIN NS_END$(8) )cpp"); - EXPECT_EQ(result.size(), 4); + EXPECT_EQ(ranges.size(), 4); EXPECT_RANGE(0, "1", "2", Namespace); EXPECT_RANGE(1, "3", "4", Namespace); EXPECT_RANGE(2, "5", "6", Namespace); @@ -79,7 +79,7 @@ enum e3 { D }; )cpp"); - EXPECT_EQ(result.size(), 2); + EXPECT_EQ(ranges.size(), 2); EXPECT_RANGE(0, "1", "2", Enum); EXPECT_RANGE(1, "3", "4", Enum); } @@ -113,7 +113,7 @@ void foo() $(9){ }$(10) )cpp"); - EXPECT_EQ(result.size(), 6); + EXPECT_EQ(ranges.size(), 6); EXPECT_RANGE(0, "1", "2", Struct); EXPECT_RANGE(1, "3", "4", Union); EXPECT_RANGE(2, "5", "6", Struct); @@ -146,7 +146,7 @@ struct s3 $(3){ }$(4); )cpp"); - EXPECT_EQ(result.size(), 4); + EXPECT_EQ(ranges.size(), 4); EXPECT_RANGE(0, "1", "2", Struct); EXPECT_RANGE(1, "3", "4", Struct); EXPECT_RANGE(2, "5", "6", FunctionBody); @@ -189,7 +189,7 @@ auto l4 = [] $(13)( )$(14) {}; )cpp"); - EXPECT_EQ(result.size(), 7); + EXPECT_EQ(ranges.size(), 7); EXPECT_RANGE(0, "1", "2", LambdaCapture); EXPECT_RANGE(1, "3", "4", FunctionBody); EXPECT_RANGE(2, "5", "6", LambdaCapture); @@ -234,7 +234,7 @@ void k() $(13){ }$(14) )cpp"); - EXPECT_EQ(result.size(), 7); + EXPECT_EQ(ranges.size(), 7); EXPECT_RANGE(0, "1", "2", FunctionParams); EXPECT_RANGE(1, "3", "4", FunctionBody); EXPECT_RANGE(2, "5", "6", FunctionParams); @@ -263,7 +263,7 @@ int main() $(1){ }$(6) )cpp"); - EXPECT_EQ(result.size(), 3); + EXPECT_EQ(ranges.size(), 3); EXPECT_RANGE(0, "1", "6", FunctionBody); EXPECT_RANGE(1, "2", "3", FunctionCall); EXPECT_RANGE(2, "4", "5", FunctionCall); @@ -306,7 +306,7 @@ L l2 = $(3){ )cpp"); - EXPECT_EQ(result.size(), 2); + EXPECT_EQ(ranges.size(), 2); EXPECT_RANGE(0, "1", "2", Initializer); EXPECT_RANGE(1, "3", "4", Initializer); } @@ -393,7 +393,7 @@ $(1)#pragma region level1 #pragma region // mismatch region, skipped )cpp"); - EXPECT_EQ(result.size(), 3); + EXPECT_EQ(ranges.size(), 3); EXPECT_RANGE(0, "1", "6", Region); EXPECT_RANGE(1, "2", "5", Region); EXPECT_RANGE(2, "3", "4", Region);