Skip to content

Autodoc enhancement: navigating to source from a decl will scroll to the decl #23562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 54 additions & 13 deletions lib/docs/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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.
Expand Down Expand Up @@ -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");
}
}
Expand Down Expand Up @@ -224,22 +229,44 @@
}
}

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) {
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];
Expand Down Expand Up @@ -384,7 +411,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");
}
}
Expand Down Expand Up @@ -414,7 +441,7 @@
renderErrorSet(base_decl, errorSetNodeList(decl_index, errorSetNode));
}

domSourceText.innerHTML = declSourceHtml(decl_index);
domSourceText.innerHTML = declSourceHtml(decl_index, false);
domSectSource.classList.remove("hidden");
}

Expand All @@ -428,7 +455,7 @@
domTldDocs.classList.remove("hidden");
}

domSourceText.innerHTML = declSourceHtml(decl_index);
domSourceText.innerHTML = declSourceHtml(decl_index, false);
domSectSource.classList.remove("hidden");
}

Expand Down Expand Up @@ -615,6 +642,7 @@
curNav.tag = 0;
curNav.decl = null;
curNav.path = null;
curNav.scrollToDeclPath = null;
curNav.viewSourceHash = null;
curNavSearch = "";

Expand All @@ -633,7 +661,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);
Expand Down Expand Up @@ -904,8 +938,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) {
Expand Down Expand Up @@ -973,6 +1007,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();
Expand Down
34 changes: 34 additions & 0 deletions lib/docs/wasm/Walk.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions lib/docs/wasm/html_render.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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 = &.{},
Expand Down
46 changes: 43 additions & 3 deletions lib/docs/wasm/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const Walk = @import("Walk");
const markdown = @import("markdown.zig");
const Decl = Walk.Decl;

const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
const html_render = @import("html_render.zig");
const fileSourceHtml = html_render.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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand All @@ -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;
Expand Down