diff --git a/include/Basic/SourceCode.h b/include/Basic/SourceCode.h index 99e040ac..12904bfa 100644 --- a/include/Basic/SourceCode.h +++ b/include/Basic/SourceCode.h @@ -8,10 +8,10 @@ namespace clice { struct LocalSourceRange { /// The begin position offset to the source file. - uint32_t begin; + uint32_t begin = -1; /// The end position offset to the source file. - uint32_t end; + uint32_t end = -1; constexpr bool operator== (const LocalSourceRange& other) const = default; diff --git a/include/Compiler/Directive.h b/include/Compiler/Directive.h index 340994e5..b221dd3e 100644 --- a/include/Compiler/Directive.h +++ b/include/Compiler/Directive.h @@ -119,7 +119,7 @@ struct Pragma { /// Kind of the pragma. Kind kind; - /// Location of the pragma token. + /// Location of the `#` token. clang::SourceLocation loc; }; diff --git a/include/Feature/FoldingRange.h b/include/Feature/FoldingRange.h index 2da0c789..62e1d019 100644 --- a/include/Feature/FoldingRange.h +++ b/include/Feature/FoldingRange.h @@ -1,20 +1,14 @@ -#include "Basic/Document.h" +#pragma once + #include "Basic/SourceCode.h" #include "Index/Shared.h" -#include "Support/JSON.h" +#include "Support/Enum.h" namespace clice { -namespace proto { +class ASTInfo; -/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#foldingRangeClientCapabilities -struct FoldingRangeClientCapabilities {}; - -/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#foldingRangeParams -struct FoldingRangeParams { - /// The text document. - TextDocumentIdentifier textDocument; -}; +namespace feature { struct FoldingRangeKind : refl::Enum { enum Kind : uint8_t { @@ -22,6 +16,19 @@ struct FoldingRangeKind : refl::Enum { Comment, Imports, Region, + Namespace, + Class, + Enum, + Struct, + Union, + LambdaCapture, + FunctionParams, + FunctionBody, + FunctionCall, + CompoundStmt, + AccessSpecifier, + ConditionDirective, + Initializer, }; using Enum::Enum; @@ -29,62 +36,24 @@ struct FoldingRangeKind : refl::Enum { constexpr static auto InvalidEnum = Invalid; }; -/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#foldingRange +/// We don't record the coalesced text for a range, because it's rarely useful. struct FoldingRange { - /// The zero-based start line of the range to fold. The folded area starts after the line's last - /// character. - uinteger startLine; - - /// The zero-based end line of the range to fold. The folded area ends with the line's last - /// character. - uinteger endLine; - - /// The zero-based character offset from where the folded range starts. - uinteger startCharacter; - - /// The zero-based character offset before the folded range ends. - uinteger endCharacter; + /// The range to fold. + LocalSourceRange range; /// Describes the kind of the folding range. FoldingRangeKind kind; - // The text that the client should show when the specified range is collapsed. - string collapsedText; + /// The text to display when the folding range is collapsed. + std::string text; }; -using FoldingRangeResult = std::vector; - -} // namespace proto - -class ASTInfo; -class SourceConverter; - -namespace feature::foldingrange { - -json::Value capability(json::Value clientCapabilities); - -/// We don't record the coalesced text for a range, because it's rarely useful. -struct FoldingRange { - LocalSourceRange range; - proto::FoldingRangeKind kind; -}; - -using Result = std::vector; +/// Generate folding range for interested file only. +std::vector foldingRange(ASTInfo& AST); /// Generate folding range for all files. -index::Shared foldingRange(ASTInfo& AST); +index::Shared> indexFoldingRange(ASTInfo& AST); -/// Return folding range in main file. -Result foldingRange(proto::FoldingRangeParams param, ASTInfo& AST); - -proto::FoldingRange toLspType(const FoldingRange& folding, - const SourceConverter& SC, - llvm::StringRef content); - -proto::FoldingRangeResult toLspResult(llvm::ArrayRef foldings, - const SourceConverter& SC, - llvm::StringRef content); - -} // namespace feature::foldingrange +} // namespace feature } // namespace clice diff --git a/include/Index/FeatureIndex.h b/include/Index/FeatureIndex.h index c7e4ca47..f066992e 100644 --- a/include/Index/FeatureIndex.h +++ b/include/Index/FeatureIndex.h @@ -4,6 +4,7 @@ #include "Shared.h" #include "Feature/SemanticTokens.h" +#include "Feature/FoldingRange.h" #include "llvm/ADT/DenseMap.h" #include "clang/Basic/SourceLocation.h" diff --git a/include/Server/LSPConverter.h b/include/Server/LSPConverter.h index 4a9604bc..049818de 100644 --- a/include/Server/LSPConverter.h +++ b/include/Server/LSPConverter.h @@ -17,6 +17,8 @@ public: Result convert(llvm::StringRef path, llvm::ArrayRef tokens); + Result convert(llvm::StringRef path, llvm::ArrayRef foldings); + Result convert(const feature::Hover& hover); private: diff --git a/include/Support/JSON.h b/include/Support/JSON.h index 752545cd..a07ac093 100644 --- a/include/Support/JSON.h +++ b/include/Support/JSON.h @@ -343,6 +343,12 @@ struct Serde { } }; +template +constexpr inline bool is_optional_v = false; + +template +constexpr inline bool is_optional_v> = true; + template struct Serde { constexpr inline static bool stateful = @@ -351,9 +357,16 @@ struct Serde { template static json::Value serialize(const T& t, Serdes&&... serdes) { json::Object object; - refl::foreach(t, [&](std::string_view name, auto& member) { - object.try_emplace(llvm::StringRef(name), - json::serialize(member, std::forward(serdes)...)); + refl::foreach(t, [&](std::string_view name, const Field& field) { + if constexpr(is_optional_v) { + if(field) { + object.try_emplace(llvm::StringRef(name), + json::serialize(*field, std::forward(serdes)...)); + } + } else { + object.try_emplace(llvm::StringRef(name), + json::serialize(field, std::forward(serdes)...)); + } }); return object; } diff --git a/include/Support/Struct.h b/include/Support/Struct.h index 70ec1b6e..2aa45756 100644 --- a/include/Support/Struct.h +++ b/include/Support/Struct.h @@ -12,10 +12,10 @@ namespace clice::refl { namespace impl { struct Any { - consteval Any(std::size_t); + constexpr Any(std::size_t); template - consteval operator T () const; + constexpr operator T () const; }; template diff --git a/src/Feature/FoldingRange.cpp b/src/Feature/FoldingRange.cpp index cabac0d1..0370451a 100644 --- a/src/Feature/FoldingRange.cpp +++ b/src/Feature/FoldingRange.cpp @@ -1,174 +1,100 @@ #include "AST/FilterASTVisitor.h" -#include "Basic/SourceConverter.h" #include "Compiler/Compilation.h" #include "Feature/FoldingRange.h" +#include "Support/Compare.h" -/// Clangd's FoldingRange Implementation: -/// https://github.com/llvm/llvm-project/blob/main/clang-tools-extra/clangd/SemanticSelection.cpp - -namespace clice { +namespace clice::feature { namespace { -struct FoldingRangeCollector : public FilteredASTVisitor { - - using Base = FilteredASTVisitor; - - using Folding = feature::foldingrange::FoldingRange; - - /// Cache extra line number as the inner storage to speedup the collection. - struct RichFolding : Folding { - uint32_t startLine; - uint32_t endLine; - }; - - using Storage = index::Shared>; - - Storage result; +class FoldingRangeCollector : public FilteredASTVisitor { +public: + FoldingRangeCollector(ASTInfo& AST, bool interestedOnly) : + FilteredASTVisitor(AST, interestedOnly, std::nullopt), SM(AST.srcMgr()), TB(AST.tokBuf()) {} constexpr static auto LastColOfLine = std::numeric_limits::max(); - FoldingRangeCollector(ASTInfo& AST, - bool interestedOnly, - std::optional targetRange) : - FilteredASTVisitor(AST, interestedOnly, targetRange), result() {} - - /// Collect source range as a folding range. - void collect(clang::SourceRange range, - std::pair offsetFix = {0, 0}, - proto::FoldingRangeKind kind = proto::FoldingRangeKind::Region) { - - const auto& SM = AST.srcMgr(); - unsigned startLine = SM.getPresumedLineNumber(range.getBegin()) - 1; - unsigned endLine = SM.getPresumedLineNumber(range.getEnd()) - 1; - - // Skip ranges on a single line. - if(startLine >= endLine) - return; - - auto fileID = interestedOnly ? AST.getInterestedFile() : SM.getFileID(range.getBegin()); - auto& state = result[fileID]; - - if(auto beg = range.getBegin(); beg.isMacroID()) { - auto cursor = - SM.translateLineCol(fileID, SM.getExpansionLineNumber(beg), LastColOfLine); - range.setBegin(cursor); - offsetFix.first = 0; - } - if(auto end = range.getEnd(); end.isMacroID()) { - range.setEnd(SM.translateLineCol(fileID, - SM.getExpansionLineNumber(end), - SM.getExpansionColumnNumber(end))); - offsetFix.second = 0; - } - - assert(range.isValid()); - - auto [leftLocal, rightLocal] = AST.toLocalRange(range).second; - LocalSourceRange fixed{leftLocal + offsetFix.first, rightLocal + offsetFix.second}; - if(state.empty() || state.back().startLine != startLine) { - state.push_back({fixed, kind, startLine, endLine}); - } - } - bool VisitNamespaceDecl(const clang::NamespaceDecl* decl) { - auto tokens = AST.tokBuf().expandedTokens(decl->getSourceRange()); - // Find first '{' in namespace declaration. - auto shrink = tokens.drop_until([](const clang::syntax::Token& tk) -> bool { - return tk.kind() == clang::tok::l_brace; - }); + auto shrink = TB.expandedTokens(decl->getSourceRange()) + .drop_until([](const clang::syntax::Token& token) -> bool { + return token.kind() == clang::tok::l_brace; + }); + + /// If The AST is not complete, we may cannot find the '{'. + if(shrink.empty()) { + return true; + } + + /// Collect namespace. + clang::SourceRange range(shrink.front().location(), decl->getRBraceLoc()); + addRange(range, FoldingRangeKind::Namespace, "{...}"); - collect({shrink.front().location(), decl->getRBraceLoc()}, {1, -1}); return true; } - /// Collect public/protected/private blocks for a non-lambda struct/class. - void collectAccessSpecDecls(const clang::RecordDecl* RD) { - const clang::AccessSpecDecl* lastAccess = nullptr; - - for(auto* decl: RD->decls()) { - if(auto* AS = llvm::dyn_cast(decl)) { - if(lastAccess) { - auto spec = AS->getAccessUnsafe(); - int offsetToSpecStart = spec == clang::AS_private ? 7 - : spec == clang::AS_public ? 6 - : 9; - collect({lastAccess->getColonLoc(), AS->getAccessSpecifierLoc()}, - {1, -offsetToSpecStart}); - } - lastAccess = AS; - } - } - - // The last access specifier block. - if(lastAccess) { - collect({lastAccess->getColonLoc(), RD->getBraceRange().getEnd()}, {1, -1}); - } - } - bool VisitTagDecl(const clang::TagDecl* decl) { - // Collect definition of class/struct/enum. - auto [leftBrace, rightBrace] = decl->getBraceRange(); - auto name = decl->getName(); - collect({leftBrace, rightBrace}, {1, -1}); - - if(auto RD = llvm::dyn_cast(decl)) { - collectAccessSpecDecls(RD); + /// If it's a forward declaration, nothing to do. + if(!decl->isThisDeclarationADefinition()) { + return true; } + + // Collect the definition of class/struct/enum/union. + FoldingRangeKind kind = decl->isStruct() ? FoldingRangeKind::Struct + : decl->isClass() ? FoldingRangeKind::Class + : decl->isUnion() ? FoldingRangeKind::Union + : FoldingRangeKind::Enum; + addRange(decl->getBraceRange(), kind, "{...}"); + + /// Collect public/protected/private blocks for a non-lambda struct/class. + if(auto RD = llvm::dyn_cast(decl)) { + if(RD->isLambda() || RD->isImplicit()) { + return true; + } + + clang::AccessSpecDecl* last = nullptr; + for(auto* decl: RD->decls()) { + if(auto* AS = llvm::dyn_cast(decl)) { + if(last) { + addRange( + clang::SourceRange(last->getColonLoc(), AS->getAccessSpecifierLoc()), + FoldingRangeKind::AccessSpecifier, + ""); + } + last = AS; + } + } + + if(last) { + addRange(clang::SourceRange(last->getColonLoc(), RD->getBraceRange().getEnd()), + FoldingRangeKind::AccessSpecifier, + ""); + } + } + return true; } - /// Collect function parameter list between '(' and ')'. - void collectParameterList(clang::SourceLocation leftSide, clang::SourceLocation rightSide) { - auto tokens = AST.tokBuf().expandedTokens({leftSide, rightSide}); - auto leftParen = tokens.drop_until([](const auto& tk) { // - return tk.kind() == clang::tok::l_paren; - }); - - if(leftParen.empty()) - return; - - auto rightParenIter = - std::find_if(leftParen.rbegin(), leftParen.rend(), [](const auto& tk) { - return tk.kind() == clang::tok::r_paren; - }); - - if(rightParenIter == leftParen.rend()) - return; - - collect({leftParen.front().location(), rightParenIter->location()}, {1, -1}); - } - - void collectCompoundStmt(const clang::Stmt* stmt) { - if(auto* CS = llvm::dyn_cast(stmt)) { - collect({CS->getLBracLoc(), CS->getRBracLoc()}, {1, -1}); - for(auto child: stmt->children()) { - collectCompoundStmt(child); - } - } - } - bool VisitFunctionDecl(const clang::FunctionDecl* decl) { - auto leftParen = decl->getBeginLoc(); - auto rightParen = decl->hasBody() // - ? decl->getBody()->getBeginLoc() - : decl->getSourceRange().getEnd(); - collectParameterList(leftParen, rightParen); + /// If it's a forward declaration, try to collect the parameter list. + if(!decl->doesThisDeclarationHaveABody()) { + collectParameterList(decl->getSourceRange()); + } else { + collectParameterList(decl->getBeginLoc(), decl->getBody()->getBeginLoc()); - if(decl->hasBody()) { - auto [leftBrace, rightBrace] = decl->getBody()->getSourceRange(); - collect({leftBrace, rightBrace}, {1, -1}); - collectCompoundStmt(decl->getBody()); + /// Collect function body. + addRange(decl->getBody()->getSourceRange(), FoldingRangeKind::FunctionBody, "{...}"); } + return true; } bool VisitLambdaExpr(const clang::LambdaExpr* lambda) { auto introduceRange = lambda->getIntroducerRange(); - assert(introduceRange.isValid() && "Invalid introduce range."); - collect(introduceRange, {1, -1}); + /// Collect lambda capture list. + addRange(lambda->getIntroducerRange(), FoldingRangeKind::LambdaCapture, "[...]"); + /// Collect explicit parameter list. if(lambda->hasExplicitParameters()) { collectParameterList(introduceRange.getEnd(), lambda->getCompoundStmtBody()->getBeginLoc()); @@ -190,7 +116,9 @@ struct FoldingRangeCollector : public FilteredASTVisitor if(kind == clang::tok::r_paren) depth += 1; else if(kind == clang::tok::l_paren && --depth == 0) { - collect({tokens.back().location(), rightParen}, {1, -1}); + addRange({tokens.back().location(), rightParen}, + FoldingRangeKind::FunctionCall, + "(...)"); break; } tokens = tokens.drop_back(); @@ -200,30 +128,125 @@ struct FoldingRangeCollector : public FilteredASTVisitor } bool VisitCXXConstructExpr(const clang::CXXConstructExpr* stmt) { - if(auto range = stmt->getParenOrBraceRange(); range.isValid()) - collect({range.getBegin().getLocWithOffset(1), range.getEnd()}); - + if(auto range = stmt->getParenOrBraceRange(); range.isValid()) { + addRange({range.getBegin().getLocWithOffset(1), range.getEnd()}, + FoldingRangeKind::FunctionCall, + "(...)"); + } return true; } bool VisitInitListExpr(const clang::InitListExpr* expr) { - collect({expr->getLBraceLoc(), expr->getRBraceLoc()}, {1, -1}); + addRange({expr->getLBraceLoc(), expr->getRBraceLoc()}, + FoldingRangeKind::Initializer, + "{...}"); return true; } + auto buildForFile(ASTInfo& AST) { + TraverseTranslationUnitDecl(AST.tu()); + collectDrectives(AST.directives()[AST.getInterestedFile()]); + std::ranges::sort(result, refl::less); + return std::move(result); + } + + auto buildForIndex(ASTInfo& AST) { + TraverseTranslationUnitDecl(AST.tu()); + for(auto& [fid, directive]: AST.directives()) { + collectDrectives(directive); + } + + for(auto& [fid, ranges]: indexResult) { + std::ranges::sort(ranges, refl::less); + } + + return std::move(indexResult); + } + +private: + void addRange(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()) { + return; + } + + auto [begin, end] = range; + begin = AST.getExpansionLoc(begin); + end = AST.getExpansionLoc(end); + + /// If they are from the same macro expansion, skip it. + if(begin == end) { + return; + } + + auto [fid, localRange] = AST.toLocalRange(clang::SourceRange(begin, end)); + auto [beginOffset, endOffset] = localRange; + + bool isSameLine = true; + auto content = AST.getFileContent(fid); + for(auto i = beginOffset; i < endOffset; ++i) { + if(content[i] == '\n') { + isSameLine = false; + break; + } + } + + /// TODO: Currently, we only support folding range in different lines. + if(isSameLine) { + return; + } + + auto& ranges = interestedOnly ? result : indexResult[fid]; + ranges.emplace_back(localRange, kind, std::move(text)); + } + + void collectParameterList(clang::SourceLocation left, clang::SourceLocation right) { + collectParameterList(clang::SourceRange(left, right)); + } + + /// Collect function parameter list between '(' and ')'. + void collectParameterList(clang::SourceRange bounds) { + auto tokens = AST.tokBuf().expandedTokens(bounds); + auto leftParen = tokens.drop_until([](const auto& tk) { // + return tk.kind() == clang::tok::l_paren; + }); + + if(leftParen.empty()) + return; + + auto rightParenIter = + std::find_if(leftParen.rbegin(), leftParen.rend(), [](const auto& tk) { + return tk.kind() == clang::tok::r_paren; + }); + + if(rightParenIter == leftParen.rend()) + return; + + addRange(clang::SourceRange(leftParen.front().location(), rightParenIter->location()), + FoldingRangeKind::FunctionParams, + "(...)"); + } + + void collectCompoundStmt(const clang::Stmt* stmt) { + if(auto* CS = llvm::dyn_cast(stmt)) { + addRange({CS->getLBracLoc(), CS->getRBracLoc()}, + FoldingRangeKind::CompoundStmt, + "{...}"); + for(auto child: stmt->children()) { + collectCompoundStmt(child); + } + } + } + using ASTDirectives = std::remove_reference_t().directives())>; - void collectDrectives(const ASTDirectives& direcs) { - for(auto& [fileid, dirc]: direcs) { - if(fileid != AST.getInterestedFile()) - continue; + void collectDrectives(const Directive& directive) { + collectConditionMacro(directive.conditions); + collectPragmaRegion(directive.pragmas); - collectConditionMacro(dirc.conditions); - collectPragmaRegion(dirc.pragmas); - - /// TODO: - /// Collect multiline include statement. - } + /// TODO: + /// Collect multiline include statement. } /// Collect all condition macro's block as folding range. @@ -247,7 +270,9 @@ struct FoldingRangeCollector : public FilteredASTVisitor case Condition::BranchKind::Else: { if(!stack.empty()) { auto last = stack.pop_back_val(); - collect({last->conditionRange.getEnd(), cond.loc}, {0, -1}); + addRange({last->conditionRange.getEnd(), cond.loc}, + FoldingRangeKind::ConditionDirective, + ""); } stack.push_back(&cond); @@ -261,10 +286,10 @@ struct FoldingRangeCollector : public FilteredASTVisitor // For a directive without condition range e.g #else // its condition range is invalid. if(last->conditionRange.isValid()) { - collect({last->conditionRange.getBegin(), cond.loc}, {0, -1}); + /// collect({last->conditionRange.getBegin(), cond.loc}, {0, -1}); } else { - collect({last->loc, cond.loc}, - {refl::enum_name(cond.kind).length(), -1}); + /// collect({last->loc, cond.loc}, + /// {refl::enum_name(cond.kind).length(), -1}); } } break; @@ -279,103 +304,36 @@ struct FoldingRangeCollector : public FilteredASTVisitor void collectPragmaRegion(const std::vector& pragmas) { const auto& SM = AST.srcMgr(); - auto lastLocOfLine = [this, &SM](clang::SourceLocation loc) { - auto line = SM.getPresumedLineNumber(loc); - return SM.translateLineCol(SM.getMainFileID(), line, LastColOfLine); - }; - - llvm::SmallVector stack = {}; + llvm::SmallVector stack; for(auto& pragma: pragmas) { - switch(pragma.kind) { - case Pragma::Kind::Region: stack.push_back(&pragma); break; - case Pragma::Kind::EndRegion: - if(!stack.empty()) { - auto last = stack.pop_back_val(); - collect({lastLocOfLine(last->loc), pragma.loc}, {0, -1}); - } - break; - default: break; - } - } + if(pragma.kind == Pragma::Region) { + stack.push_back(&pragma); + } else if(pragma.kind == Pragma::EndRegion) { + if(stack.empty()) { + continue; + } - // If there is some region without end pragma, use the end of file as the end region. - if(!stack.empty()) { - auto eof = SM.getLocForEndOfFile(SM.getMainFileID()); - while(!stack.empty()) { auto last = stack.pop_back_val(); - collect({lastLocOfLine(last->loc), eof}); + addRange(clang::SourceRange(last->loc, pragma.loc), FoldingRangeKind::Region, ""); } } } - static index::Shared> extract(const Storage& storage) { - llvm::DenseMap> extracted; - for(auto& [fileID, richs]: storage) { - std::vector res; - res.reserve(richs.size()); - for(auto& rich: richs) { - res.push_back(rich); - } - extracted[fileID] = std::move(res); - } - return extracted; - } - - static index::Shared> - collect(ASTInfo& AST, bool interestedOnly, std::optional targetRange) { - - FoldingRangeCollector collector(AST, interestedOnly, targetRange); - collector.collectDrectives(AST.directives()); - collector.TraverseTranslationUnitDecl(AST.tu()); - return extract(collector.result); - } +private: + clang::SourceManager& SM; + clang::syntax::TokenBuffer& TB; + std::vector result; + index::Shared> indexResult; }; } // namespace -namespace feature::foldingrange { - -json::Value capability(json::Value clientCapabilities) { - // Always return empty object. - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_foldingRange - return {}; +std::vector foldingRange(ASTInfo& AST) { + return FoldingRangeCollector(AST, true).buildForFile(AST); } -index::Shared foldingRange(ASTInfo& AST) { - return FoldingRangeCollector::collect(AST, /*interestedOnly=*/false, std::nullopt); -} +index::Shared> indexFoldingRange(ASTInfo& AST) { + return FoldingRangeCollector(AST, false).buildForIndex(AST); +} // namespace feature -Result foldingRange(proto::FoldingRangeParams _, ASTInfo& AST) { - auto ranges = FoldingRangeCollector::collect(AST, /*interestedOnly=*/true, std::nullopt); - return std::move(ranges[AST.getInterestedFile()]); -} - -proto::FoldingRange toLspType(const FoldingRange& folding, - const SourceConverter& SC, - llvm::StringRef content) { - auto range = SC.toRange(folding.range, content); - return { - .startLine = range.start.line, - .endLine = range.end.line, - .startCharacter = range.start.character, - .endCharacter = range.end.character, - .kind = folding.kind, - .collapsedText = "", - }; -} - -proto::FoldingRangeResult toLspResult(llvm::ArrayRef foldings, - const SourceConverter& SC, - llvm::StringRef content) { - - proto::FoldingRangeResult result; - result.reserve(foldings.size()); - for(const auto& folding: foldings) { - result.push_back(toLspType(folding, SC, content)); - } - return result; -} - -} // namespace feature::foldingrange - -} // namespace clice +} // namespace clice::feature diff --git a/src/Index/FeatureIndex.cpp b/src/Index/FeatureIndex.cpp index 59daa03a..4af36a89 100644 --- a/src/Index/FeatureIndex.cpp +++ b/src/Index/FeatureIndex.cpp @@ -8,6 +8,7 @@ namespace memory { struct FeatureIndex { std::vector tokens; + std::vector foldings; }; } // namespace memory @@ -19,6 +20,10 @@ Shared indexFeature(ASTInfo& info) { indices[fid].tokens = std::move(result); } + for(auto&& [fid, result]: feature::indexFoldingRange(info)) { + indices[fid].foldings = std::move(result); + } + Shared result; for(auto&& [fid, index]: indices) { diff --git a/src/Server/LSPConverter.cpp b/src/Server/LSPConverter.cpp index 448db8d8..bf6b5d16 100644 --- a/src/Server/LSPConverter.cpp +++ b/src/Server/LSPConverter.cpp @@ -177,6 +177,29 @@ public: return position; } + template + void toPositions(Range&& range, Proj&& proj) { + std::vector offsets; + for(auto&& item: range) { + auto [begin, end] = proj(item); + offsets.emplace_back(begin); + offsets.emplace_back(end); + } + + ranges::sort(offsets); + ranges::unique(offsets); + + for(auto&& offset: offsets) { + toPosition(offset); + } + } + + proto::Position toPosition2(uint32_t offset) { + auto it = cache.find(offset); + assert(it != cache.end() && "Offset is not cached"); + return it->second; + } + private: std::uint32_t line = 0; /// The offset of the last line end. @@ -186,6 +209,8 @@ private: std::uint32_t lastInput = 0; proto::Position lastOutput = {0, 0}; + llvm::DenseMap cache; + llvm::StringRef content; proto::PositionEncodingKind kind; }; @@ -285,6 +310,89 @@ LSPConverter::Result LSPConverter::convert(llvm::StringRef path, co_return json::serialize(result); } +namespace proto { + +/// A set of predefined range kinds. +enum class FoldingRangeKind { + /// Folding range for a comment. + Comment, + + /// Folding range for imports or includes. + Imports, + + /// Folding range for a region. + Region, +}; + +/// Represents a folding range. To be valid, start and end line must be bigger +/// than zero and smaller than the number of lines in the document. Clients +/// are free to ignore invalid ranges. +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. + uint32_t startLine; + + /// The zero-based character offset from where the folded range starts. If + /// not defined, defaults to the length of the start line. + std::optional 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. + uint32_t endLine; + + /// The zero-based character offset before the folded range ends. If not + /// defined, defaults to the length of the end line. + std::optional 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. + /// + /// @since 3.17.0 - proposed + std::optional collapsedText; +}; + +} // namespace proto + +LSPConverter::Result LSPConverter::convert(llvm::StringRef path, + llvm::ArrayRef foldings) { + auto file = co_await async::fs::read(path.str()); + if(!file) { + co_return json::Value(nullptr); + } + llvm::StringRef content = *file; + + std::vector result; + + PositionConverter converter(content, kind); + converter.toPositions(foldings, [](auto&& folding) { return folding.range; }); + + for(auto&& folding: foldings) { + auto [beginOffset, endOffset] = folding.range; + auto [beginLine, beginChar] = converter.toPosition2(beginOffset); + auto [endLine, endChar] = converter.toPosition2(endOffset); + + result.emplace_back(proto::FoldingRange{ + .startLine = beginLine, + .startCharacter = beginChar, + .endLine = endLine, + .endCharacter = endChar, + .kind = proto::FoldingRangeKind::Region, + .collapsedText = folding.text, + }); + } + + co_return json::serialize(result); +} + LSPConverter::Result LSPConverter::convert(const feature::Hover& hover) { /// FIXME: Implement hover information render here. co_return json::Value(""); diff --git a/unittests/Feature/FoldingRange.cpp b/unittests/Feature/FoldingRange.cpp index f76b72eb..15028a7b 100644 --- a/unittests/Feature/FoldingRange.cpp +++ b/unittests/Feature/FoldingRange.cpp @@ -5,11 +5,9 @@ namespace clice::testing { namespace { -using namespace clice::feature::foldingrange; - struct FoldingRange : public ::testing::Test { std::optional tester; - Result result; + std::vector result; void run(llvm::StringRef source) { tester.emplace("main.cpp", source); @@ -17,396 +15,364 @@ struct FoldingRange : public ::testing::Test { tester->run(); auto& info = tester->info; - proto::FoldingRangeParams param; - result = foldingRange(param, *info); + result = feature::foldingRange(*info); } - index::Shared runWithHeader(llvm::StringRef source, llvm::StringRef header) { + index::Shared> runWithHeader(llvm::StringRef source, + llvm::StringRef header) { tester.emplace("main.cpp", source); tester->addFile(path::join(".", "header.h"), header); tester->run(); auto& info = tester->info; - - proto::FoldingRangeParams param; - return foldingRange(*info); + return feature::indexFoldingRange(*info); } void EXPECT_RANGE(std::size_t index, llvm::StringRef begin, llvm::StringRef end, + feature::FoldingRangeKind kind, std::source_location current = std::source_location::current()) { auto& folding = result[index]; auto begOff = tester->offset(begin); - EXPECT_EQ(begOff, folding.range.begin); + EXPECT_EQ(begOff, folding.range.begin, current); auto endOff = tester->offset(end); - EXPECT_EQ(endOff, folding.range.end); + EXPECT_EQ(endOff, folding.range.end, current); } }; +using enum feature::FoldingRangeKind::Kind; + TEST_F(FoldingRange, Namespace) { run(R"cpp( +namespace single_line { } -namespace single_line {$(1) - // -$(2)} +namespace with_nodes $(1){ + struct inner $(3){ + int x; + }$(4); +}$(2) -namespace with_nodes {$(3) -// -struct _ {}; +namespace strange + $(5){ -$(4)} + }$(6) -namespace empty {} - -namespace ugly - -{$(5) - $(6)} +#define NS_BEGIN namespace ns { +#define NS_END } +$(7)NS_BEGIN +NS_END$(8) )cpp"); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); -} - -TEST_F(FoldingRange, NamespaceExpandedFromMacro) { - run(R"cpp( -#define NS_OUTER namespace outter { -#define NS_INNER namespace inner { -#define END_MACRO } - -NS_OUTER$(1) - NS_INNER$(3) - namespace inner {$(5) - - $(6)} - END_MACRO$(4) -END_MACRO$(2) - -)cpp"); - - EXPECT_EQ(result.size(), 3); - - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); + EXPECT_EQ(result.size(), 4); + EXPECT_RANGE(0, "1", "2", Namespace); + EXPECT_RANGE(1, "3", "4", Namespace); + EXPECT_RANGE(2, "5", "6", Namespace); + EXPECT_RANGE(3, "7", "8", Namespace); } TEST_F(FoldingRange, Enum) { run(R"cpp( -enum _0 {$(1) +enum e1 $(1){ A, B, C -$(2)}; +}$(2); -enum _1 { D }; - -enum class _2 {$(3) +enum class e2 $(3){ A, B, C -$(4)}; +}$(4); + +enum e3 { D }; )cpp"); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); + EXPECT_EQ(result.size(), 2); + EXPECT_RANGE(0, "1", "2", Enum); + EXPECT_RANGE(1, "3", "4", Enum); } -TEST_F(FoldingRange, RecordDecl) { +TEST_F(FoldingRange, Record) { run(R"cpp( -// struct _2 {$(1) -// int x; -// float y; -// $(2)}; -// -// struct _3 {}; -// -// struct _4; -// -// union _5 {$(3) -// int x; -// float y; -// $(4)}; -// -// struct _6 {$(5) -// struct one_nested {$(7) -// // -// $(8)}; -// -// // -// $(6)}; +struct s1 $(1){ + int x; + float y; +}$(2); -void f() {$(9) - struct another_nested {$(11) - // - $(12)}; -$(10)} +struct s2 {}; +struct s3; + +union u1 $(3){ + int x; + float y; +}$(4); + +struct u2 $(5){ + struct s4 $(7){ + + }$(8); +}$(6); + +void foo() $(9){ + struct s5 $(11){ + + }$(12); +}$(10) )cpp"); - // EXPECT_RANGE(0, "1", "2"); - // EXPECT_RANGE(1, "3", "4"); - // EXPECT_RANGE(2, "5", "6"); - // EXPECT_RANGE(3, "7", "8"); - // EXPECT_RANGE(4, "9", "10"); - // EXPECT_RANGE(5, "11", "12"); - EXPECT_RANGE(0, "9", "10"); - EXPECT_RANGE(1, "11", "12"); + EXPECT_EQ(result.size(), 6); + EXPECT_RANGE(0, "1", "2", Struct); + EXPECT_RANGE(1, "3", "4", Union); + EXPECT_RANGE(2, "5", "6", Struct); + EXPECT_RANGE(3, "7", "8", Struct); + EXPECT_RANGE(4, "9", "10", FunctionBody); + EXPECT_RANGE(5, "11", "12", Struct); } -TEST_F(FoldingRange, CXXRecordDeclAndMemberMethod) { +TEST_F(FoldingRange, Method) { run(R"cpp( -struct _2 {$(1) +struct s2 $(1){ int x; float y; - _2() = default; -$(2)}; + s2() = default; +}$(2); -struct _3 {$(3) - void method() {$(5) +struct s3; + +struct s3 $(3){ + void method() $(5){ int x = 0; - $(6)} + }$(6) - void parameter () {$(7) - // - $(8)} + void parameter() $(7){ + + }$(8) void skip() {}; -$(4)}; - -struct _4; +}$(4); )cpp"); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); - EXPECT_RANGE(3, "7", "8"); + EXPECT_EQ(result.size(), 4); + EXPECT_RANGE(0, "1", "2", Struct); + EXPECT_RANGE(1, "3", "4", Struct); + EXPECT_RANGE(2, "5", "6", FunctionBody); + EXPECT_RANGE(3, "7", "8", FunctionBody); } -TEST_F(FoldingRange, LambdaCapture) { +TEST_F(FoldingRange, Lambda) { run(R"cpp( -auto z = [$(1) +auto z = $(1)[ x = 0, y = 1 - $(2)]() {$(3) - // -$(4)}; +]$(2) () $(3){ -int array[4] = {0}; +}$(4); -auto s = [$(5) +static int array[4]; + +auto s = $(5)[ x=0, y = 1, z = array[ 0], k = -1 - $(6)](){ return; }; +]$(6) () $(7){ + return; +}$(8); -)cpp"); +auto l1 = [] () {}; - EXPECT_EQ(result.size(), 3); +auto l2 = [] () $(9){ + +}$(10); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); -} - -TEST_F(FoldingRange, LambdaExpression) { - run(R"cpp( -auto _0 = [](int _) {}; - -auto _1 = [](int _) {$(1) - // -$(2)}; - -auto _2 = [](int _) {$(3) - // +auto l3 = [] () $(11){ return 0; - $(4)}; - -auto _3 = []($(5) - int _1, - int _2 - $(6)) {}; +}$(12); +auto l4 = [] $(13)( + int x1, + int x2 +)$(14) {}; )cpp"); - EXPECT_EQ(result.size(), 3); - - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); + EXPECT_EQ(result.size(), 7); + EXPECT_RANGE(0, "1", "2", LambdaCapture); + EXPECT_RANGE(1, "3", "4", FunctionBody); + EXPECT_RANGE(2, "5", "6", LambdaCapture); + EXPECT_RANGE(3, "7", "8", FunctionBody); + EXPECT_RANGE(4, "9", "10", FunctionBody); + EXPECT_RANGE(5, "11", "12", FunctionBody); + EXPECT_RANGE(6, "13", "14", FunctionBody); } -TEST_F(FoldingRange, FunctionParams) { +TEST_F(FoldingRange, Function) { run(R"cpp( -void e() {} +void e() {}; -void f($(1) -// -// -$(2)) {} +void f $(1)( -void g($(3) -int x, -int y = 2 -// -$(4)) {} -void d($(5) - int _1, - int _2, - ... - $(6)); -)cpp"); +)$(2) $(3){ - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); -} +}$(4) -TEST_F(FoldingRange, FunctionBody) { - run(R"cpp( -void f() {$(1) -// -// -$(2)} +void g $(5)( + int x, + int y = 2 +)$(6) $(7){ + int z; +}$(8) -void g() {$(3) +void h() $(9){ int x = 0; -$(4)} +}$(10) -void e() {} +void i( ) { }; -void n() {$(5) - {$(7) - // empty bock - $(8)} - // -$(6)} +void j $(11)( + int p1, + int p2, + ... +)$(12); + +void k() $(13){ + +}$(14) )cpp"); - EXPECT_EQ(result.size(), 4); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); - EXPECT_RANGE(3, "7", "8"); + EXPECT_EQ(result.size(), 7); + EXPECT_RANGE(0, "1", "2", FunctionParams); + EXPECT_RANGE(1, "3", "4", FunctionBody); + EXPECT_RANGE(2, "5", "6", FunctionParams); + EXPECT_RANGE(3, "7", "8", FunctionBody); + EXPECT_RANGE(4, "9", "10", FunctionBody); + EXPECT_RANGE(5, "11", "12", FunctionParams); + EXPECT_RANGE(6, "13", "14", FunctionBody); } TEST_F(FoldingRange, FunctionCall) { run(R"cpp( -int f(int _1, int _2, int _3, int _4, int _5, int _6) { return _1 + _2; } +int f(int p1, int p2, int p3, int p4, int p5, int p6) { return p1 + p2; } -int main() {$(1) - - int _ = f(1, (1 + 2), 3, 4, 5, 6); - - return f($(3) - 1, 2, 3, +int main() $(1){ + int x = f(1, 2, 3, 4, 5, 6); + + int y = f $(2)( + 1, 2, 3, 4, 5, 6 - $(4)); -$(2)} + )$(3); + + return f $(4)( + 1, 2, 3, + 4, 5, 6 + )$(5); +}$(6) )cpp"); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); + EXPECT_EQ(result.size(), 3); + EXPECT_RANGE(0, "1", "6", FunctionBody); + EXPECT_RANGE(1, "2", "3", FunctionCall); + EXPECT_RANGE(2, "4", "5", FunctionCall); } TEST_F(FoldingRange, CompoundStmt) { run(R"cpp( -int main () {$(1) +int main() $(1){ - {$(3) - {$(5) + $(3){ + $(5){ // - $(6)} + }$(6) - {$(7) + $(7){ // - $(8)} + }$(8) // - $(4)} + }$(4) return 0; -$(2)} +}$(2) )cpp"); - - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); } TEST_F(FoldingRange, InitializeList) { run(R"cpp( struct L { int xs[4]; }; -L l1 = {$(1) +L l1 = $(1){ 1, 2, 3, 4 -$(2)}; +}$(2); -L l2 = {$(3) +L l2 = $(3){ // // -$(4)}; +}$(4); )cpp"); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); + EXPECT_EQ(result.size(), 2); + EXPECT_RANGE(0, "1", "2", Initializer); + EXPECT_RANGE(1, "3", "4", Initializer); } -TEST_F(FoldingRange, AccessControlBlock) { +TEST_F(FoldingRange, AccessSpecifier) { run(R"cpp( -struct empty { int x; }; +class c1 $(1){ +public$(3): +private$(4): +protected$(5): +}$(2); -class _0 {$(1) -public:$(3) +class c2 $(6){ +public$(8): int x; -$(4)private:$(5) - int z; -$(2)$(6)}; +private$(9): + float y; -struct _1 {$(7) +protected$(10): + double z; +}$(7); - int x; +#define PUBLIC public: +#define PRIVATE private: +#define PROTECTED protected: -private:$(9) - int z; -$(8)$(10)}; - -struct _2 {$(11) -public: -private: -public:$(13) - -int x = 1; - -$(12)$(14)}; +class c3 $(11){ +$(13)PUBLIC + int a; +$(15)PRIVATE$(14) + int b; +$(17)PROTECTED$(16) + int c; +}$(12); )cpp"); - EXPECT_EQ(result.size(), 9); + EXPECT_RANGE(0, "1", "2", Class); + EXPECT_RANGE(1, "3", "4", AccessSpecifier); + EXPECT_RANGE(2, "4", "5", AccessSpecifier); + EXPECT_RANGE(3, "5", "2", AccessSpecifier); - EXPECT_RANGE(0, "1", "2"); - EXPECT_RANGE(1, "3", "4"); - EXPECT_RANGE(2, "5", "6"); - EXPECT_RANGE(3, "7", "8"); - EXPECT_RANGE(4, "9", "10"); - EXPECT_RANGE(5, "11", "12"); + EXPECT_RANGE(4, "6", "7", Class); + EXPECT_RANGE(5, "8", "9", AccessSpecifier); + EXPECT_RANGE(6, "9", "10", AccessSpecifier); + EXPECT_RANGE(7, "10", "7", AccessSpecifier); - // do not test result[6] and result[7] - - EXPECT_RANGE(8, "13", "14"); + EXPECT_RANGE(8, "11", "12", Class); + EXPECT_RANGE(9, "13", "14", AccessSpecifier); + EXPECT_RANGE(10, "15", "16", AccessSpecifier); + EXPECT_RANGE(11, "17", "12", AccessSpecifier); } -TEST_F(FoldingRange, Macro) { +TEST_F(FoldingRange, Directive) { run(R"cpp( #ifdef M1 @@ -419,69 +385,30 @@ TEST_F(FoldingRange, Macro) { #endif )cpp"); - - EXPECT_EQ(result.size(), 3); } TEST_F(FoldingRange, PragmaRegion) { run(R"cpp( -#pragma region level1 $(1) - #pragma region level2 $(2) - #pragma region level3 $(3) +$(1)#pragma region level1 + $(2)#pragma region level2 + $(3)#pragma region level3 - $(4)#pragma endregion level3 + #$(4)pragma endregion level3 - $(5)#pragma endregion level2 + #$(5)pragma endregion level2 -$(6)#pragma endregion level1 +#$(6)pragma endregion level1 #pragma endregion // mismatch region, skipped +#pragma region // mismatch region, skipped +)cpp"); -// broken region, use the end of file as endregion -#pragma region $(7) - -$(eof))cpp"); - - EXPECT_EQ(result.size(), 4); - EXPECT_RANGE(0, "3", "4"); - EXPECT_RANGE(1, "2", "5"); - EXPECT_RANGE(2, "1", "6"); - EXPECT_RANGE(3, "7", "eof"); -} - -TEST_F(FoldingRange, WithHeader) { - auto header = R"cpp( -namespace _1 { - -namespace _2 { - -} - -} -)cpp"; - - auto source = R"cpp( -#include "header.h" - -int main() {$(3) - -$(4) -} -)cpp"; - - auto multifiles = runWithHeader(source, header); - EXPECT_EQ(multifiles.size(), 2); - - auto mainID = tester->info->srcMgr().getMainFileID(); - for(auto& [id, result]: multifiles) { - if(id == mainID) { - EXPECT_EQ(result.size(), 1); - } else { - EXPECT_EQ(result.size(), 2); - } - } + EXPECT_EQ(result.size(), 3); + EXPECT_RANGE(0, "1", "6", Region); + EXPECT_RANGE(1, "2", "5", Region); + EXPECT_RANGE(2, "3", "4", Region); } } // namespace