Skip to content

Commit d75fb1e

Browse files
committed
[clangd] Support #pragma mark in the outline
Xcode uses `#pragma mark -` to draw a divider in the outline view and `#pragma mark Note` to add `Note` in the outline view. For more information, see https://nshipster.com/pragma/. Since the LSP spec doesn't contain dividers for the symbol outline, instead we treat `#pragma mark -` as a group with children - the decls that come after it, implicitly terminating when the symbol's parent ends. The following code: ``` @implementation MyClass - (id)init {} - (int)foo; @EnD ``` Would give an outline like ``` MyClass > Overrides > init > Public Accessors > foo ``` Differential Revision: https://reviews.llvm.org/D105904
1 parent 5fcde57 commit d75fb1e

File tree

12 files changed

+327
-6
lines changed

12 files changed

+327
-6
lines changed

Diff for: clang-tools-extra/clangd/CollectMacros.cpp

+28
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,33 @@ void CollectMainFileMacros::add(const Token &MacroNameTok, const MacroInfo *MI,
3030
else
3131
Out.UnknownMacros.push_back({Range, IsDefinition});
3232
}
33+
34+
class CollectPragmaMarks : public PPCallbacks {
35+
public:
36+
explicit CollectPragmaMarks(const SourceManager &SM,
37+
std::vector<clangd::PragmaMark> &Out)
38+
: SM(SM), Out(Out) {}
39+
40+
void PragmaMark(SourceLocation Loc, StringRef Trivia) override {
41+
if (isInsideMainFile(Loc, SM)) {
42+
// FIXME: This range should just cover `XX` in `#pragma mark XX` and
43+
// `- XX` in `#pragma mark - XX`.
44+
Position Start = sourceLocToPosition(SM, Loc);
45+
Position End = {Start.line + 1, 0};
46+
Out.emplace_back(clangd::PragmaMark{{Start, End}, Trivia.str()});
47+
}
48+
}
49+
50+
private:
51+
const SourceManager &SM;
52+
std::vector<clangd::PragmaMark> &Out;
53+
};
54+
55+
std::unique_ptr<PPCallbacks>
56+
collectPragmaMarksCallback(const SourceManager &SM,
57+
std::vector<PragmaMark> &Out) {
58+
return std::make_unique<CollectPragmaMarks>(SM, Out);
59+
}
60+
3361
} // namespace clangd
3462
} // namespace clang

Diff for: clang-tools-extra/clangd/CollectMacros.h

+12
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ class CollectMainFileMacros : public PPCallbacks {
9999
MainFileMacros &Out;
100100
};
101101

102+
/// Represents a `#pragma mark` in the main file.
103+
///
104+
/// There can be at most one pragma mark per line.
105+
struct PragmaMark {
106+
Range Rng;
107+
std::string Trivia;
108+
};
109+
110+
/// Collect all pragma marks from the main file.
111+
std::unique_ptr<PPCallbacks>
112+
collectPragmaMarksCallback(const SourceManager &, std::vector<PragmaMark> &Out);
113+
102114
} // namespace clangd
103115
} // namespace clang
104116

Diff for: clang-tools-extra/clangd/FindSymbols.cpp

+127-1
Original file line numberDiff line numberDiff line change
@@ -523,9 +523,135 @@ class DocumentOutline {
523523
ParsedAST &AST;
524524
};
525525

526+
struct PragmaMarkSymbol {
527+
DocumentSymbol DocSym;
528+
bool IsGroup;
529+
};
530+
531+
/// Merge in `PragmaMarkSymbols`, sorted ascending by range, into the given
532+
/// `DocumentSymbol` tree.
533+
void mergePragmas(DocumentSymbol &Root, ArrayRef<PragmaMarkSymbol> Pragmas) {
534+
while (!Pragmas.empty()) {
535+
// We'll figure out where the Pragmas.front() should go.
536+
PragmaMarkSymbol P = std::move(Pragmas.front());
537+
Pragmas = Pragmas.drop_front();
538+
DocumentSymbol *Cur = &Root;
539+
while (Cur->range.contains(P.DocSym.range)) {
540+
bool Swapped = false;
541+
for (auto &C : Cur->children) {
542+
// We assume at most 1 child can contain the pragma (as pragmas are on
543+
// a single line, and children have disjoint ranges).
544+
if (C.range.contains(P.DocSym.range)) {
545+
Cur = &C;
546+
Swapped = true;
547+
break;
548+
}
549+
}
550+
// Cur is the parent of P since none of the children contain P.
551+
if (!Swapped)
552+
break;
553+
}
554+
// Pragma isn't a group so we can just insert it and we are done.
555+
if (!P.IsGroup) {
556+
Cur->children.emplace_back(std::move(P.DocSym));
557+
continue;
558+
}
559+
// Pragma is a group, so we need to figure out where it terminates:
560+
// - If the next Pragma is not contained in Cur, P owns all of its
561+
// parent's children which occur after P.
562+
// - If the next pragma is contained in Cur but actually belongs to one
563+
// of the parent's children, we temporarily skip over it and look at
564+
// the next pragma to decide where we end.
565+
// - Otherwise nest all of its parent's children which occur after P but
566+
// before the next pragma.
567+
bool TerminatedByNextPragma = false;
568+
for (auto &NextPragma : Pragmas) {
569+
// If we hit a pragma outside of Cur, the rest will be outside as well.
570+
if (!Cur->range.contains(NextPragma.DocSym.range))
571+
break;
572+
573+
// NextPragma cannot terminate P if it is nested inside a child, look for
574+
// the next one.
575+
if (llvm::any_of(Cur->children, [&NextPragma](const auto &Child) {
576+
return Child.range.contains(NextPragma.DocSym.range);
577+
}))
578+
continue;
579+
580+
// Pragma owns all the children between P and NextPragma
581+
auto It = llvm::partition(Cur->children,
582+
[&P, &NextPragma](const auto &S) -> bool {
583+
return !(P.DocSym.range < S.range &&
584+
S.range < NextPragma.DocSym.range);
585+
});
586+
P.DocSym.children.assign(make_move_iterator(It),
587+
make_move_iterator(Cur->children.end()));
588+
Cur->children.erase(It, Cur->children.end());
589+
TerminatedByNextPragma = true;
590+
break;
591+
}
592+
if (!TerminatedByNextPragma) {
593+
// P is terminated by the end of current symbol, hence it owns all the
594+
// children after P.
595+
auto It = llvm::partition(Cur->children, [&P](const auto &S) -> bool {
596+
return !(P.DocSym.range < S.range);
597+
});
598+
P.DocSym.children.assign(make_move_iterator(It),
599+
make_move_iterator(Cur->children.end()));
600+
Cur->children.erase(It, Cur->children.end());
601+
}
602+
// Update the range for P to cover children and append to Cur.
603+
for (DocumentSymbol &Sym : P.DocSym.children)
604+
unionRanges(P.DocSym.range, Sym.range);
605+
Cur->children.emplace_back(std::move(P.DocSym));
606+
}
607+
}
608+
609+
PragmaMarkSymbol markToSymbol(const PragmaMark &P) {
610+
StringRef Name = StringRef(P.Trivia).trim();
611+
bool IsGroup = false;
612+
// "-\s+<group name>" or "<name>" after an initial trim. The former is
613+
// considered a group, the latter just a mark. Like Xcode, we don't consider
614+
// `-Foo` to be a group (space(s) after the `-` is required).
615+
//
616+
// We need to include a name here, otherwise editors won't properly render the
617+
// symbol.
618+
StringRef MaybeGroupName = Name;
619+
if (MaybeGroupName.consume_front("-") &&
620+
(MaybeGroupName.ltrim() != MaybeGroupName || MaybeGroupName.empty())) {
621+
Name = MaybeGroupName.empty() ? "(unnamed group)" : MaybeGroupName.ltrim();
622+
IsGroup = true;
623+
} else if (Name.empty()) {
624+
Name = "(unnamed mark)";
625+
}
626+
DocumentSymbol Sym;
627+
Sym.name = Name.str();
628+
Sym.kind = SymbolKind::File;
629+
Sym.range = P.Rng;
630+
Sym.selectionRange = P.Rng;
631+
return {Sym, IsGroup};
632+
}
633+
526634
std::vector<DocumentSymbol> collectDocSymbols(ParsedAST &AST) {
527-
return DocumentOutline(AST).build();
635+
std::vector<DocumentSymbol> Syms = DocumentOutline(AST).build();
636+
637+
const auto &PragmaMarks = AST.getMarks();
638+
if (PragmaMarks.empty())
639+
return Syms;
640+
641+
std::vector<PragmaMarkSymbol> Pragmas;
642+
Pragmas.reserve(PragmaMarks.size());
643+
for (const auto &P : PragmaMarks)
644+
Pragmas.push_back(markToSymbol(P));
645+
Range EntireFile = {
646+
{0, 0},
647+
{std::numeric_limits<int>::max(), std::numeric_limits<int>::max()}};
648+
DocumentSymbol Root;
649+
Root.children = std::move(Syms);
650+
Root.range = EntireFile;
651+
mergePragmas(Root, llvm::makeArrayRef(Pragmas));
652+
return Root.children;
528653
}
654+
529655
} // namespace
530656

531657
llvm::Expected<std::vector<DocumentSymbol>> getDocumentSymbols(ParsedAST &AST) {

Diff for: clang-tools-extra/clangd/ParsedAST.cpp

+12-2
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,13 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
449449
std::make_unique<CollectMainFileMacros>(Clang->getSourceManager(),
450450
Macros));
451451

452+
std::vector<PragmaMark> Marks;
453+
// FIXME: We need to patch the marks for stale preambles.
454+
if (Preamble)
455+
Marks = Preamble->Marks;
456+
Clang->getPreprocessor().addPPCallbacks(
457+
collectPragmaMarksCallback(Clang->getSourceManager(), Marks));
458+
452459
// Copy over the includes from the preamble, then combine with the
453460
// non-preamble includes below.
454461
CanonicalIncludes CanonIncludes;
@@ -510,7 +517,7 @@ ParsedAST::build(llvm::StringRef Filename, const ParseInputs &Inputs,
510517
}
511518
return ParsedAST(Inputs.Version, std::move(Preamble), std::move(Clang),
512519
std::move(Action), std::move(Tokens), std::move(Macros),
513-
std::move(ParsedDecls), std::move(Diags),
520+
std::move(Marks), std::move(ParsedDecls), std::move(Diags),
514521
std::move(Includes), std::move(CanonIncludes));
515522
}
516523

@@ -550,6 +557,7 @@ llvm::ArrayRef<Decl *> ParsedAST::getLocalTopLevelDecls() {
550557
}
551558

552559
const MainFileMacros &ParsedAST::getMacros() const { return Macros; }
560+
const std::vector<PragmaMark> &ParsedAST::getMarks() const { return Marks; }
553561

554562
std::size_t ParsedAST::getUsedBytes() const {
555563
auto &AST = getASTContext();
@@ -596,12 +604,14 @@ ParsedAST::ParsedAST(llvm::StringRef Version,
596604
std::unique_ptr<CompilerInstance> Clang,
597605
std::unique_ptr<FrontendAction> Action,
598606
syntax::TokenBuffer Tokens, MainFileMacros Macros,
607+
std::vector<PragmaMark> Marks,
599608
std::vector<Decl *> LocalTopLevelDecls,
600609
llvm::Optional<std::vector<Diag>> Diags,
601610
IncludeStructure Includes, CanonicalIncludes CanonIncludes)
602611
: Version(Version), Preamble(std::move(Preamble)), Clang(std::move(Clang)),
603612
Action(std::move(Action)), Tokens(std::move(Tokens)),
604-
Macros(std::move(Macros)), Diags(std::move(Diags)),
613+
Macros(std::move(Macros)), Marks(std::move(Marks)),
614+
Diags(std::move(Diags)),
605615
LocalTopLevelDecls(std::move(LocalTopLevelDecls)),
606616
Includes(std::move(Includes)), CanonIncludes(std::move(CanonIncludes)) {
607617
Resolver = std::make_unique<HeuristicResolver>(getASTContext());

Diff for: clang-tools-extra/clangd/ParsedAST.h

+6-1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class ParsedAST {
101101
/// Gets all macro references (definition, expansions) present in the main
102102
/// file, including those in the preamble region.
103103
const MainFileMacros &getMacros() const;
104+
/// Gets all pragma marks in the main file.
105+
const std::vector<PragmaMark> &getMarks() const;
104106
/// Tokens recorded while parsing the main file.
105107
/// (!) does not have tokens from the preamble.
106108
const syntax::TokenBuffer &getTokens() const { return Tokens; }
@@ -121,7 +123,8 @@ class ParsedAST {
121123
std::shared_ptr<const PreambleData> Preamble,
122124
std::unique_ptr<CompilerInstance> Clang,
123125
std::unique_ptr<FrontendAction> Action, syntax::TokenBuffer Tokens,
124-
MainFileMacros Macros, std::vector<Decl *> LocalTopLevelDecls,
126+
MainFileMacros Macros, std::vector<PragmaMark> Marks,
127+
std::vector<Decl *> LocalTopLevelDecls,
125128
llvm::Optional<std::vector<Diag>> Diags, IncludeStructure Includes,
126129
CanonicalIncludes CanonIncludes);
127130

@@ -144,6 +147,8 @@ class ParsedAST {
144147

145148
/// All macro definitions and expansions in the main file.
146149
MainFileMacros Macros;
150+
// Pragma marks in the main file.
151+
std::vector<PragmaMark> Marks;
147152
// Data, stored after parsing. None if AST was built with a stale preamble.
148153
llvm::Optional<std::vector<Diag>> Diags;
149154
// Top-level decls inside the current file. Not that this does not include

Diff for: clang-tools-extra/clangd/Preamble.cpp

+7-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ class CppFilePreambleCallbacks : public PreambleCallbacks {
7373

7474
MainFileMacros takeMacros() { return std::move(Macros); }
7575

76+
std::vector<PragmaMark> takeMarks() { return std::move(Marks); }
77+
7678
CanonicalIncludes takeCanonicalIncludes() { return std::move(CanonIncludes); }
7779

7880
bool isMainFileIncludeGuarded() const { return IsMainFileIncludeGuarded; }
@@ -103,7 +105,9 @@ class CppFilePreambleCallbacks : public PreambleCallbacks {
103105

104106
return std::make_unique<PPChainedCallbacks>(
105107
collectIncludeStructureCallback(*SourceMgr, &Includes),
106-
std::make_unique<CollectMainFileMacros>(*SourceMgr, Macros));
108+
std::make_unique<PPChainedCallbacks>(
109+
std::make_unique<CollectMainFileMacros>(*SourceMgr, Macros),
110+
collectPragmaMarksCallback(*SourceMgr, Marks)));
107111
}
108112

109113
CommentHandler *getCommentHandler() override {
@@ -130,6 +134,7 @@ class CppFilePreambleCallbacks : public PreambleCallbacks {
130134
IncludeStructure Includes;
131135
CanonicalIncludes CanonIncludes;
132136
MainFileMacros Macros;
137+
std::vector<PragmaMark> Marks;
133138
bool IsMainFileIncludeGuarded = false;
134139
std::unique_ptr<CommentHandler> IWYUHandler = nullptr;
135140
const clang::LangOptions *LangOpts = nullptr;
@@ -389,6 +394,7 @@ buildPreamble(PathRef FileName, CompilerInvocation CI,
389394
Result->Diags = std::move(Diags);
390395
Result->Includes = CapturedInfo.takeIncludes();
391396
Result->Macros = CapturedInfo.takeMacros();
397+
Result->Marks = CapturedInfo.takeMarks();
392398
Result->CanonIncludes = CapturedInfo.takeCanonicalIncludes();
393399
Result->StatCache = std::move(StatCache);
394400
Result->MainIsIncludeGuarded = CapturedInfo.isMainFileIncludeGuarded();

Diff for: clang-tools-extra/clangd/Preamble.h

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ struct PreambleData {
6161
// Users care about headers vs main-file, not preamble vs non-preamble.
6262
// These should be treated as main-file entities e.g. for code completion.
6363
MainFileMacros Macros;
64+
// Pragma marks defined in the preamble section of the main file.
65+
std::vector<PragmaMark> Marks;
6466
// Cache of FS operations performed when building the preamble.
6567
// When reusing a preamble, this cache can be consumed to save IO.
6668
std::unique_ptr<PreambleFileStatusCache> StatCache;

Diff for: clang-tools-extra/clangd/SourceCode.cpp

+7
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,13 @@ Range halfOpenToRange(const SourceManager &SM, CharSourceRange R) {
471471
return {Begin, End};
472472
}
473473

474+
void unionRanges(Range &A, Range B) {
475+
if (B.start < A.start)
476+
A.start = B.start;
477+
if (A.end < B.end)
478+
A.end = B.end;
479+
}
480+
474481
std::pair<size_t, size_t> offsetToClangLineColumn(llvm::StringRef Code,
475482
size_t Offset) {
476483
Offset = std::min(Code.size(), Offset);

Diff for: clang-tools-extra/clangd/SourceCode.h

+3
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ llvm::StringRef toSourceCode(const SourceManager &SM, SourceRange R);
129129
// Note that clang also uses closed source ranges, which this can't handle!
130130
Range halfOpenToRange(const SourceManager &SM, CharSourceRange R);
131131

132+
// Expand range `A` to also contain `B`.
133+
void unionRanges(Range &A, Range B);
134+
132135
// Converts an offset to a clang line/column (1-based, columns are bytes).
133136
// The offset must be in range [0, Code.size()].
134137
// Prefer to use SourceManager if one is available.

0 commit comments

Comments
 (0)