Skip to content

Commit 4e8adb5

Browse files
committed
Navigating to source from a decl will scroll to the decl
1 parent e24e9ff commit 4e8adb5

File tree

4 files changed

+136
-16
lines changed

4 files changed

+136
-16
lines changed

lib/docs/main.js

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
const domErrors = document.getElementById("errors");
5757
const domErrorsText = document.getElementById("errorsText");
5858

59+
// Chosen to prevent collisions with the IDs above.
60+
const navPrefix = "nav_";
61+
5962
var searchTimer = null;
6063

6164
const curNav = {
@@ -67,6 +70,8 @@
6770
decl: null,
6871
// string file name matching tarball path
6972
path: null,
73+
// string decl path within source file
74+
scrollToDeclPath: null,
7075

7176
// when this is populated, pressing the "view source" command will
7277
// navigate to this hash.
@@ -180,7 +185,7 @@
180185
} else {
181186
return renderDecl(curNav.decl);
182187
}
183-
case 2: return renderSource(curNav.path);
188+
case 2: return renderSource(curNav.path, curNav.scrollToDeclPath);
184189
default: throw new Error("invalid navigation state");
185190
}
186191
}
@@ -224,22 +229,44 @@
224229
}
225230
}
226231

227-
function renderSource(path) {
228-
const decl_index = findFileRoot(path);
229-
if (decl_index == null) return renderNotFound();
232+
function renderSource(path, scroll_to_decl_path) {
233+
const root_index = findFileRoot(path);
234+
if (root_index == null) return renderNotFound();
230235

231-
renderNavFancy(decl_index, [{
236+
renderNavFancy(root_index, [{
232237
name: "[src]",
233238
href: location.hash,
234239
}]);
235240

236-
domSourceText.innerHTML = declSourceHtml(decl_index);
237-
241+
domSourceText.innerHTML = declSourceHtml(root_index, true);
238242
domSectSource.classList.remove("hidden");
243+
244+
const scroll_to_decl_index = findDeclPathInNamespace(root_index, scroll_to_decl_path);
245+
if (scroll_to_decl_index !== null) {
246+
const to_elem = document.getElementById(navPrefix + scroll_to_decl_index);
247+
if (to_elem != null) {
248+
setTimeout(function() {
249+
to_elem.scrollIntoView();
250+
}, 0);
251+
}
252+
}
239253
}
240254

241255
function renderDeclHeading(decl_index) {
242256
curNav.viewSourceHash = "#src/" + unwrapString(wasm_exports.decl_file_path(decl_index));
257+
const is_root = wasm_exports.decl_is_root(decl_index);
258+
if (!is_root) {
259+
// E.g. if `decl_index` corresponds to `root.foo.bar` we want `foo.bar`
260+
var subcomponents = [];
261+
let decl_it = decl_index;
262+
while (decl_it != null) {
263+
subcomponents.push(declIndexName(decl_it));
264+
decl_it = declParent(decl_it);
265+
}
266+
subcomponents.pop();
267+
subcomponents.reverse();
268+
curNav.viewSourceHash += ":" + subcomponents.join(".");
269+
}
243270

244271
const hdrNameSpan = domHdrName.children[0];
245272
const srcLink = domHdrName.children[1];
@@ -384,7 +411,7 @@
384411
if (members.length !== 0 || fields.length !== 0) {
385412
renderNamespace(decl_index, members, fields);
386413
} else {
387-
domSourceText.innerHTML = declSourceHtml(decl_index);
414+
domSourceText.innerHTML = declSourceHtml(decl_index, false);
388415
domSectSource.classList.remove("hidden");
389416
}
390417
}
@@ -414,7 +441,7 @@
414441
renderErrorSet(base_decl, errorSetNodeList(decl_index, errorSetNode));
415442
}
416443

417-
domSourceText.innerHTML = declSourceHtml(decl_index);
444+
domSourceText.innerHTML = declSourceHtml(decl_index, false);
418445
domSectSource.classList.remove("hidden");
419446
}
420447

@@ -428,7 +455,7 @@
428455
domTldDocs.classList.remove("hidden");
429456
}
430457

431-
domSourceText.innerHTML = declSourceHtml(decl_index);
458+
domSourceText.innerHTML = declSourceHtml(decl_index, false);
432459
domSectSource.classList.remove("hidden");
433460
}
434461

@@ -615,6 +642,7 @@
615642
curNav.tag = 0;
616643
curNav.decl = null;
617644
curNav.path = null;
645+
curNav.scrollToDeclPath = null;
618646
curNav.viewSourceHash = null;
619647
curNavSearch = "";
620648

@@ -633,7 +661,13 @@
633661
const source_mode = nonSearchPart.startsWith("src/");
634662
if (source_mode) {
635663
curNav.tag = 2;
636-
curNav.path = nonSearchPart.substring(4);
664+
const idpos = nonSearchPart.indexOf(":");
665+
if (idpos === -1) {
666+
curNav.path = nonSearchPart.substring(4);
667+
} else {
668+
curNav.path = nonSearchPart.substring(4, idpos);
669+
curNav.scrollToDeclPath = nonSearchPart.substring(idpos + 1);
670+
}
637671
} else {
638672
curNav.tag = 1;
639673
curNav.decl = findDecl(nonSearchPart);
@@ -904,8 +938,8 @@
904938
return unwrapString(wasm_exports.decl_name(decl_index));
905939
}
906940

907-
function declSourceHtml(decl_index) {
908-
return unwrapString(wasm_exports.decl_source_html(decl_index));
941+
function declSourceHtml(decl_index, decl_nav_targets) {
942+
return unwrapString(wasm_exports.decl_source_html(decl_index, decl_nav_targets));
909943
}
910944

911945
function declDoctestHtml(decl_index) {
@@ -973,6 +1007,13 @@
9731007
return result;
9741008
}
9751009

1010+
function findDeclPathInNamespace(namespace_decl_index, path) {
1011+
setInputString(path);
1012+
const result = wasm_exports.find_decl_path_in_namespace(namespace_decl_index);
1013+
if (result === -1) return null;
1014+
return result;
1015+
}
1016+
9761017
function findFileRoot(path) {
9771018
setInputString(path);
9781019
const result = wasm_exports.find_file_root();

lib/docs/wasm/Walk.zig

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ pub const File = struct {
5353
/// struct/union/enum/opaque decl node => its namespace scope
5454
/// local var decl node => its local variable scope
5555
scopes: std.AutoArrayHashMapUnmanaged(Ast.Node.Index, *Scope) = .empty,
56+
/// Last decl in the file (exclusive).
57+
decl_end: Decl.Index = .none,
58+
59+
pub const DeclIter = struct {
60+
idx: DeclIndexTagType,
61+
end: DeclIndexTagType,
62+
63+
const DeclIndexTagType = @typeInfo(Decl.Index).@"enum".tag_type;
64+
65+
pub fn next(iter: *DeclIter) ?Decl.Index {
66+
if (iter.idx >= iter.end) return null;
67+
const decl_idx = iter.idx;
68+
iter.idx += 1;
69+
return @enumFromInt(decl_idx);
70+
}
71+
72+
pub fn remaining(iter: DeclIter) usize {
73+
if (iter.idx >= iter.end) return 0;
74+
return iter.end - iter.idx;
75+
}
76+
};
5677

5778
pub fn lookup_token(file: *File, token: Ast.TokenIndex) Decl.Index {
5879
const decl_node = file.ident_decls.get(token) orelse return .none;
@@ -89,6 +110,18 @@ pub const File = struct {
89110
return file_index.get().node_decls.values()[0];
90111
}
91112

113+
/// Excludes the root decl.
114+
/// Only valid after `Walk.add_file()`.
115+
pub fn iterDecls(file_index: File.Index) DeclIter {
116+
const root_idx = @intFromEnum(file_index.findRootDecl());
117+
const end_idx = file_index.get().decl_end;
118+
assert(end_idx != .none);
119+
return DeclIter{
120+
.idx = root_idx + 1,
121+
.end = @intFromEnum(end_idx),
122+
};
123+
}
124+
92125
pub fn categorize_decl(file_index: File.Index, node: Ast.Node.Index) Category {
93126
const ast = file_index.get_ast();
94127
switch (ast.nodeTag(node)) {
@@ -405,6 +438,7 @@ pub fn add_file(file_name: []const u8, bytes: []u8) !File.Index {
405438
try struct_decl(&w, scope, decl_index, .root, ast.containerDeclRoot());
406439

407440
const file = file_index.get();
441+
file.decl_end = @enumFromInt(decls.items.len);
408442
shrinkToFit(&file.ident_decls);
409443
shrinkToFit(&file.token_parents);
410444
shrinkToFit(&file.node_decls);

lib/docs/wasm/html_render.zig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ const Oom = error{OutOfMemory};
1111
/// Delete this to find out where URL escaping needs to be added.
1212
pub const missing_feature_url_escape = true;
1313

14+
/// Prevents collisions with IDs in index.html
15+
/// Keep in sync with the `navPrefix` constant in `main.js`.
16+
pub const nav_prefix: []const u8 = "nav_";
17+
1418
pub const RenderSourceOptions = struct {
1519
skip_doc_comments: bool = false,
1620
skip_comments: bool = false,
1721
collapse_whitespace: bool = false,
22+
/// Render a specific function as a link to its documentation.
1823
fn_link: Decl.Index = .none,
1924
/// Assumed to be sorted ascending.
2025
source_location_annotations: []const Annotation = &.{},

lib/docs/wasm/main.zig

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ const Walk = @import("Walk");
66
const markdown = @import("markdown.zig");
77
const Decl = Walk.Decl;
88

9-
const fileSourceHtml = @import("html_render.zig").fileSourceHtml;
9+
const html_render = @import("html_render.zig");
10+
const fileSourceHtml = html_render.fileSourceHtml;
1011
const appendEscaped = @import("html_render.zig").appendEscaped;
1112
const resolveDeclLink = @import("html_render.zig").resolveDeclLink;
1213
const missing_feature_url_escape = @import("html_render.zig").missing_feature_url_escape;
@@ -541,11 +542,38 @@ export fn decl_fn_proto_html(decl_index: Decl.Index, linkify_fn_name: bool) Stri
541542
return String.init(string_result.items);
542543
}
543544

544-
export fn decl_source_html(decl_index: Decl.Index) String {
545+
/// `decl_nav_targets`: create targets for jumping to decls. If true, asserts `decl_index` is the
546+
/// root decl of a file.
547+
export fn decl_source_html(decl_index: Decl.Index, decl_nav_targets: bool) String {
545548
const decl = decl_index.get();
546549

550+
var sla: std.ArrayListUnmanaged(html_render.Annotation) = .empty;
551+
defer sla.deinit(gpa);
552+
if (decl_nav_targets) {
553+
const root_file = decl_index.get().file;
554+
assert(decl_index == root_file.findRootDecl());
555+
556+
const ast = root_file.get_ast();
557+
var it = root_file.iterDecls();
558+
sla.ensureTotalCapacityPrecise(gpa, it.remaining()) catch @panic("OOM");
559+
while (true) {
560+
const inner_decl_index = (it.next() orelse break);
561+
const inner_decl = inner_decl_index.get();
562+
if (!inner_decl.is_pub()) continue;
563+
const decl_tok = ast.firstToken(inner_decl.ast_node);
564+
const tok_start = ast.tokenStart(decl_tok);
565+
sla.appendAssumeCapacity(.{
566+
.file_byte_offset = tok_start,
567+
.dom_id = @intFromEnum(inner_decl_index),
568+
});
569+
}
570+
}
571+
547572
string_result.clearRetainingCapacity();
548-
fileSourceHtml(decl.file, &string_result, decl.ast_node, .{}) catch |err| {
573+
fileSourceHtml(decl.file, &string_result, decl.ast_node, .{
574+
.source_location_annotations = sla.items,
575+
.annotation_prefix = html_render.nav_prefix,
576+
}) catch |err| {
549577
std.debug.panic("unable to render source: {s}", .{@errorName(err)});
550578
};
551579
return String.init(string_result.items);
@@ -841,6 +869,11 @@ export fn find_file_root() Decl.Index {
841869
return file.findRootDecl();
842870
}
843871

872+
/// Does the decl correspond to the root struct of a file?
873+
export fn decl_is_root(decl_index: Decl.Index) bool {
874+
return decl_index.get().file.findRootDecl() == decl_index;
875+
}
876+
844877
/// Uses `input_string`.
845878
/// Tries to look up the Decl component-wise but then falls back to a file path
846879
/// based scan.
@@ -863,6 +896,13 @@ export fn find_decl() Decl.Index {
863896
return .none;
864897
}
865898

899+
/// Uses `input_string` as a decl path.
900+
/// Start in the namespace corresponding to `decl_index`, find a child decl by path.
901+
/// The path can contain multiple components e.g. `foo.bar`.
902+
export fn find_decl_path_in_namespace(decl_index: Decl.Index) Decl.Index {
903+
return resolve_decl_path(decl_index, input_string.items) orelse .none;
904+
}
905+
866906
/// Set only by `categorize_decl`; read only by `get_aliasee`, valid only
867907
/// when `categorize_decl` returns `.alias`.
868908
var global_aliasee: Decl.Index = .none;

0 commit comments

Comments
 (0)