Autodoc: new decl search system (#15475)

New search system is based on a Radix Tree. The Radix Tree contains a shallow list of all decl names (ie no paths), plus some suffixes, split by following the official style guide (eg "HashMapUnmanaged" also produces "MapUnmanaged" and "Unmanaged", same with snake_case and camelCase names).

Additionally, the search system uses the decl graph data to recognize hierarchical relationships between decls, allowing you to zero on a target namespace for search. As an example "fs create" will score highe all things related to the creation of files and directories inside of `std.fs`, while still showing (but with lower score) matches from `std.Bulild`. 

As another example "fs windows" will prioritize windows-related results in `std.fs`, while "windows fs" will prioritize fs-related results in `std.windows`.
This commit is contained in:
Loris Cro 2023-04-26 18:17:20 +02:00 committed by GitHub
parent b55b8e7745
commit b294bff1a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 10936 additions and 113 deletions

10255
lib/docs/commonmark.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
--search-bg-color: #f3f3f3; --search-bg-color: #f3f3f3;
--search-bg-color-focus: #ffffff; --search-bg-color-focus: #ffffff;
--search-sh-color: rgba(0, 0, 0, 0.18); --search-sh-color: rgba(0, 0, 0, 0.18);
--search-other-results-color: rgb(100, 100, 100);
--help-sh-color: rgba(0, 0, 0, 0.75); --help-sh-color: rgba(0, 0, 0, 0.75);
--help-bg-color: #aaa; --help-bg-color: #aaa;
} }
@ -83,6 +84,7 @@
.flex-right { .flex-right {
display: flex; display: flex;
flex-direction: column;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
flex-grow: 1; flex-grow: 1;
@ -200,7 +202,7 @@
} }
#guides { #guides {
padding: 1rem 0.7rem 2.4rem 1.4rem; padding: 0rem 0.7rem 2.4rem 1.4rem;
box-sizing: border-box; box-sizing: border-box;
font-size: 1rem; font-size: 1rem;
background-color: var(--bg-color); background-color: var(--bg-color);
@ -209,7 +211,7 @@
/* docs section */ /* docs section */
.docs { .docs {
padding: 1rem 0.7rem 2.4rem 1.4rem; padding: 0rem 0.7rem 2.4rem 1.4rem;
font-size: 1rem; font-size: 1rem;
background-color: var(--bg-color); background-color: var(--bg-color);
overflow-wrap: break-word; overflow-wrap: break-word;
@ -248,6 +250,37 @@
left: 5px; left: 5px;
} }
.other-results {
line-height: 1em;
position: relative;
outline: 0;
border: 0;
color: var(--search-other-results-color);
text-align: center;
height: 1.5em;
opacity: .5;
}
.other-results:before {
content: '';
background: var(--search-other-results-color);
position: absolute;
left: 0;
top: 50%;
width: 100%;
height: 1px;
}
.other-results:after {
content: "other results";
position: relative;
display: inline-block;
padding: 0 .5em;
line-height: 1.5em;
color: var(--search-other-results-color);
background-color: var(--bg-color);
}
.docs a { .docs a {
color: var(--link-color); color: var(--link-color);
} }
@ -505,6 +538,7 @@
--search-bg-color: #3c3c3c; --search-bg-color: #3c3c3c;
--search-bg-color-focus: #000; --search-bg-color-focus: #000;
--search-sh-color: rgba(255, 255, 255, 0.28); --search-sh-color: rgba(255, 255, 255, 0.28);
--search-other-results-color: rgba(255, 255, 255, 0.28);
--help-sh-color: rgba(142, 142, 142, 0.5); --help-sh-color: rgba(142, 142, 142, 0.5);
--help-bg-color: #333; --help-bg-color: #333;
} }
@ -717,22 +751,38 @@
<h2><span>Zig Version</span></h2> <h2><span>Zig Version</span></h2>
<p class="str" id="tdZigVer"></p> <p class="str" id="tdZigVer"></p>
</div> </div>
<div>
<input id="privDeclsBox" type="checkbox"/>
<label for="privDeclsBox">Internal Doc Mode</label>
</div>
</div> </div>
</nav> </nav>
</div> </div>
<div class="flex-right"> <div class="flex-right">
<div class="wrap">
<section class="docs" style="padding-top: 1.5rem; padding-bottom:0;">
<div style="position: relative">
<span id="searchPlaceholder"><kbd>s</kbd> to search, <kbd>?</kbd> for more options</span>
<input type="search" class="search" id="search" autocomplete="off" spellcheck="false" disabled>
</div>
</section>
</div>
<div id="sectSearchResults" class="docs hidden">
<h2>Search Results</h2>
<ul id="listSearchResults"></ul>
<p id="sectSearchAllResultsLink" class="hidden"><a href="">show all results</a></p>
</div>
<div id="sectSearchNoResults" class="docs hidden">
<h2>No Results Found</h2>
<p>Here are some things you can try:</p>
<ul>
<li>Check out the <a id="langRefLink">Language Reference</a> for the language itself.</li>
<li>Check out the <a href="https://ziglang.org/learn/">Learn page</a> for other helpful resources for learning Zig.</li>
<li>Use your search engine.</li>
</ul>
<p>Press <kbd>?</kbd> to see keyboard shortcuts and <kbd>Esc</kbd> to return.</p>
</div>
<div id="guides" class="wrap hidden"> <div id="guides" class="wrap hidden">
<div id="activeGuide" class="hidden"></div>
</div> </div>
<div id="docs" class="wrap hidden"> <div id="docs" class="wrap hidden">
<section class="docs"> <section class="docs">
<div style="position: relative">
<span id="searchPlaceholder"><kbd>s</kbd> to search, <kbd>?</kbd> for more options</span>
<input type="search" class="search" id="search" autocomplete="off" spellcheck="false" disabled>
</div>
<p id="status">Loading...</p> <p id="status">Loading...</p>
<div id="sectNav" class="hidden"><ul id="listNav"></ul></div> <div id="sectNav" class="hidden"><ul id="listNav"></ul></div>
<div id="fnProto" class="hidden"> <div id="fnProto" class="hidden">
@ -762,21 +812,6 @@
</div> </div>
<div id="tableFnErrors"><dl id="listFnErrors"></dl></div> <div id="tableFnErrors"><dl id="listFnErrors"></dl></div>
</div> </div>
<div id="sectSearchResults" class="hidden">
<h2>Search Results</h2>
<ul id="listSearchResults"></ul>
<p id="sectSearchAllResultsLink" class="hidden"><a href="">show all results</a></p>
</div>
<div id="sectSearchNoResults" class="hidden">
<h2>No Results Found</h2>
<p>Here are some things you can try:</p>
<ul>
<li>Check out the <a id="langRefLink">Language Reference</a> for the language itself.</li>
<li>Check out the <a href="https://ziglang.org/learn/">Learn page</a> for other helpful resources for learning Zig.</li>
<li>Use your search engine.</li>
</ul>
<p>Press <kbd>?</kbd> to see keyboard shortcuts and <kbd>Esc</kbd> to return.</p>
</div>
<div id="sectFields" class="hidden"> <div id="sectFields" class="hidden">
<h2>Fields</h2> <h2>Fields</h2>
<div id="listFields"></div> <div id="listFields"></div>
@ -852,6 +887,7 @@
</div> </div>
</div> </div>
<script src="data.js"></script> <script src="data.js"></script>
<script src="commonmark.js"></script>
<script src="main.js"></script> <script src="main.js"></script>
</body> </body>
</html> </html>

View File

@ -4,7 +4,6 @@ var zigAnalysis;
const NAV_MODES = { const NAV_MODES = {
API: "#A;", API: "#A;",
API_INTERNAL: "#a;",
GUIDES: "#G;", GUIDES: "#G;",
}; };
@ -56,12 +55,13 @@ const NAV_MODES = {
const domSectSearchResults = document.getElementById("sectSearchResults"); const domSectSearchResults = document.getElementById("sectSearchResults");
const domSectSearchAllResultsLink = document.getElementById("sectSearchAllResultsLink"); const domSectSearchAllResultsLink = document.getElementById("sectSearchAllResultsLink");
const domDocs = document.getElementById("docs"); const domDocs = document.getElementById("docs");
const domGuides = document.getElementById("guides"); const domGuidesSection = document.getElementById("guides");
const domActiveGuide = document.getElementById("activeGuide");
const domListSearchResults = document.getElementById("listSearchResults"); const domListSearchResults = document.getElementById("listSearchResults");
const domSectSearchNoResults = document.getElementById("sectSearchNoResults"); const domSectSearchNoResults = document.getElementById("sectSearchNoResults");
const domSectInfo = document.getElementById("sectInfo"); const domSectInfo = document.getElementById("sectInfo");
// const domTdTarget = (document.getElementById("tdTarget")); // const domTdTarget = (document.getElementById("tdTarget"));
const domPrivDeclsBox = document.getElementById("privDeclsBox");
const domTdZigVer = document.getElementById("tdZigVer"); const domTdZigVer = document.getElementById("tdZigVer");
const domHdrName = document.getElementById("hdrName"); const domHdrName = document.getElementById("hdrName");
const domHelpModal = document.getElementById("helpModal"); const domHelpModal = document.getElementById("helpModal");
@ -83,15 +83,16 @@ const NAV_MODES = {
let typeTypeId = findTypeTypeId(); let typeTypeId = findTypeTypeId();
let pointerSizeEnum = { One: 0, Many: 1, Slice: 2, C: 3 }; let pointerSizeEnum = { One: 0, Many: 1, Slice: 2, C: 3 };
let declSearchIndex = new RadixTree();
window.search = declSearchIndex;
// for each module, is an array with modules to get to this one // for each module, is an array with modules to get to this one
let canonModPaths = computeCanonicalModulePaths(); let canonModPaths = computeCanonicalModulePaths();
// for each decl, is an array with {declNames, modNames} to get to this one // for each decl, is an array with {declNames, modNames} to get to this one
let canonDeclPaths = null; // lazy; use getCanonDeclPath let canonDeclPaths = null; // lazy; use getCanonDeclPath
// for each type, is an array with {declNames, modNames} to get to this one // for each type, is an array with {declNames, modNames} to get to this one
let canonTypeDecls = null; // lazy; use getCanonTypeDecl let canonTypeDecls = null; // lazy; use getCanonTypeDecl
let curNav = { let curNav = {
@ -122,6 +123,12 @@ const NAV_MODES = {
// map of decl index to list of comptime fn calls // map of decl index to list of comptime fn calls
// let nodesToCallsMap = indexNodesToCalls(); // let nodesToCallsMap = indexNodesToCalls();
let guidesSearchIndex = {};
window.guideSearch = guidesSearchIndex;
parseGuides();
domSearch.disabled = false; domSearch.disabled = false;
domSearch.addEventListener("keydown", onSearchKeyDown, false); domSearch.addEventListener("keydown", onSearchKeyDown, false);
domSearch.addEventListener("focus", ev => { domSearch.addEventListener("focus", ev => {
@ -139,31 +146,6 @@ const NAV_MODES = {
onHashChange(); onHashChange();
} }
domPrivDeclsBox.addEventListener(
"change",
function () {
if (this.checked != curNav.showPrivDecls) {
if (
this.checked &&
location.hash.length > 1 &&
location.hash[1] != "*"
) {
location.hash = "#*" + location.hash.substring(1);
return;
}
if (
!this.checked &&
location.hash.length > 1 &&
location.hash[1] == "*"
) {
location.hash = "#" + location.hash.substring(2);
return;
}
}
},
false
);
if (location.hash == "") { if (location.hash == "") {
location.hash = "#A;"; location.hash = "#A;";
} }
@ -189,7 +171,6 @@ const NAV_MODES = {
let suffix = " - Zig"; let suffix = " - Zig";
switch (curNav.mode) { switch (curNav.mode) {
case NAV_MODES.API: case NAV_MODES.API:
case NAV_MODES.API_INTERNAL:
let list = curNav.modNames.concat(curNav.declNames); let list = curNav.modNames.concat(curNav.declNames);
if (list.length === 0) { if (list.length === 0) {
document.title = zigAnalysis.modules[zigAnalysis.rootMod].name + suffix; document.title = zigAnalysis.modules[zigAnalysis.rootMod].name + suffix;
@ -284,8 +265,11 @@ const NAV_MODES = {
function resolveValue(value) { function resolveValue(value) {
let i = 0; let i = 0;
while (i < 1000) { while (true) {
i += 1; i += 1;
if (i >= 10000) {
throw "resolveValue quota exceeded"
}
if ("refPath" in value.expr) { if ("refPath" in value.expr) {
value = { expr: value.expr.refPath[value.expr.refPath.length - 1] }; value = { expr: value.expr.refPath[value.expr.refPath.length - 1] };
@ -307,8 +291,31 @@ const NAV_MODES = {
return value; return value;
} }
console.assert(false); }
return {};
function resolveGenericRet(genericFunc) {
if (genericFunc.generic_ret == null) return null;
let result = resolveValue({expr: genericFunc.generic_ret});
let i = 0;
while (true) {
i += 1;
if (i >= 10000) {
throw "resolveGenericRet quota exceeded"
}
if ("call" in result.expr) {
let call = zigAnalysis.calls[result.expr.call];
let resolvedFunc = resolveValue({ expr: call.func });
if (!("type" in resolvedFunc.expr)) return null;
let callee = getType(resolvedFunc.expr.type);
if (!callee.generic_ret) return null;
result = resolveValue({ expr: callee.generic_ret });
continue;
}
return result;
}
} }
// function typeOfDecl(decl){ // function typeOfDecl(decl){
@ -401,8 +408,12 @@ const NAV_MODES = {
domGuideSwitch.classList.add("active"); domGuideSwitch.classList.add("active");
domApiSwitch.classList.remove("active"); domApiSwitch.classList.remove("active");
domDocs.classList.add("hidden"); domDocs.classList.add("hidden");
domGuides.classList.remove("hidden"); domGuidesSection.classList.remove("hidden");
domActiveGuide.classList.add("hidden");
domApiMenu.classList.add("hidden"); domApiMenu.classList.add("hidden");
domSectSearchResults.classList.add("hidden");
domSectSearchAllResultsLink.classList.add("hidden");
domSectSearchNoResults.classList.add("hidden");
// sidebar guides list // sidebar guides list
const section_list = zigAnalysis.guide_sections; const section_list = zigAnalysis.guide_sections;
@ -417,7 +428,7 @@ const NAV_MODES = {
const guide = section.guides[i]; const guide = section.guides[i];
let liDom = domGuides.children[i]; let liDom = domGuides.children[i];
let aDom = liDom.children[0]; let aDom = liDom.children[0];
aDom.textContent = guide.name; aDom.textContent = guide.title;
aDom.setAttribute("href", NAV_MODES.GUIDES + guide.name); aDom.setAttribute("href", NAV_MODES.GUIDES + guide.name);
if (guide.name === curNav.activeGuide) { if (guide.name === curNav.activeGuide) {
aDom.classList.add("active"); aDom.classList.add("active");
@ -431,6 +442,11 @@ const NAV_MODES = {
domGuidesMenu.classList.remove("hidden"); domGuidesMenu.classList.remove("hidden");
} }
if (curNavSearch !== "") {
return renderSearchGuides();
}
// main content // main content
let activeGuide = undefined; let activeGuide = undefined;
outer: for (let i = 0; i < zigAnalysis.guide_sections.length; i += 1) { outer: for (let i = 0; i < zigAnalysis.guide_sections.length; i += 1) {
@ -447,7 +463,7 @@ const NAV_MODES = {
if (activeGuide == undefined) { if (activeGuide == undefined) {
const root_file_idx = zigAnalysis.modules[zigAnalysis.rootMod].file; const root_file_idx = zigAnalysis.modules[zigAnalysis.rootMod].file;
const root_file_name = getFile(root_file_idx).name; const root_file_name = getFile(root_file_idx).name;
domGuides.innerHTML = markdown(` domActiveGuide.innerHTML = markdown(`
# Zig Guides # Zig Guides
These autodocs don't contain any guide. These autodocs don't contain any guide.
@ -461,29 +477,38 @@ const NAV_MODES = {
You can add guides by specifying which markdown files to include You can add guides by specifying which markdown files to include
in the top level doc comment of your root file, like so: in the top level doc comment of your root file, like so:
(At the top of \`${root_file_name}\`) (At the top of *${root_file_name}*)
\`\`\` \`\`\`
//!zig-autodoc-guide: intro.md //!zig-autodoc-guide: intro.md
//!zig-autodoc-guide: quickstart.md //!zig-autodoc-guide: quickstart.md
//!zig-autodoc-section: Advanced topics //!zig-autodoc-guide: advanced-docs/advanced-stuff.md
//!zig-autodoc-guide: ../advanced-docs/advanced-stuff.md
\`\`\` \`\`\`
You can also create sections to group guides together:
\`\`\`
//!zig-autodoc-section: CLI Usage
//!zig-autodoc-guide: cli-basics.md
//!zig-autodoc-guide: cli-advanced.md
\`\`\`
**Note that this feature is still under heavy development so expect bugs** **Note that this feature is still under heavy development so expect bugs**
**and missing features!** **and missing features!**
Happy writing! Happy writing!
`); `);
} else { } else {
domGuides.innerHTML = markdown(activeGuide.body); domActiveGuide.innerHTML = markdown(activeGuide.body);
} }
domActiveGuide.classList.remove("hidden");
} }
function renderApi() { function renderApi() {
// set Api mode // set Api mode
domApiSwitch.classList.add("active"); domApiSwitch.classList.add("active");
domGuideSwitch.classList.remove("active"); domGuideSwitch.classList.remove("active");
domGuides.classList.add("hidden"); domGuidesSection.classList.add("hidden");
domDocs.classList.remove("hidden"); domDocs.classList.remove("hidden");
domApiMenu.classList.remove("hidden"); domApiMenu.classList.remove("hidden");
domGuidesMenu.classList.add("hidden"); domGuidesMenu.classList.add("hidden");
@ -520,10 +545,8 @@ const NAV_MODES = {
renderInfo(); renderInfo();
renderModList(); renderModList();
domPrivDeclsBox.checked = curNav.mode == NAV_MODES.API_INTERNAL;
if (curNavSearch !== "") { if (curNavSearch !== "") {
return renderSearch(); return renderSearchAPI();
} }
let rootMod = zigAnalysis.modules[zigAnalysis.rootMod]; let rootMod = zigAnalysis.modules[zigAnalysis.rootMod];
@ -542,6 +565,7 @@ const NAV_MODES = {
curNav.declObjs = [currentType]; curNav.declObjs = [currentType];
for (let i = 0; i < curNav.declNames.length; i += 1) { for (let i = 0; i < curNav.declNames.length; i += 1) {
let childDecl = findSubDecl(currentType, curNav.declNames[i]); let childDecl = findSubDecl(currentType, curNav.declNames[i]);
window.last_decl = childDecl;
if (childDecl == null) { if (childDecl == null) {
return render404(); return render404();
} }
@ -605,7 +629,6 @@ const NAV_MODES = {
function render() { function render() {
switch (curNav.mode) { switch (curNav.mode) {
case NAV_MODES.API: case NAV_MODES.API:
case NAV_MODES.API_INTERNAL:
return renderApi(); return renderApi();
case NAV_MODES.GUIDES: case NAV_MODES.GUIDES:
return renderGuides(); return renderGuides();
@ -3123,22 +3146,21 @@ const NAV_MODES = {
const mode = location.hash.substring(0, 3); const mode = location.hash.substring(0, 3);
let query = location.hash.substring(3); let query = location.hash.substring(3);
let qpos = query.indexOf("?");
let nonSearchPart;
if (qpos === -1) {
nonSearchPart = query;
} else {
nonSearchPart = query.substring(0, qpos);
curNavSearch = decodeURIComponent(query.substring(qpos + 1));
}
const DEFAULT_HASH = NAV_MODES.API + zigAnalysis.modules[zigAnalysis.rootMod].name; const DEFAULT_HASH = NAV_MODES.API + zigAnalysis.modules[zigAnalysis.rootMod].name;
switch (mode) { switch (mode) {
case NAV_MODES.API: case NAV_MODES.API:
case NAV_MODES.API_INTERNAL: // #A;MODULE:decl.decl.decl?search-term
// #A;MODULE:decl.decl.decl?search-term
curNav.mode = mode; curNav.mode = mode;
let qpos = query.indexOf("?");
let nonSearchPart;
if (qpos === -1) {
nonSearchPart = query;
} else {
nonSearchPart = query.substring(0, qpos);
curNavSearch = decodeURIComponent(query.substring(qpos + 1));
}
let parts = nonSearchPart.split(":"); let parts = nonSearchPart.split(":");
if (parts[0] == "") { if (parts[0] == "") {
location.hash = DEFAULT_HASH; location.hash = DEFAULT_HASH;
@ -3152,14 +3174,18 @@ const NAV_MODES = {
return; return;
case NAV_MODES.GUIDES: case NAV_MODES.GUIDES:
const sections = zigAnalysis.guide_sections; const sections = zigAnalysis.guide_sections;
if (sections.length != 0 && sections[0].guides.length != 0 && query == "") { if (sections.length != 0 && sections[0].guides.length != 0 && nonSearchPart == "") {
location.hash = NAV_MODES.GUIDES + sections[0].guides[0].name; location.hash = NAV_MODES.GUIDES + sections[0].guides[0].name;
if (qpos != -1) {
location.hash += query.substring(qpos);
}
return; return;
} }
curNav.mode = mode; curNav.mode = mode;
curNav.activeGuide = query; curNav.activeGuide = nonSearchPart;
return; return;
default: default:
@ -3230,22 +3256,6 @@ const NAV_MODES = {
} }
} }
if (parentType.privDecls) {
for (let i = 0; i < parentType.privDecls.length; i += 1) {
let declIndex = parentType.privDecls[i];
let childDecl = getDecl(declIndex);
if (childDecl.name === childName) {
childDecl.find_subdecl_idx = declIndex;
return childDecl;
} else if (childDecl.is_uns) {
let declValue = resolveValue(childDecl.value);
if (!("type" in declValue.expr)) continue;
let uns_container = getType(declValue.expr.type);
let uns_res = findSubDecl(uns_container, childName);
if (uns_res !== null) return uns_res;
}
}
}
return null; return null;
} }
@ -3276,6 +3286,12 @@ const NAV_MODES = {
}); });
} }
} }
for (let i = 0; i < zigAnalysis.modules.length; i += 1) {
const p = zigAnalysis.modules[i];
// TODO
// declSearchIndex.add(p.name, {moduleId: i});
}
return list; return list;
} }
@ -3291,6 +3307,7 @@ const NAV_MODES = {
let stack = [ let stack = [
{ {
declNames: [], declNames: [],
declIndexes: [],
type: getType(mod.main), type: getType(mod.main),
}, },
]; ];
@ -3334,19 +3351,28 @@ const NAV_MODES = {
} }
} }
} }
window.cdp = list;
return list; return list;
} }
function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) { function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
let declVal = resolveValue(decl.value); let declVal = resolveValue(decl.value);
let declNames = item.declNames.concat([decl.name]); let declNames = item.declNames.concat([decl.name]);
let declIndexes = item.declIndexes.concat([declIndex]);
if (list[declIndex] != null) return; if (list[declIndex] != null) return;
list[declIndex] = { list[declIndex] = {
modNames: modNames, modNames: modNames,
declNames: declNames, declNames: declNames,
declIndexes: declIndexes,
}; };
// add to search index
{
declSearchIndex.add(decl.name, {declIndex});
}
if ("type" in declVal.expr) { if ("type" in declVal.expr) {
let value = getType(declVal.expr.type); let value = getType(declVal.expr.type);
if (declCanRepresentTypeKind(value.kind)) { if (declCanRepresentTypeKind(value.kind)) {
@ -3356,18 +3382,20 @@ function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
if (isContainerType(value)) { if (isContainerType(value)) {
stack.push({ stack.push({
declNames: declNames, declNames: declNames,
declIndexes: declIndexes,
type: value, type: value,
}); });
} }
// Generic function // Generic function
if (value.kind == typeKinds.Fn && value.generic_ret != null) { if (typeIsGenericFn(declVal.expr.type)) {
let resolvedVal = resolveValue({ expr: value.generic_ret }); let ret = resolveGenericRet(value);
if ("type" in resolvedVal.expr) { if (ret != null && "type" in ret.expr) {
let generic_type = getType(resolvedVal.expr.type); let generic_type = getType(ret.expr.type);
if (isContainerType(generic_type)) { if (isContainerType(generic_type)) {
stack.push({ stack.push({
declNames: declNames, declNames: declNames,
declIndexes: declIndexes,
type: generic_type, type: generic_type,
}); });
} }
@ -3419,6 +3447,53 @@ function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
return markdown(shortDesc(docs)); return markdown(shortDesc(docs));
} }
function parseGuides() {
for (let j = 0; j < zigAnalysis.guide_sections.length; j+=1){
const section = zigAnalysis.guide_sections[j];
for (let i = 0; i < section.guides.length; i+=1){
let reader = new commonmark.Parser({smart: true});
const guide = section.guides[i];
const ast = reader.parse(guide.body);
// Find the first text thing to use as a sidebar title
guide.title = "[empty guide]";
{
let walker = ast.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (node.type === 'text') {
guide.title = node.literal;
break;
}
}
}
// Index this guide
{
let walker = ast.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering == true && node.type === 'text') {
indexTextForGuide(j, i, node);
}
}
}
}
}
}
function indexTextForGuide(section_idx, guide_idx, node){
const terms = node.literal.split(" ");
for (let i = 0; i < terms.length; i += 1){
const t = terms[i];
if (!guidesSearchIndex[t]) guidesSearchIndex[t] = new Set();
node.guide = {section_idx, guide_idx};
guidesSearchIndex[t].add(node);
}
}
function markdown(input, contextType) { function markdown(input, contextType) {
const raw_lines = input.split("\n"); // zig allows no '\r', so we don't need to split on CR const raw_lines = input.split("\n"); // zig allows no '\r', so we don't need to split on CR
@ -3842,6 +3917,7 @@ function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
} }
} }
function onSearchKeyDown(ev) { function onSearchKeyDown(ev) {
switch (getKeyString(ev)) { switch (getKeyString(ev)) {
case "Enter": case "Enter":
@ -3982,16 +4058,207 @@ function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
clearAsyncSearch(); clearAsyncSearch();
let oldHash = location.hash; let oldHash = location.hash;
let parts = oldHash.split("?"); let parts = oldHash.split("?");
let newPart2 = domSearch.value === "" ? "" : "?" + domSearch.value; // TODO: make a tooltip that shows the user that we've replaced their dots
let box_text = domSearch.value.replaceAll(".", " ");
let newPart2 = box_text === "" ? "" : "?" + box_text;
location.replace(parts.length === 1 ? oldHash + newPart2 : parts[0] + newPart2); location.replace(parts.length === 1 ? oldHash + newPart2 : parts[0] + newPart2);
} }
function getSearchTerms() { function getSearchTerms() {
let list = curNavSearch.trim().split(/[ \r\n\t]+/); let list = curNavSearch.trim().split(/[ \r\n\t]+/);
list.sort();
return list; return list;
} }
function renderSearch() { function renderSearchGuides() {
const searchTrimmed = false;
let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
let terms = getSearchTerms();
let matchedItems = new Set();
for (let i = 0; i < terms.length; i += 1) {
const nodes = guidesSearchIndex[terms[i]];
if (nodes) {
for (const n of nodes) {
matchedItems.add(n);
}
}
}
if (matchedItems.size !== 0) {
// Build up the list of search results
let matchedItemsHTML = "";
for (const node of matchedItems) {
const text = node.literal;
const href = "";
matchedItemsHTML += "<li><a href=\"" + href + "\">" + text + "</a></li>";
}
// Replace the search results using our newly constructed HTML string
domListSearchResults.innerHTML = matchedItemsHTML;
if (searchTrimmed) {
domSectSearchAllResultsLink.classList.remove("hidden");
}
renderSearchCursor();
domSectSearchResults.classList.remove("hidden");
} else {
domSectSearchNoResults.classList.remove("hidden");
}
}
function renderSearchAPI(){
if (canonDeclPaths == null) {
canonDeclPaths = computeCanonDeclPaths();
}
let declSet = new Set();
let otherDeclSet = new Set(); // for low quality results
let declScores = {};
let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
let term_list = getSearchTerms();
for (let i = 0; i < term_list.length; i += 1) {
let term = term_list[i];
let result = declSearchIndex.search(term.toLowerCase());
if (result == null) {
domSectSearchNoResults.classList.remove("hidden");
domSectSearchResults.classList.add("hidden");
return;
}
let termSet = new Set();
let termOtherSet = new Set();
for (let list of [result.full, result.partial]) {
for (let r of list) {
const d = r.declIndex;
const decl = getDecl(d);
const canonPath = getCanonDeclPath(d);
// collect unconditionally for the first term
if (i == 0) {
declSet.add(d);
} else {
// path intersection for subsequent terms
let found = false;
for (let p of canonPath.declIndexes) {
if (declSet.has(p)) {
found = true;
break;
}
}
if (!found) {
otherDeclSet.add(d);
} else {
termSet.add(d);
}
}
if (declScores[d] == undefined) declScores[d] = 0;
// scores (lower is better)
let decl_name = decl.name;
if (ignoreCase) decl_name = decl_name.toLowerCase();
// shallow path are preferable
const path_depth = canonPath.declNames.length * 50;
// matching the start of a decl name is good
const match_from_start = decl_name.startsWith(term) ? -term.length * (1 -ignoreCase) : (decl_name.length - term.length) + 1;
// being a perfect match is good
const is_full_match = (list == result.full) ? -decl_name.length * (2 - ignoreCase) : decl_name.length - term.length;
// matching the end of a decl name is good
const matches_the_end = decl_name.endsWith(term) ? -term.length * (1 - ignoreCase) : (decl_name.length - term.length) + 1;
// explicitly penalizing scream case decls
const decl_is_scream_case = decl.name.toUpperCase() != decl.name ? 0 : decl.name.length;
const score = path_depth
+ match_from_start
+ is_full_match
+ matches_the_end
+ decl_is_scream_case;
declScores[d] += score;
}
}
if (i != 0) {
for (let d of declSet) {
if (termSet.has(d)) continue;
let found = false;
for (let p of getCanonDeclPath(d).declIndexes) {
if (termSet.has(p) || otherDeclSet.has(p)) {
found = true;
break;
}
}
if (found) {
declScores[d] = declScores[d] / term_list.length;
}
termOtherSet.add(d);
}
declSet = termSet;
for (let d of termOtherSet) {
otherDeclSet.add(d);
}
}
}
let matchedItems = {
high_quality: [],
low_quality: [],
};
for (let idx of declSet) {
matchedItems.high_quality.push({points: declScores[idx], declIndex: idx})
}
for (let idx of otherDeclSet) {
matchedItems.low_quality.push({points: declScores[idx], declIndex: idx})
}
matchedItems.high_quality.sort(function (a, b) {
let cmp = operatorCompare(a.points, b.points);
return cmp;
});
matchedItems.low_quality.sort(function (a, b) {
let cmp = operatorCompare(a.points, b.points);
return cmp;
});
// Build up the list of search results
let matchedItemsHTML = "";
for (let list of [matchedItems.high_quality, matchedItems.low_quality]) {
if (list == matchedItems.low_quality && list.length > 0) {
matchedItemsHTML += "<hr class='other-results'>"
}
for (let result of list) {
const points = result.points;
const match = result.declIndex;
let canonPath = getCanonDeclPath(match);
if (canonPath == null) continue;
let lastModName = canonPath.modNames[canonPath.modNames.length - 1];
let text = lastModName + "." + canonPath.declNames.join(".");
const href = navLink(canonPath.modNames, canonPath.declNames);
matchedItemsHTML += "<li><a href=\"" + href + "\">" + text + "</a></li>";
}
}
// Replace the search results using our newly constructed HTML string
domListSearchResults.innerHTML = matchedItemsHTML;
renderSearchCursor();
domSectSearchResults.classList.remove("hidden");
}
function renderSearchAPIOld() {
let matchedItems = []; let matchedItems = [];
let ignoreCase = curNavSearch.toLowerCase() === curNavSearch; let ignoreCase = curNavSearch.toLowerCase() === curNavSearch;
let terms = getSearchTerms(); let terms = getSearchTerms();
@ -4063,7 +4330,7 @@ function addDeclToSearchResults(decl, declIndex, modNames, item, list, stack) {
}); });
let searchTrimmed = false; let searchTrimmed = false;
const searchTrimResultsMaxItems = 200; const searchTrimResultsMaxItems = 60;
if (searchTrimResults && matchedItems.length > searchTrimResultsMaxItems) { if (searchTrimResults && matchedItems.length > searchTrimResultsMaxItems) {
matchedItems = matchedItems.slice(0, searchTrimResultsMaxItems); matchedItems = matchedItems.slice(0, searchTrimResultsMaxItems);
searchTrimmed = true; searchTrimmed = true;
@ -4314,3 +4581,255 @@ function toggleExpand(event) {
parent.parentElement.parentElement.scrollIntoView(true); parent.parentElement.parentElement.scrollIntoView(true);
} }
} }
function RadixTree() {
this.root = null;
RadixTree.prototype.search = function (query) {
return this.root.search(query);
}
RadixTree.prototype.add = function (declName, value) {
if (this.root == null) {
this.root = new Node(declName.toLowerCase(), null, [value]);
} else {
this.root.add(declName.toLowerCase(), value);
}
const not_scream_case = declName.toUpperCase() != declName;
let found_separator = false;
for (let i = 1; i < declName.length; i +=1) {
if (declName[i] == '_' || declName[i] == '.') {
found_separator = true;
continue;
}
if (found_separator || (declName[i].toLowerCase() !== declName[i])) {
if (declName.length > i+1
&& declName[i+1].toLowerCase() != declName[i+1]) continue;
let suffix = declName.slice(i);
this.root.add(suffix.toLowerCase(), value);
found_separator = false;
}
}
}
function Node(labels, next, values) {
this.labels = labels;
this.next = next;
this.values = values;
}
Node.prototype.isCompressed = function () {
return !Array.isArray(this.next);
}
Node.prototype.search = function (word) {
let full_matches = [];
let partial_matches = [];
let subtree_root = null;
let cn = this;
char_loop: for (let i = 0; i < word.length;) {
if (cn.isCompressed()) {
for (let j = 0; j < cn.labels.length; j += 1) {
let current_idx = i+j;
if (current_idx == word.length) {
partial_matches = cn.values;
subtree_root = cn.next;
break char_loop;
}
if (word[current_idx] != cn.labels[j]) return null;
}
// the full label matched
let new_idx = i + cn.labels.length;
if (new_idx == word.length) {
full_matches = cn.values;
subtree_root = cn.next;
break char_loop;
}
i = new_idx;
cn = cn.next;
continue;
} else {
for (let j = 0; j < cn.labels.length; j += 1) {
if (word[i] == cn.labels[j]) {
if (i == word.length - 1) {
full_matches = cn.values[j];
subtree_root = cn.next[j];
break char_loop;
}
let next = cn.next[j];
if (next == null) return null;
cn = next;
i += 1;
continue char_loop;
}
}
// didn't find a match
return null;
}
}
// Match was found, let's collect all other
// partial matches from the subtree
let stack = [subtree_root];
let node;
while (node = stack.pop()) {
if (node.isCompressed()) {
partial_matches = partial_matches.concat(node.values);
if (node.next != null) {
stack.push(node.next);
}
} else {
for (let v of node.values) {
partial_matches = partial_matches.concat(v);
}
for (let n of node.next) {
if (n != null) stack.push(n);
}
}
}
return {full: full_matches, partial: partial_matches};
}
Node.prototype.add = function (word, value) {
let cn = this;
char_loop: for (let i = 0; i < word.length;) {
if (cn.isCompressed()) {
for(let j = 0; j < cn.labels.length; j += 1) {
let current_idx = i+j;
if (current_idx == word.length) {
if (j < cn.labels.length - 1) {
let node = new Node(cn.labels.slice(j), cn.next, cn.values);
cn.labels = cn.labels.slice(0, j);
cn.next = node;
cn.values = [];
}
cn.values.push(value);
return;
}
if (word[current_idx] == cn.labels[j]) continue;
// if we're here, a mismatch was found
if (j != cn.labels.length - 1) {
// create a suffix node
const label_suffix = cn.labels.slice(j+1);
let node = new Node(label_suffix, cn.next, [...cn.values]);
cn.next = node;
cn.values = [];
}
// turn current node into a split node
let node = null;
let word_values = [];
if (current_idx == word.length - 1) {
// mismatch happened in the last character of word
// meaning that the current node should hold its value
word_values.push(value);
} else {
node = new Node(word.slice(current_idx+1), null, [value]);
}
cn.labels = cn.labels[j] + word[current_idx];
cn.next = [cn.next, node];
cn.values = [cn.values, word_values];
if (j != 0) {
// current node must be turned into a prefix node
let splitNode = new Node(cn.labels, cn.next, cn.values);
cn.labels = word.slice(i, current_idx);
cn.next = splitNode;
cn.values = [];
}
return;
}
// label matched fully with word, are there any more chars?
const new_idx = i + cn.labels.length;
if (new_idx == word.length) {
cn.values.push(value);
return;
} else {
if (cn.next == null) {
let node = new Node(word.slice(new_idx), null, [value]);
cn.next = node;
return;
} else {
cn = cn.next;
i = new_idx;
continue;
}
}
} else { // node is not compressed
let letter = word[i];
for (let j = 0; j < cn.labels.length; j += 1) {
if (letter == cn.labels[j]) {
if (i == word.length - 1) {
cn.values[j].push(value);
return;
}
if (cn.next[j] == null) {
let node = new Node(word.slice(i+1), null, [value]);
cn.next[j] = node;
return;
} else {
cn = cn.next[j];
i += 1;
continue char_loop;
}
}
}
// if we're here we didn't find a match
cn.labels += letter;
if (i == word.length - 1) {
cn.next.push(null);
cn.values.push([value]);
} else {
let node = new Node(word.slice(i+1), null, [value]);
cn.next.push(node);
cn.values.push([]);
}
return;
}
}
}
}
// RADIX TREE:
// apple
// appliance
// "appl" => [
// 'e', => $
// 'i' => "ance" => $
// ]
// OUR STUFF:
// AutoHashMap
// AutoArrayHashMap
// "Auto" => [
// 'A', => "rrayHashMap" => $
// 'H' => "ashMap" => $
// ]
// BUT!
// We want to be able to search "Hash", for example!

View File

@ -234,12 +234,13 @@ pub fn generateZirData(self: *Autodoc) !void {
}; };
const tldoc_comment = try self.getTLDocComment(file); const tldoc_comment = try self.getTLDocComment(file);
const cleaned_tldoc_comment = try self.findGuidePaths(file, tldoc_comment);
defer self.arena.free(cleaned_tldoc_comment);
try self.ast_nodes.append(self.arena, .{ try self.ast_nodes.append(self.arena, .{
.name = "(root)", .name = "(root)",
.docs = tldoc_comment, .docs = cleaned_tldoc_comment,
}); });
try self.files.put(self.arena, file, main_type_index); try self.files.put(self.arena, file, main_type_index);
try self.findGuidePaths(file, tldoc_comment);
_ = try self.walkInstruction(file, &root_scope, .{}, Zir.main_struct_inst, false); _ = try self.walkInstruction(file, &root_scope, .{}, Zir.main_struct_inst, false);
@ -349,6 +350,7 @@ pub fn generateZirData(self: *Autodoc) !void {
var docs_dir = try self.comp_module.comp.zig_lib_directory.handle.openDir("docs", .{}); var docs_dir = try self.comp_module.comp.zig_lib_directory.handle.openDir("docs", .{});
defer docs_dir.close(); defer docs_dir.close();
try docs_dir.copyFile("main.js", output_dir, "main.js", .{}); try docs_dir.copyFile("main.js", output_dir, "main.js", .{});
try docs_dir.copyFile("commonmark.js", output_dir, "commonmark.js", .{});
try docs_dir.copyFile("index.html", output_dir, "index.html", .{}); try docs_dir.copyFile("index.html", output_dir, "index.html", .{});
} }
@ -4754,14 +4756,20 @@ fn getTLDocComment(self: *Autodoc, file: *File) ![]const u8 {
return comment.items; return comment.items;
} }
fn findGuidePaths(self: *Autodoc, file: *File, str: []const u8) !void { /// Returns the doc comment cleared of autodoc directives.
fn findGuidePaths(self: *Autodoc, file: *File, str: []const u8) ![]const u8 {
const guide_prefix = "zig-autodoc-guide:"; const guide_prefix = "zig-autodoc-guide:";
const section_prefix = "zig-autodoc-section:"; const section_prefix = "zig-autodoc-section:";
try self.guide_sections.append(self.arena, .{}); // add a default section try self.guide_sections.append(self.arena, .{}); // add a default section
var current_section = &self.guide_sections.items[self.guide_sections.items.len - 1]; var current_section = &self.guide_sections.items[self.guide_sections.items.len - 1];
var it = std.mem.tokenize(u8, str, "\n"); var clean_docs: std.ArrayListUnmanaged(u8) = .{};
errdefer clean_docs.deinit(self.arena);
// TODO: this algo is kinda inefficient
var it = std.mem.split(u8, str, "\n");
while (it.next()) |line| { while (it.next()) |line| {
const trimmed_line = std.mem.trim(u8, line, " "); const trimmed_line = std.mem.trim(u8, line, " ");
if (std.mem.startsWith(u8, trimmed_line, guide_prefix)) { if (std.mem.startsWith(u8, trimmed_line, guide_prefix)) {
@ -4775,8 +4783,13 @@ fn findGuidePaths(self: *Autodoc, file: *File, str: []const u8) !void {
.name = trimmed_section_name, .name = trimmed_section_name,
}); });
current_section = &self.guide_sections.items[self.guide_sections.items.len - 1]; current_section = &self.guide_sections.items[self.guide_sections.items.len - 1];
} else {
try clean_docs.appendSlice(self.arena, line);
try clean_docs.append(self.arena, '\n');
} }
} }
return clean_docs.toOwnedSlice(self.arena);
} }
fn addGuide(self: *Autodoc, file: *File, guide_path: []const u8, section: *Section) !void { fn addGuide(self: *Autodoc, file: *File, guide_path: []const u8, section: *Section) !void {