This commit is contained in:
achan1989 2025-11-23 22:57:18 +00:00 committed by GitHub
commit caf5af99be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 248 additions and 49 deletions

View file

@ -56,6 +56,9 @@
const domErrors = document.getElementById("errors");
const domErrorsText = document.getElementById("errorsText");
// Chosen to prevent collisions with the IDs above.
const navPrefix = "nav_";
var searchTimer = null;
const curNav = {
@ -67,6 +70,8 @@
decl: null,
// string file name matching tarball path
path: null,
// string decl path within source file
scrollToDeclPath: null,
// when this is populated, pressing the "view source" command will
// navigate to this hash.
@ -74,7 +79,9 @@
};
var curNavSearch = "";
var curSearchIndex = -1;
var imFeelingLucky = false;
// When true: load the search result indicated by `curSearchIndex`, or the first result if
// `curSearchIndex` isn't valid.
var loadSearchResult = false;
// names of modules in the same order as wasm
const moduleList = [];
@ -185,7 +192,7 @@
} else {
return renderDecl(curNav.decl);
}
case 2: return renderSource(curNav.path);
case 2: return renderSource(curNav.path, curNav.scrollToDeclPath);
default: throw new Error("invalid navigation state");
}
}
@ -229,22 +236,43 @@
}
}
function renderSource(path) {
const decl_index = findFileRoot(path);
if (decl_index == null) return renderNotFound();
function renderSource(path, scroll_to_decl_path) {
const root_index = findFileRoot(path);
if (root_index == null) return renderNotFound();
renderNavFancy(decl_index, [{
renderNavFancy(root_index, [{
name: "[src]",
href: location.hash,
}]);
domSourceText.innerHTML = declSourceHtml(decl_index);
domSourceText.innerHTML = declSourceHtml(root_index, true);
domSectSource.classList.remove("hidden");
const scroll_to_decl_index = findDeclPathInNamespace(root_index, scroll_to_decl_path);
if (scroll_to_decl_index !== null) {
const to_elem = document.getElementById(navPrefix + scroll_to_decl_index);
if (to_elem != null) {
// Needs a delay, else the DOM hasn't been fully updated and the scroll does nothing.
setTimeout(function() {to_elem.scrollIntoView();}, 0);
}
}
}
function renderDeclHeading(decl_index) {
curNav.viewSourceHash = "#src/" + unwrapString(wasm_exports.decl_file_path(decl_index));
const is_root = wasm_exports.decl_is_root(decl_index);
if (!is_root) {
// E.g. if `decl_index` corresponds to `root.foo.bar` we want `foo.bar`
var subcomponents = [];
let decl_it = decl_index;
while (decl_it != null) {
subcomponents.push(declIndexName(decl_it));
decl_it = declParent(decl_it);
}
subcomponents.pop();
subcomponents.reverse();
curNav.viewSourceHash += ":" + subcomponents.join(".");
}
const hdrNameSpan = domHdrName.children[0];
const srcLink = domHdrName.children[1];
@ -389,7 +417,7 @@
if (members.length !== 0 || fields.length !== 0) {
renderNamespace(decl_index, members, fields);
} else {
domSourceText.innerHTML = declSourceHtml(decl_index);
domSourceText.innerHTML = declSourceHtml(decl_index, false);
domSectSource.classList.remove("hidden");
}
}
@ -419,7 +447,7 @@
renderErrorSet(base_decl, errorSetNodeList(decl_index, errorSetNode));
}
domSourceText.innerHTML = declSourceHtml(decl_index);
domSourceText.innerHTML = declSourceHtml(decl_index, false);
domSectSource.classList.remove("hidden");
}
@ -433,7 +461,7 @@
domTldDocs.classList.remove("hidden");
}
domSourceText.innerHTML = declSourceHtml(decl_index);
domSourceText.innerHTML = declSourceHtml(decl_index, false);
domSectSource.classList.remove("hidden");
}
@ -620,6 +648,7 @@
curNav.tag = 0;
curNav.decl = null;
curNav.path = null;
curNav.scrollToDeclPath = null;
curNav.viewSourceHash = null;
curNavSearch = "";
@ -638,7 +667,13 @@
const source_mode = nonSearchPart.startsWith("src/");
if (source_mode) {
curNav.tag = 2;
curNav.path = nonSearchPart.substring(4);
const idpos = nonSearchPart.indexOf(":");
if (idpos === -1) {
curNav.path = nonSearchPart.substring(4);
} else {
curNav.path = nonSearchPart.substring(4, idpos);
curNav.scrollToDeclPath = nonSearchPart.substring(idpos + 1);
}
} else {
curNav.tag = 1;
curNav.decl = findDecl(nonSearchPart);
@ -648,10 +683,15 @@
}
function onHashChange(state) {
if (state != null) {
const restore_search_index = state.curSearchIndex;
if (restore_search_index !== undefined) curSearchIndex = restore_search_index;
}
// Use a non-null state value to prevent the window scrolling if the user goes back to this history entry.
history.replaceState({}, "");
navigate(location.hash);
if (state == null) window.scrollTo({top: 0});
if (curNavSearch !== "") domSearch.focus({preventScroll: true});
}
function onPopState(ev) {
@ -662,8 +702,8 @@
function navigate(location_hash) {
updateCurNav(location_hash);
render();
if (imFeelingLucky) {
imFeelingLucky = false;
if (loadSearchResult) {
loadSearchResult = false;
activateSelectedResult();
}
}
@ -674,6 +714,19 @@
}
}
function doLoadSearchResult() {
clearAsyncSearch();
loadSearchResult = true;
const old_hash = location.hash;
location.hash = computeSearchHash();
if (location.hash === old_hash) {
// With certain sequences of history navigation and input, setting location.hash here
// causes no change, and the enter key isn't acted on until another modification is made
// to the search text. Force navigation to work around this.
navigate(location.hash);
}
}
function activateSelectedResult() {
if (domSectSearchResults.classList.contains("hidden")) {
return;
@ -685,21 +738,29 @@
}
if (liDom != null) {
var aDom = liDom.children[0];
history.replaceState({curSearchIndex: curSearchIndex}, "");
location.href = aDom.getAttribute("href");
curSearchIndex = -1;
}
domSearch.blur();
}
function onSearchResultClick(ev) {
const liDom = ev.target.parentElement;
const search_index = Array.from(domListSearchResults.children).indexOf(liDom);
curSearchIndex = search_index;
doLoadSearchResult();
ev.preventDefault();
ev.stopPropagation();
}
function onSearchKeyDown(ev) {
switch (ev.code) {
case "Enter":
if (ev.shiftKey || ev.ctrlKey || ev.altKey) return;
clearAsyncSearch();
imFeelingLucky = true;
location.hash = computeSearchHash();
doLoadSearchResult();
ev.preventDefault();
ev.stopPropagation();
return;
@ -846,6 +907,7 @@
const full_name = fullyQualifiedName(match);
aDom.textContent = full_name;
aDom.setAttribute('href', navLinkFqn(full_name));
aDom.addEventListener("click", onSearchResultClick);
}
renderSearchCursor();
@ -914,8 +976,8 @@
return unwrapString(wasm_exports.decl_name(decl_index));
}
function declSourceHtml(decl_index) {
return unwrapString(wasm_exports.decl_source_html(decl_index));
function declSourceHtml(decl_index, decl_nav_targets) {
return unwrapString(wasm_exports.decl_source_html(decl_index, decl_nav_targets));
}
function declDoctestHtml(decl_index) {
@ -983,6 +1045,13 @@
return result;
}
function findDeclPathInNamespace(namespace_decl_index, path) {
setInputString(path);
const result = wasm_exports.find_decl_path_in_namespace(namespace_decl_index);
if (result === -1) return null;
return result;
}
function findFileRoot(path) {
setInputString(path);
const result = wasm_exports.find_file_root();
@ -998,7 +1067,7 @@
function fnErrorSet(decl_index) {
const result = wasm_exports.fn_error_set(decl_index);
if (result === 0) return null;
if (result === 0 || result === -1) return null;
return result;
}

View file

@ -168,7 +168,7 @@ pub fn get_type_fn_return_expr(decl: *const Decl) ?Ast.Node.Index {
for (statements) |stmt| {
if (ast.nodeTag(stmt) == .@"return") {
return ast.nodeData(stmt).node;
if (ast.nodeData(stmt).opt_node.unwrap()) |expr_node| return expr_node;
}
}
return null;

View file

@ -15,6 +15,7 @@ pub var decls: std.ArrayList(Decl) = .empty;
pub var modules: std.StringArrayHashMapUnmanaged(File.Index) = .empty;
file: File.Index,
suppress_new_decls: u32 = 0,
/// keep in sync with "CAT_" constants in main.js
pub const Category = union(enum(u8)) {
@ -53,6 +54,27 @@ pub const File = struct {
/// struct/union/enum/opaque decl node => its namespace scope
/// local var decl node => its local variable scope
scopes: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, *Scope) = .empty,
/// Last decl in the file (exclusive).
decl_end: Decl.Index = .none,
pub const DeclIter = struct {
idx: DeclIndexTagType,
end: DeclIndexTagType,
const DeclIndexTagType = @typeInfo(Decl.Index).@"enum".tag_type;
pub fn next(iter: *DeclIter) ?Decl.Index {
if (iter.idx >= iter.end) return null;
const decl_idx = iter.idx;
iter.idx += 1;
return @enumFromInt(decl_idx);
}
pub fn remaining(iter: DeclIter) usize {
if (iter.idx >= iter.end) return 0;
return iter.end - iter.idx;
}
};
pub fn lookup_token(file: *File, token: Ast.TokenIndex) Decl.Index {
const decl_node = file.ident_decls.get(token) orelse return .none;
@ -63,6 +85,7 @@ pub const File = struct {
_,
fn add_decl(i: Index, node: Ast.Node.Index, parent_decl: Decl.Index) Oom!Decl.Index {
assert(node == .root or parent_decl != .none);
try decls.append(gpa, .{
.ast_node = node,
.file = i,
@ -89,6 +112,18 @@ pub const File = struct {
return file_index.get().node_decls.values()[0];
}
/// Excludes the root decl.
/// Only valid after `Walk.add_file()`.
pub fn iterDecls(file_index: File.Index) DeclIter {
const root_idx = @intFromEnum(file_index.findRootDecl());
const end_idx = file_index.get().decl_end;
assert(end_idx != .none);
return DeclIter{
.idx = root_idx + 1,
.end = @intFromEnum(end_idx),
};
}
pub fn categorize_decl(file_index: File.Index, node: Ast.Node.Index) Category {
const ast = file_index.get_ast();
switch (ast.nodeTag(node)) {
@ -315,23 +350,9 @@ pub const File = struct {
if (std.mem.eql(u8, builtin_name, "@import")) {
const str_lit_token = ast.nodeMainToken(params[0]);
const str_bytes = ast.tokenSlice(str_lit_token);
const file_path = std.zig.string_literal.parseAlloc(gpa, str_bytes) catch @panic("OOM");
defer gpa.free(file_path);
if (modules.get(file_path)) |imported_file_index| {
return .{ .alias = File.Index.findRootDecl(imported_file_index) };
}
const base_path = file_index.path();
const resolved_path = std.fs.path.resolvePosix(gpa, &.{
base_path, "..", file_path,
}) catch @panic("OOM");
defer gpa.free(resolved_path);
log.debug("from '{s}' @import '{s}' resolved='{s}'", .{
base_path, file_path, resolved_path,
});
if (files.getIndex(resolved_path)) |imported_file_index| {
return .{ .alias = File.Index.findRootDecl(@enumFromInt(imported_file_index)) };
} else {
log.warn("import target '{s}' did not resolve to any file", .{resolved_path});
switch (file_index.resolve_import(str_bytes)) {
.none => {},
else => |decl_index| return .{ .alias = decl_index },
}
} else if (std.mem.eql(u8, builtin_name, "@This")) {
if (file_index.get().node_decls.get(node)) |decl_index| {
@ -344,6 +365,28 @@ pub const File = struct {
return .{ .global_const = node };
}
pub fn resolve_import(file_index: File.Index, str_bytes: []const u8) Decl.Index {
const file_path = std.zig.string_literal.parseAlloc(gpa, str_bytes) catch @panic("OOM");
defer gpa.free(file_path);
if (modules.get(file_path)) |imported_file_index| {
return File.Index.findRootDecl(imported_file_index);
}
const base_path = file_index.path();
const resolved_path = std.fs.path.resolvePosix(gpa, &.{
base_path, "..", file_path,
}) catch @panic("OOM");
defer gpa.free(resolved_path);
log.debug("from '{s}' @import '{s}' resolved='{s}'", .{
base_path, file_path, resolved_path,
});
if (files.getIndex(resolved_path)) |imported_file_index| {
return File.Index.findRootDecl(@enumFromInt(imported_file_index));
} else {
log.warn("import target '{s}' did not resolve to any file", .{resolved_path});
return .none;
}
}
fn categorize_switch(file_index: File.Index, node: Ast.Node.Index) Category {
const ast = file_index.get_ast();
const full = ast.fullSwitch(node).?;
@ -401,6 +444,7 @@ pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
try struct_decl(&w, scope, decl_index, .root, ast.containerDeclRoot());
const file = file_index.get();
file.decl_end = @enumFromInt(decls.items.len);
shrinkToFit(&file.ident_decls);
shrinkToFit(&file.token_parents);
shrinkToFit(&file.node_decls);
@ -480,6 +524,7 @@ pub const Scope = struct {
},
.namespace => {
const namespace: *Namespace = @alignCast(@fieldParentPtr("base", it));
assert(namespace.decl_index != .none);
return namespace.decl_index;
},
};
@ -556,7 +601,8 @@ fn struct_decl(
if (namespace.doctests.get(fn_name)) |doctest_node| {
try w.file.get().doctests.put(gpa, member, doctest_node);
}
const decl_index = try w.file.add_decl(member, parent_decl);
const decl_index =
if (w.suppress_new_decls > 0) Decl.Index.none else try w.file.add_decl(member, parent_decl);
const body = if (ast.nodeTag(member) == .fn_decl) ast.nodeData(member).node_and_node[1].toOptional() else .none;
try w.fn_decl(&namespace.base, decl_index, body, full);
},
@ -566,14 +612,22 @@ fn struct_decl(
.simple_var_decl,
.aligned_var_decl,
=> {
const decl_index = try w.file.add_decl(member, parent_decl);
const decl_index =
if (w.suppress_new_decls > 0) Decl.Index.none else try w.file.add_decl(member, parent_decl);
try w.global_var_decl(&namespace.base, decl_index, ast.fullVarDecl(member).?);
},
.@"comptime",
=> try w.expr(&namespace.base, parent_decl, ast.nodeData(member).node),
.test_decl => try w.expr(&namespace.base, parent_decl, ast.nodeData(member).opt_token_and_node[1]),
.test_decl => {
// We're not interested in autodoc search within test declarations. It clutters the
// search with irrelevant results; and the FQN of decls in the test block can
// shadow other decls in the file, so we often can't even navigate to the results.
w.suppress_new_decls += 1;
try w.expr(&namespace.base, parent_decl, ast.nodeData(member).opt_token_and_node[1]);
w.suppress_new_decls -= 1;
},
else => unreachable,
};
@ -973,7 +1027,7 @@ fn builtin_call(
const ast = w.file.get_ast();
const builtin_token = ast.nodeMainToken(node);
const builtin_name = ast.tokenSlice(builtin_token);
if (std.mem.eql(u8, builtin_name, "@This")) {
if (w.suppress_new_decls == 0 and std.mem.eql(u8, builtin_name, "@This")) {
try w.file.get().node_decls.put(gpa, node, scope.getNamespaceDecl());
}

View file

@ -13,10 +13,15 @@ const Oom = error{OutOfMemory};
/// Delete this to find out where URL escaping needs to be added.
pub const missing_feature_url_escape = true;
/// Prevents collisions with IDs in index.html
/// Keep in sync with the `navPrefix` constant in `main.js`.
pub const nav_prefix: []const u8 = "nav_";
pub const RenderSourceOptions = struct {
skip_doc_comments: bool = false,
skip_comments: bool = false,
collapse_whitespace: bool = false,
/// Render a specific function as a link to its documentation.
fn_link: Decl.Index = .none,
/// Assumed to be sorted ascending.
source_location_annotations: []const Annotation = &.{},
@ -155,6 +160,26 @@ pub fn fileSourceHtml(
.char_literal,
.multiline_string_literal_line,
=> {
if (ast.isTokenPrecededByTags(token_index, &.{ .builtin, .l_paren }) and
std.mem.eql(u8, "@import", ast.tokenSlice(token_index - 2)))
{
g.field_access_buffer.clearRetainingCapacity();
try resolveImportLink(
file_index,
&g.field_access_buffer,
ast.tokenSlice(token_index),
);
if (g.field_access_buffer.items.len > 0) {
try out.appendSlice(gpa, "<a class=\"tok-str\" href=\"#");
_ = missing_feature_url_escape;
try out.appendSlice(gpa, g.field_access_buffer.items);
try out.appendSlice(gpa, "\">");
try appendEscaped(out, slice);
try out.appendSlice(gpa, "</a>");
continue;
}
}
try out.appendSlice(gpa, "<span class=\"tok-str\">");
try appendEscaped(out, slice);
try out.appendSlice(gpa, "</span>");
@ -381,6 +406,17 @@ fn resolveIdentLink(
try resolveDeclLink(decl_index, out);
}
fn resolveImportLink(
file_index: Walk.File.Index,
out: *std.ArrayListUnmanaged(u8),
str_bytes: []const u8,
) Oom!void {
switch (file_index.resolve_import(str_bytes)) {
.none => {},
else => |decl_index| try decl_index.get().fqn(out),
}
}
fn unindent(s: []const u8, indent: usize) []const u8 {
var indent_idx: usize = 0;
for (s) |c| {

View file

@ -8,10 +8,11 @@ const Decl = Walk.Decl;
const ArrayList = std.ArrayList;
const Writer = std.Io.Writer;
const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
const appendEscaped = @import("html_render.zig").appendEscaped;
const resolveDeclLink = @import("html_render.zig").resolveDeclLink;
const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape;
const html_render = @import("html_render.zig");
const fileSourceHtml = html_render.fileSourceHtml;
const appendEscaped = html_render.appendEscaped;
const resolveDeclLink = html_render.resolveDeclLink;
const missing_feature_url_escape = html_render.missing_feature_url_escape;
const gpa = std.heap.wasm_allocator;
@ -543,11 +544,38 @@ export fn decl_fn_proto_html(decl_index: Decl.Index, linkify_fn_name: bool) Stri
return String.init(string_result.items);
}
export fn decl_source_html(decl_index: Decl.Index) String {
/// `decl_nav_targets`: create targets for jumping to decls. If true, asserts `decl_index` is the
/// root decl of a file.
export fn decl_source_html(decl_index: Decl.Index, decl_nav_targets: bool) String {
const decl = decl_index.get();
var sla: std.ArrayListUnmanaged(html_render.Annotation) = .empty;
defer sla.deinit(gpa);
if (decl_nav_targets) {
const root_file = decl_index.get().file;
assert(decl_index == root_file.findRootDecl());
const ast = root_file.get_ast();
var it = root_file.iterDecls();
sla.ensureTotalCapacityPrecise(gpa, it.remaining()) catch @panic("OOM");
while (true) {
const inner_decl_index = (it.next() orelse break);
const inner_decl = inner_decl_index.get();
if (!inner_decl.is_pub()) continue;
const decl_tok = ast.firstToken(inner_decl.ast_node);
const tok_start = ast.tokenStart(decl_tok);
sla.appendAssumeCapacity(.{
.file_byte_offset = tok_start,
.dom_id = @intFromEnum(inner_decl_index),
});
}
}
string_result.clearRetainingCapacity();
fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
fileSourceHtml(decl.file, &string_result, decl.ast_node, .{
.source_location_annotations = sla.items,
.annotation_prefix = html_render.nav_prefix,
}) catch |err| {
std.debug.panic("unable to render source: {s}", .{@errorName(err)});
};
return String.init(string_result.items);
@ -847,6 +875,11 @@ export fn find_file_root() Decl.Index {
return file.findRootDecl();
}
/// Does the decl correspond to the root struct of a file?
export fn decl_is_root(decl_index: Decl.Index) bool {
return decl_index.get().file.findRootDecl() == decl_index;
}
/// Uses `input_string`.
/// Tries to look up the Decl component-wise but then falls back to a file path
/// based scan.
@ -869,6 +902,13 @@ export fn find_decl() Decl.Index {
return .none;
}
/// Uses `input_string` as a decl path.
/// Start in the namespace corresponding to `decl_index`, find a child decl by path.
/// The path can contain multiple components e.g. `foo.bar`.
export fn find_decl_path_in_namespace(decl_index: Decl.Index) Decl.Index {
return resolve_decl_path(decl_index, input_string.items) orelse .none;
}
/// Set only by `categorize_decl`; read only by `get_aliasee`, valid only
/// when `categorize_decl` returns `.alias`.
var global_aliasee: Decl.Index = .none;