Skip to content

Commit abee8a8

Browse files
authored
[clang-doc] Add a breadcrumb navigation bar (llvm#173297)
This patch adds a breadcrumb navigation bar to the `<navbar>` element. Now, you can navigate between the different scopes of a record or namespace. This is done by keeping track of a Decl's parent Decl through its USR. That allows us to traverse the set of `Info`s through a directed graph during JSON generation to create `Context`s. A context is just a `Reference` that holds a relative path to a scope's file from a particular `Info`.
1 parent 4998280 commit abee8a8

13 files changed

Lines changed: 258 additions & 15 deletions

clang-tools-extra/clang-doc/BitcodeReader.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ static llvm::Error parseRecord(const Record &R, unsigned ID,
159159
return decodeRecord(R, I->Name, Blob);
160160
case NAMESPACE_PATH:
161161
return decodeRecord(R, I->Path, Blob);
162+
case NAMESPACE_PARENT_USR:
163+
return decodeRecord(R, I->ParentUSR, Blob);
162164
default:
163165
return llvm::createStringError(llvm::inconvertibleErrorCode(),
164166
"invalid field for NamespaceInfo");
@@ -184,6 +186,8 @@ static llvm::Error parseRecord(const Record &R, unsigned ID,
184186
return decodeRecord(R, I->IsTypeDef, Blob);
185187
case RECORD_MANGLED_NAME:
186188
return decodeRecord(R, I->MangledName, Blob);
189+
case RECORD_PARENT_USR:
190+
return decodeRecord(R, I->ParentUSR, Blob);
187191
default:
188192
return llvm::createStringError(llvm::inconvertibleErrorCode(),
189193
"invalid field for RecordInfo");

clang-tools-extra/clang-doc/BitcodeWriter.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ static const llvm::IndexedMap<RecordIdDsc, RecordIdToIndexFunctor>
174174
{NAMESPACE_USR, {"USR", &genSymbolIdAbbrev}},
175175
{NAMESPACE_NAME, {"Name", &genStringAbbrev}},
176176
{NAMESPACE_PATH, {"Path", &genStringAbbrev}},
177+
{NAMESPACE_PARENT_USR, {"ParentUSR", &genSymbolIdAbbrev}},
177178
{ENUM_USR, {"USR", &genSymbolIdAbbrev}},
178179
{ENUM_NAME, {"Name", &genStringAbbrev}},
179180
{ENUM_DEFLOCATION, {"DefLocation", &genLocationAbbrev}},
@@ -190,6 +191,7 @@ static const llvm::IndexedMap<RecordIdDsc, RecordIdToIndexFunctor>
190191
{RECORD_TAG_TYPE, {"TagType", &genIntAbbrev}},
191192
{RECORD_IS_TYPE_DEF, {"IsTypeDef", &genBoolAbbrev}},
192193
{RECORD_MANGLED_NAME, {"MangledName", &genStringAbbrev}},
194+
{RECORD_PARENT_USR, {"ParentUSR", &genSymbolIdAbbrev}},
193195
{BASE_RECORD_USR, {"USR", &genSymbolIdAbbrev}},
194196
{BASE_RECORD_NAME, {"Name", &genStringAbbrev}},
195197
{BASE_RECORD_PATH, {"Path", &genStringAbbrev}},
@@ -270,12 +272,12 @@ static const std::vector<std::pair<BlockId, std::vector<RecordId>>>
270272
{TYPEDEF_USR, TYPEDEF_NAME, TYPEDEF_DEFLOCATION, TYPEDEF_IS_USING}},
271273
// Namespace Block
272274
{BI_NAMESPACE_BLOCK_ID,
273-
{NAMESPACE_USR, NAMESPACE_NAME, NAMESPACE_PATH}},
275+
{NAMESPACE_USR, NAMESPACE_NAME, NAMESPACE_PATH, NAMESPACE_PARENT_USR}},
274276
// Record Block
275277
{BI_RECORD_BLOCK_ID,
276278
{RECORD_USR, RECORD_NAME, RECORD_PATH, RECORD_DEFLOCATION,
277279
RECORD_LOCATION, RECORD_TAG_TYPE, RECORD_IS_TYPE_DEF,
278-
RECORD_MANGLED_NAME}},
280+
RECORD_MANGLED_NAME, RECORD_PARENT_USR}},
279281
// BaseRecord Block
280282
{BI_BASE_RECORD_BLOCK_ID,
281283
{BASE_RECORD_USR, BASE_RECORD_NAME, BASE_RECORD_PATH,
@@ -570,6 +572,7 @@ void ClangDocBitcodeWriter::emitBlock(const NamespaceInfo &I) {
570572
emitRecord(I.USR, NAMESPACE_USR);
571573
emitRecord(I.Name, NAMESPACE_NAME);
572574
emitRecord(I.Path, NAMESPACE_PATH);
575+
emitRecord(I.ParentUSR, NAMESPACE_PARENT_USR);
573576
for (const auto &N : I.Namespace)
574577
emitBlock(N, FieldId::F_namespace);
575578
for (const auto &CI : I.Description)
@@ -624,6 +627,7 @@ void ClangDocBitcodeWriter::emitBlock(const RecordInfo &I) {
624627
emitRecord(I.Name, RECORD_NAME);
625628
emitRecord(I.Path, RECORD_PATH);
626629
emitRecord(I.MangledName, RECORD_MANGLED_NAME);
630+
emitRecord(I.ParentUSR, RECORD_PARENT_USR);
627631
for (const auto &N : I.Namespace)
628632
emitBlock(N, FieldId::F_namespace);
629633
for (const auto &CI : I.Description)

clang-tools-extra/clang-doc/BitcodeWriter.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ enum RecordId {
108108
NAMESPACE_USR,
109109
NAMESPACE_NAME,
110110
NAMESPACE_PATH,
111+
NAMESPACE_PARENT_USR,
111112
ENUM_USR,
112113
ENUM_NAME,
113114
ENUM_DEFLOCATION,
@@ -124,6 +125,7 @@ enum RecordId {
124125
RECORD_TAG_TYPE,
125126
RECORD_IS_TYPE_DEF,
126127
RECORD_MANGLED_NAME,
128+
RECORD_PARENT_USR,
127129
BASE_RECORD_USR,
128130
BASE_RECORD_NAME,
129131
BASE_RECORD_PATH,

clang-tools-extra/clang-doc/JSONGenerator.cpp

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,64 @@ static Object serializeComment(const CommentInfo &I, Object &Description) {
278278
llvm_unreachable("Unknown comment kind encountered.");
279279
}
280280

281+
/// Creates Contexts for namespaces and records to allow for navigation.
282+
static void generateContext(const Info &I, Object &Obj) {
283+
json::Value ContextArray = json::Array();
284+
auto &ContextArrayRef = *ContextArray.getAsArray();
285+
ContextArrayRef.reserve(I.Contexts.size());
286+
287+
std::string CurrentRelativePath;
288+
bool PreviousRecord = false;
289+
for (const auto &Current : I.Contexts) {
290+
json::Value ContextVal = Object();
291+
Object &Context = *ContextVal.getAsObject();
292+
serializeReference(Current, Context);
293+
294+
if (ContextArrayRef.empty() && I.IT == InfoType::IT_record) {
295+
if (Current.DocumentationFileName == "index") {
296+
// If the record's immediate context is a namespace, then the
297+
// "index.html" is in the same directory.
298+
PreviousRecord = false;
299+
Context["RelativePath"] = "./";
300+
} else {
301+
// If the immediate context is a record, then the file is one level
302+
// above
303+
PreviousRecord = true;
304+
CurrentRelativePath += "../";
305+
Context["RelativePath"] = CurrentRelativePath;
306+
}
307+
ContextArrayRef.push_back(ContextVal);
308+
continue;
309+
}
310+
311+
if (PreviousRecord && (Current.DocumentationFileName == "index")) {
312+
// If the previous Context was a record then we already went up a level,
313+
// so the current namespace index is in the same directory.
314+
PreviousRecord = false;
315+
} else if (Current.DocumentationFileName != "index") {
316+
// If the current Context is a record but the previous wasn't a record,
317+
// then the namespace index is located one level above.
318+
PreviousRecord = true;
319+
CurrentRelativePath += "../";
320+
} else {
321+
// The current Context is a namespace and so was the previous Context.
322+
PreviousRecord = false;
323+
CurrentRelativePath += "../";
324+
// If this namespace is the global namespace, then its documentation
325+
// name needs to be changed to link correctly.
326+
if (Current.QualName == "GlobalNamespace" && Current.RelativePath != "./")
327+
Context["DocumentationFileName"] =
328+
SmallString<16>("GlobalNamespace/index");
329+
}
330+
Context["RelativePath"] = CurrentRelativePath;
331+
ContextArrayRef.insert(ContextArrayRef.begin(), ContextVal);
332+
}
333+
334+
ContextArrayRef.back().getAsObject()->insert({"End", true});
335+
Obj["Contexts"] = ContextArray;
336+
Obj["HasContexts"] = true;
337+
}
338+
281339
static void
282340
serializeCommonAttributes(const Info &I, json::Object &Obj,
283341
const std::optional<StringRef> RepositoryUrl) {
@@ -323,6 +381,9 @@ serializeCommonAttributes(const Info &I, json::Object &Obj,
323381
Obj["Location"] =
324382
serializeLocation(Symbol->DefLoc.value(), RepositoryUrl);
325383
}
384+
385+
if (!I.Contexts.empty())
386+
generateContext(I, Obj);
326387
}
327388

328389
static void serializeReference(const Reference &Ref, Object &ReferenceObj) {
@@ -335,7 +396,7 @@ static void serializeReference(const Reference &Ref, Object &ReferenceObj) {
335396

336397
// If the reference is a nested class it will be put into a folder named
337398
// after the parent class. We can get that name from the path's stem.
338-
if (Ref.Path != "GlobalNamespace")
399+
if (Ref.Path != "GlobalNamespace" && !Ref.Path.empty())
339400
ReferenceObj["PathStem"] = sys::path::stem(Ref.Path);
340401
}
341402
}
@@ -730,6 +791,29 @@ static Error serializeIndex(const ClangDocContext &CDCtx, StringRef RootDir) {
730791
return Error::success();
731792
}
732793

794+
static void serializeContexts(Info *I,
795+
StringMap<std::unique_ptr<Info>> &Infos) {
796+
if (I->USR == GlobalNamespaceID)
797+
return;
798+
auto ParentUSR = I->ParentUSR;
799+
800+
while (true) {
801+
auto &ParentInfo = Infos.at(llvm::toHex(ParentUSR));
802+
803+
if (ParentInfo && ParentInfo->USR == GlobalNamespaceID) {
804+
Context GlobalRef(ParentInfo->USR, "Global Namespace",
805+
InfoType::IT_namespace, "GlobalNamespace", "",
806+
SmallString<16>("index"));
807+
I->Contexts.push_back(GlobalRef);
808+
return;
809+
}
810+
811+
Context ParentRef(*ParentInfo);
812+
I->Contexts.push_back(ParentRef);
813+
ParentUSR = ParentInfo->ParentUSR;
814+
}
815+
}
816+
733817
Error JSONGenerator::generateDocumentation(
734818
StringRef RootDir, llvm::StringMap<std::unique_ptr<doc::Info>> Infos,
735819
const ClangDocContext &CDCtx, std::string DirName) {
@@ -763,9 +847,12 @@ Error JSONGenerator::generateDocumentation(
763847
if (FileErr)
764848
return createFileError("cannot open file " + Group.getKey(), FileErr);
765849

766-
for (const auto &Info : Group.getValue())
850+
for (const auto &Info : Group.getValue()) {
851+
if (Info->IT == InfoType::IT_record || Info->IT == InfoType::IT_namespace)
852+
serializeContexts(Info, Infos);
767853
if (Error Err = generateDocForInfo(Info, InfoOS, CDCtx))
768854
return Err;
855+
}
769856
}
770857

771858
return serializeIndex(CDCtx, RootDir);

clang-tools-extra/clang-doc/Representation.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ void Info::mergeBase(Info &&Other) {
273273
llvm::sort(Description);
274274
auto Last = llvm::unique(Description);
275275
Description.erase(Last, Description.end());
276+
if (ParentUSR == EmptySID)
277+
ParentUSR = Other.ParentUSR;
278+
if (DocumentationFileName.empty())
279+
DocumentationFileName = Other.DocumentationFileName;
276280
}
277281

278282
bool Info::mergeable(const Info &Other) {

clang-tools-extra/clang-doc/Representation.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,16 @@ struct Reference {
165165
SmallString<16> DocumentationFileName;
166166
};
167167

168+
// A Context is a reference that holds a relative path from a certain Info's
169+
// location.
170+
struct Context : public Reference {
171+
Context(SymbolID USR, StringRef Name, InfoType IT, StringRef QualName,
172+
StringRef Path, SmallString<16> DocumentationFileName)
173+
: Reference(USR, Name, IT, QualName, Path, DocumentationFileName) {}
174+
explicit Context(const Info &I);
175+
SmallString<128> RelativePath;
176+
};
177+
168178
// Holds the children of a record or namespace.
169179
struct ScopeChildren {
170180
// Namespaces and Records are references because they will be properly
@@ -356,13 +366,21 @@ struct Info {
356366
// Unique identifier for the decl described by this Info.
357367
SymbolID USR = SymbolID();
358368

369+
// Currently only used for namespaces and records.
370+
SymbolID ParentUSR = SymbolID();
371+
359372
// InfoType of this particular Info.
360373
InfoType IT = InfoType::IT_default;
361374

362375
// Comment description of this decl.
363376
std::vector<CommentInfo> Description;
377+
378+
SmallVector<Context, 4> Contexts;
364379
};
365380

381+
inline Context::Context(const Info &I)
382+
: Reference(I.USR, I.Name, I.IT, I.Name, I.Path, I.DocumentationFileName) {}
383+
366384
// Info for namespaces.
367385
struct NamespaceInfo : public Info {
368386
NamespaceInfo(SymbolID USR = SymbolID(), StringRef Name = StringRef(),

clang-tools-extra/clang-doc/Serialize.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,10 +697,46 @@ static TemplateParamInfo convertTemplateArgToInfo(const clang::Decl *D,
697697
return TemplateParamInfo(Str);
698698
}
699699

700+
// Check if the DeclKind is one for which we support contextual relationships.
701+
// There might be other ContextDecls, like blocks, that we currently don't
702+
// handle at all.
703+
static bool isSupportedContext(Decl::Kind DeclKind) {
704+
switch (DeclKind) {
705+
case Decl::Kind::Record:
706+
case Decl::Kind::CXXRecord:
707+
case Decl::Kind::ClassTemplateSpecialization:
708+
case Decl::Kind::ClassTemplatePartialSpecialization:
709+
case Decl::Kind::Namespace:
710+
return true;
711+
default:
712+
return false;
713+
}
714+
}
715+
716+
static void findParent(Info &I, const Decl *D) {
717+
assert(D && "Invalid Decl");
718+
719+
// Only walk up contexts if D is a record or namespace.
720+
if (!isSupportedContext(D->getKind()))
721+
return;
722+
723+
const DeclContext *ParentCtx = dyn_cast<DeclContext>(D)->getLexicalParent();
724+
while (ParentCtx) {
725+
if (isSupportedContext(ParentCtx->getDeclKind())) {
726+
// Break when we reach the first record or namespace.
727+
I.ParentUSR = getUSRForDecl(dyn_cast<Decl>(ParentCtx));
728+
break;
729+
}
730+
ParentCtx = ParentCtx->getParent();
731+
}
732+
}
733+
700734
template <typename T>
701735
static void populateInfo(Info &I, const T *D, const FullComment *C,
702736
bool &IsInAnonymousNamespace) {
703737
I.USR = getUSRForDecl(D);
738+
findParent(I, D);
739+
704740
if (auto ConversionDecl = dyn_cast_or_null<CXXConversionDecl>(D);
705741
ConversionDecl && ConversionDecl->getConversionType()
706742
.getTypePtr()

clang-tools-extra/clang-doc/assets/clang-doc-mustache.css

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ body, html {
112112
width: 100%;
113113
top: 0;
114114
left: 0;
115-
height: 60px; /* Adjust as needed */
116115
color: white;
117116
display: flex;
118117
align-items: center;
@@ -255,6 +254,38 @@ body, html {
255254
color:var(--text1)
256255
}
257256

257+
.navbar-breadcrumb-container {
258+
position: absolute;
259+
top: 60px;
260+
left: 0;
261+
width: 100%;
262+
background: var(--surface2);
263+
padding: 0.5rem 1rem;
264+
display: flex;
265+
gap: 0.5rem;
266+
border-bottom: 1px solid var(--text2);
267+
box-sizing: border-box;
268+
border-top: 1px solid var(--text2);
269+
border-bottom: 1px solid var(--text2);
270+
}
271+
272+
.navbar-breadcrumb-item {
273+
padding: 0.25rem 0.75rem;
274+
background: var(--surface1);
275+
border: 1px solid var(--text2);
276+
border-radius: 4px;
277+
color: var(--text1);
278+
font-size: 0.9rem;
279+
white-space: nowrap;
280+
}
281+
282+
.navbar-breadcrumb-item:hover {
283+
background: var(--brand);
284+
color: var(--text1-inverse);
285+
border-color: var(--brand);
286+
cursor: pointer;
287+
}
288+
258289
.hero__container {
259290
margin-top:1rem;
260291
display:flex;
@@ -317,9 +348,7 @@ body, html {
317348
max-width: 2048px;
318349
margin-left:auto;
319350
margin-right:auto;
320-
margin-top:0;
321351
margin-bottom: 1rem;
322-
padding:1rem 2rem
323352
}
324353

325354
@media(max-width:768px) {
@@ -404,16 +433,17 @@ body, html {
404433

405434
.sidebar {
406435
width: 250px;
407-
top: 60px;
408436
left: 0;
409-
height: 100%;
437+
top: 60px;
438+
bottom: 0;
410439
position: fixed;
411440
background-color: var(--surface1);
412441
display: flex;
413442
border-left: 1px solid var(--text2);
414443
flex-direction: column;
415444
overflow-y: auto;
416445
scrollbar-width: thin;
446+
flex-shrink: 0;
417447
}
418448

419449
.sidebar h2 {
@@ -445,8 +475,8 @@ body, html {
445475

446476
/* Content */
447477
.content {
478+
top: 60px;
448479
background-color: var(--text1-inverse);
449-
padding: 20px;
450480
left: 250px;
451481
position: relative;
452482
width: calc(100% - 250px);

0 commit comments

Comments
 (0)