╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │116 // when the route named none) can canonicalize each tab's mirror url to the │ │117 // explicit ref it's viewing rather than leaving it bare. both resolve the │ │118 // same requested ref, so they end up viewing the same one. │ │110 - const files = try Files.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir); │ │119 + const files = try Files.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, files_dir, files_after);│ │120 const commits = try Commits.init(arena, session, &found.event_id, rf.identity, requested_ref_or_oid, requested_ref_value, commits_after); │ │121 │ │122 // each tab mirror carries this page's route for that tab; tabs not targeted │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │124 // mirror carries the selected file (when the route named one) so the url │ │125 // keeps it before focus enters the view. │ │126 const files_path = if (files.selected_file) |f| try Files.childDir(arena.allocator(), files.dir, f) else files.dir; │ │118 - const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files.ref_or_oid, files.ref_or_oid_value, files_path) orelse return error.NotFound).repo; │ │127 + // the content window offset only applies to a selected file, so the bare │ │128 + // directory route drops it. │ │129 + const files_route_after = if (files.selected_file != null) files_after else 0; │ │130 + const route_name = (ui.RoutablePage.repoFilesRoute(rf.identity, files.ref_or_oid, files.ref_or_oid_value, files_path, files_route_after) orelse return error.NotFound).repo_files.name;│ │131 const commits_route_name = (ui.RoutablePage.repoCommitsRoute(rf.identity, commits.ref_or_oid, commits.ref_or_oid_value, commits_after) orelse return error.NotFound).repo_commits.name;│ │132 │ │133 return .{ │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │142 .auth = Auth.init(), │ │143 .quit = Quit.init(), │ │144 .route_name = route_name, │ │145 + .files_after = files_route_after, │ │146 .commits_route_name = commits_route_name, │ │147 .commits_after = commits_after, │ │148 .identity = Array.from(rf.identity) orelse return error.NotFound,│ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │249 // alone (nothing to mirror into the url). │ │250 .quit => {}, │ │251 // the files tab (this page's directory route) │ │239 - else => self.session.data.current_page = .{ .repo = self.data.route_name }, │ │252 + else => self.session.data.current_page = .{ .repo_files = .{ .name = self.data.route_name, .after = self.data.files_after } },│ │253 } │ │254 } │ │255 try self.box.build(allocator, constraint, root_focus); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │829 // the "a:" link to the files tab at commit `oid` (an object ref), at its root │ │830 // directory, within `identity` ("owner/name"). │ │831 fn filesObjectLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, oid: []const u8) ![]const u8 { │ │832 - const route = ui.RoutablePage.repoFilesRoute(identity, .object, oid, "") orelse return error.RouteTooLong; │ │832 + const route = ui.RoutablePage.repoFilesRoute(identity, .object, oid, "", 0) orelse return error.RouteTooLong;│ │833 const url = try route.urlAlloc(page_arena); │ │834 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │835 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │15 const SubHeader = @import("SubHeader.zig"); │ │16 │ │17 const RefOrOid = ui.RoutablePage.RefOrOid; │ │18 + │ │19 +// how many lines of a file's content one window shows before a "next" link. │ │20 +const file_page = 2000; │ │21 + │ │22 +// the most directory entries (files + dirs) we read from a tree and display. │ │23 +// each file's content is also loaded (windowed), so an unbounded directory could│ │24 +// add up to a lot of memory; entries past this limit are ignored. │ │25 +// │ │26 +// TODO: this caps what we keep, but `tr.Tree.init` still flattens the entire │ │27 +// repo tree (every path) into memory before we ever apply the cap. read the │ │28 +// directory level-by-level instead, using `obj.Object` to load one tree object │ │29 +// at a time: initCommit -> root tree, then descend each `dir` segment, reading │ │30 +// only the immediate entries of the target tree (and applying max_entries │ │31 +// there). that bounds the work to O(depth) tree objects rather than the whole │ │32 +// repo. it's doable entirely in haxy (no xit change) — `obj.Object(...).init` │ │33 +// on a tree oid already returns just that tree's immediate children. │ │34 +const max_entries = 100; │ │35 │ │36 // one entry in the directory currently being viewed. │ │37 pub const Entry = struct { │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │38 name: []const u8, │ │39 is_dir: bool, │ │23 - // a file's contents split into lines (each without its trailing newline);│ │24 - // empty for directories and for binary or empty files. │ │40 + // a file's content window split into lines (each without its trailing │ │41 + // newline); empty for directories and for binary or empty files. │ │42 lines: []const []const u8 = &.{}, │ │43 // true when xit's line iterator flagged the file as binary, so the detail│ │44 // pane shows a placeholder rather than its bytes. │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │45 is_binary: bool = false, │ │46 + // the line index this window starts at (0 = the first window). non-zero only│ │47 + // for the route's selected file, paginated by the url's `after`. │ │48 + window_start: usize = 0, │ │49 + // whether more lines exist after this window. │ │50 + has_more: bool = false, │ │51 }; │ │52 │ │53 // "owner/name", so the view can build /repo/owner/name/files/... links. │ ╰─────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │76 requested_ref_or_oid: ?RefOrOid, │ │77 requested_value: []const u8, │ │78 path: []const u8, │ │79 + // the line offset the selected file's content window starts at (0 = first).│ │80 + after: usize, │ │81 ) !Self { │ │82 const aa = arena.allocator(); │ │83 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │137 const slash = std.mem.indexOfScalar(u8, rel, '/'); │ │138 const name = if (slash) |s| rel[0..s] else rel; │ │139 const is_dir = slash != null; │ │140 + // stop taking new entries once we hit the cap; the rest are ignored. │ │141 + if (children.count() >= max_entries and !children.contains(name)) continue; │ │142 const gop = try children.getOrPut(gpa, name); │ │143 if (!gop.found_existing) gop.value_ptr.* = is_dir else if (is_dir) gop.value_ptr.* = true;│ │144 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯