╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────╮ │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 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │161 if (!is_dir) { │ │162 const file_path = try childDir(aa, dir, name); │ │163 if (tree.entries.get(file_path)) |tree_entry| { │ │138 - const content = readFileContent(state, io, gpa, aa, file_path, tree_entry) catch │ │139 - FileContent{ .lines = &.{}, .is_binary = false }; │ │164 + // only the route's selected file paginates; the rest show │ │165 + // their first window (for the in-page detail when selected). │ │166 + const is_selected = if (selected_file) |sf| std.mem.eql(u8, sf, name) else false; │ │167 + const window_start = if (is_selected) after else 0; │ │168 + const content = readFileContent(state, io, gpa, aa, file_path, tree_entry, window_start) catch│ │169 + FileContent{ .lines = &.{} }; │ │170 entry.lines = content.lines; │ │171 entry.is_binary = content.is_binary; │ │172 + entry.window_start = window_start; │ │173 + entry.has_more = content.has_more; │ │174 } │ │175 } │ │176 entries[i] = entry; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │191 │ │192 const FileContent = struct { │ │193 lines: []const []const u8, │ │162 - is_binary: bool, │ │194 + is_binary: bool = false, │ │195 + has_more: bool = false, │ │196 }; │ │197 │ │165 -// read a committed file's contents at `tree_entry` into `arena`-owned lines. │ │166 -// xit's line iterator flags binary files (its source becomes `.binary`), in │ │167 -// which case we report no lines and let the view show a placeholder. │ │198 +// read the window [start, start+file_page) of a committed file's contents at │ │199 +// `tree_entry` into `arena`-owned lines, flagging whether more follow. xit's │ │200 +// line iterator flags binary files (its source becomes `.binary`), in which case│ │201 +// we report no lines and let the view show a placeholder. │ │202 fn readFileContent( │ │203 state: rp.Repo(.xit, .{}).State(.read_only), │ │204 io: std.Io, │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────╮ │206 arena: std.mem.Allocator, │ │207 path: []const u8, │ │208 tree_entry: tr.TreeEntry((rp.RepoOpts(.xit){}).hash), │ │209 + start: usize, │ │210 ) !FileContent { │ │211 var line_iter = try df.LineIterator(.xit, .{}).initFromTree(state, io, gpa, path, tree_entry);│ │212 defer line_iter.deinit(); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │213 if (line_iter.source == .binary) return .{ .lines = &.{}, .is_binary = true };│ │214 │ │180 - // init reads through the file to validate it (and buffer it in memory), │ │181 - // leaving the cursor at the end, so rewind before reading the lines out. │ │182 - try line_iter.reset(); │ │215 + // init reads through the file to validate it (and buffer it in memory), so │ │216 + // the line count is known and a window can be sliced out of the buffer. │ │217 + const total = line_iter.count(); │ │218 + const end = @min(start + file_page, total); │ │219 │ │220 var lines: std.ArrayList([]const u8) = .empty; │ │185 - while (try line_iter.next()) |line| { │ │221 + var i = start; │ │222 + while (i < end) : (i += 1) { │ │223 + const line = try line_iter.get(i); │ │224 defer line_iter.free(line); │ │225 try lines.append(arena, try arena.dupe(u8, line)); │ │226 } │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │189 - return .{ .lines = try lines.toOwnedSlice(arena), .is_binary = false }; │ │227 + return .{ │ │228 + .lines = try lines.toOwnedSlice(arena), │ │229 + .has_more = end < total, │ │230 + }; │ │231 } │ │232 │ │233 // an empty listing pinned to a ref, for the wasm / no-repo / unresolved paths.│ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯