From 18eddf8d31e7ff12948127530cd98ce704ddf8a4 Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sun, 13 Apr 2025 17:26:47 +0100 Subject: [PATCH 1/7] Navigating to source from a decl will scroll to the decl When rendering source HTML for a file there's now the option to generate navigation targets. This re-uses the source location annotations feature that's used by the fuzzing web UI. [src] hrefs now look like #src/std/os/emscripten.zig:MSG.BATCH and scroll to the requested item. --- lib/docs/main.js | 66 ++++++++++++++++++++++++++++------- lib/docs/wasm/Walk.zig | 34 ++++++++++++++++++ lib/docs/wasm/html_render.zig | 5 +++ lib/docs/wasm/main.zig | 52 +++++++++++++++++++++++---- 4 files changed, 138 insertions(+), 19 deletions(-) diff --git a/lib/docs/main.js b/lib/docs/main.js index 330b51ba49d1..78396679eff6 100644 --- a/lib/docs/main.js +++ b/lib/docs/main.js @@ -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. @@ -180,7 +185,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"); } } @@ -224,22 +229,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]; @@ -384,7 +410,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"); } } @@ -414,7 +440,7 @@ renderErrorSet(base_decl, errorSetNodeList(decl_index, errorSetNode)); } - domSourceText.innerHTML = declSourceHtml(decl_index); + domSourceText.innerHTML = declSourceHtml(decl_index, false); domSectSource.classList.remove("hidden"); } @@ -428,7 +454,7 @@ domTldDocs.classList.remove("hidden"); } - domSourceText.innerHTML = declSourceHtml(decl_index); + domSourceText.innerHTML = declSourceHtml(decl_index, false); domSectSource.classList.remove("hidden"); } @@ -615,6 +641,7 @@ curNav.tag = 0; curNav.decl = null; curNav.path = null; + curNav.scrollToDeclPath = null; curNav.viewSourceHash = null; curNavSearch = ""; @@ -633,7 +660,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); @@ -904,8 +937,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) { @@ -973,6 +1006,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(); diff --git a/lib/docs/wasm/Walk.zig b/lib/docs/wasm/Walk.zig index e3884f6271e8..d246f19fb0c9 100644 --- a/lib/docs/wasm/Walk.zig +++ b/lib/docs/wasm/Walk.zig @@ -53,6 +53,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; @@ -89,6 +110,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)) { @@ -405,6 +438,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); diff --git a/lib/docs/wasm/html_render.zig b/lib/docs/wasm/html_render.zig index b7e79e5732c9..907ebe622504 100644 --- a/lib/docs/wasm/html_render.zig +++ b/lib/docs/wasm/html_render.zig @@ -11,10 +11,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 = &.{}, diff --git a/lib/docs/wasm/main.zig b/lib/docs/wasm/main.zig index d886f8037ca2..728947b9faca 100644 --- a/lib/docs/wasm/main.zig +++ b/lib/docs/wasm/main.zig @@ -6,10 +6,11 @@ const Walk = @import("Walk"); const markdown = @import("markdown.zig"); const Decl = Walk.Decl; -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; @@ -541,11 +542,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); @@ -841,6 +869,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. @@ -863,6 +896,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; From d7cf9beaa1ff7aa43bed05afc8ca2385f7cf6dda Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sat, 19 Apr 2025 18:05:57 +0100 Subject: [PATCH 2/7] Fix an out-of-bounds read --- lib/docs/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/main.js b/lib/docs/main.js index 78396679eff6..dbf3efea46ad 100644 --- a/lib/docs/main.js +++ b/lib/docs/main.js @@ -1028,7 +1028,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; } From b7904af63d2c7a0678b40d25879d4605a345f3f1 Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sat, 19 Apr 2025 21:16:11 +0100 Subject: [PATCH 3/7] Fix wrong union field access --- lib/docs/wasm/Decl.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/docs/wasm/Decl.zig b/lib/docs/wasm/Decl.zig index ba1a7b455ad5..e18d81c211a6 100644 --- a/lib/docs/wasm/Decl.zig +++ b/lib/docs/wasm/Decl.zig @@ -167,7 +167,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; From 1377381cb949043dacbabad70a82b2c40cc7e19a Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sat, 19 Apr 2025 18:09:37 +0100 Subject: [PATCH 4/7] Fix redundant search results Was caused by decls in test declarations, which are now ignored. --- lib/docs/wasm/Walk.zig | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/docs/wasm/Walk.zig b/lib/docs/wasm/Walk.zig index d246f19fb0c9..86c88098f645 100644 --- a/lib/docs/wasm/Walk.zig +++ b/lib/docs/wasm/Walk.zig @@ -15,6 +15,7 @@ pub var decls: std.ArrayListUnmanaged(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)) { @@ -84,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, @@ -514,6 +516,7 @@ pub const Scope = struct { }, .namespace => { const namespace: *Namespace = @alignCast(@fieldParentPtr("base", it)); + assert(namespace.decl_index != .none); return namespace.decl_index; }, }; @@ -590,7 +593,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); }, @@ -600,7 +604,8 @@ 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).?); }, @@ -608,7 +613,14 @@ fn struct_decl( .@"usingnamespace", => 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, }; @@ -1012,7 +1024,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()); } From ea4c9c44270bfa9553dd0c6578e7f8295048b8a3 Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sat, 19 Apr 2025 22:14:19 +0100 Subject: [PATCH 5/7] up&down arrows keep working when navigating back to search results And the position of the cursor within the results is now remembered. --- lib/docs/main.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/docs/main.js b/lib/docs/main.js index dbf3efea46ad..7705dc9f76c8 100644 --- a/lib/docs/main.js +++ b/lib/docs/main.js @@ -676,9 +676,14 @@ } function onHashChange(state) { + if (state != null) { + const restore_search_index = state.curSearchIndex; + if (restore_search_index !== undefined) curSearchIndex = restore_search_index; + } history.replaceState({}, ""); navigate(location.hash); if (state == null) window.scrollTo({top: 0}); + if (curNavSearch !== "") domSearch.focus({preventScroll: true}); } function onPopState(ev) { @@ -708,6 +713,7 @@ } if (liDom != null) { var aDom = liDom.children[0]; + history.replaceState({curSearchIndex: curSearchIndex}, ""); location.href = aDom.getAttribute("href"); curSearchIndex = -1; } @@ -722,6 +728,10 @@ clearAsyncSearch(); imFeelingLucky = true; location.hash = computeSearchHash(); + // 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); ev.preventDefault(); ev.stopPropagation(); From 7fcd411d0591998fb49a879efe7c5cc720fd3758 Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sun, 20 Apr 2025 13:04:41 +0100 Subject: [PATCH 6/7] Clicking search results preserves the page history --- lib/docs/main.js | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/docs/main.js b/lib/docs/main.js index 7705dc9f76c8..ae55a1d82d44 100644 --- a/lib/docs/main.js +++ b/lib/docs/main.js @@ -79,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 = []; @@ -696,12 +698,25 @@ domSearch.value = curNavSearch; } render(); - if (imFeelingLucky) { - imFeelingLucky = false; + if (loadSearchResult) { + loadSearchResult = false; activateSelectedResult(); } } + 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; @@ -720,19 +735,22 @@ 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(); - // 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); - + doLoadSearchResult(); ev.preventDefault(); ev.stopPropagation(); return; @@ -879,6 +897,7 @@ const full_name = fullyQualifiedName(match); aDom.textContent = full_name; aDom.setAttribute('href', navLinkFqn(full_name)); + aDom.addEventListener("click", onSearchResultClick); } renderSearchCursor(); From 2aff9b3ccd5656d5727ce593070343e94abc4b69 Mon Sep 17 00:00:00 2001 From: achan1989 Date: Sun, 20 Apr 2025 15:27:37 +0100 Subject: [PATCH 7/7] In source view, make @imports into links --- lib/docs/wasm/Walk.zig | 42 +++++++++++++++++++++-------------- lib/docs/wasm/html_render.zig | 31 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/lib/docs/wasm/Walk.zig b/lib/docs/wasm/Walk.zig index 86c88098f645..9412496950e7 100644 --- a/lib/docs/wasm/Walk.zig +++ b/lib/docs/wasm/Walk.zig @@ -354,23 +354,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| { @@ -383,6 +369,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).?; diff --git a/lib/docs/wasm/html_render.zig b/lib/docs/wasm/html_render.zig index 907ebe622504..a0a5e0a72464 100644 --- a/lib/docs/wasm/html_render.zig +++ b/lib/docs/wasm/html_render.zig @@ -161,6 +161,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, ""); + try appendEscaped(out, slice); + try out.appendSlice(gpa, ""); + continue; + } + } + try out.appendSlice(gpa, ""); try appendEscaped(out, slice); try out.appendSlice(gpa, ""); @@ -387,6 +407,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| {