╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │20 // selecting them shows contents without a reload. every entry is still listed;│ │21 // files past this limit carry an "a:" link instead, so activating them reloads│ │22 // the page with that file selected (and its content loaded as the selection). │ │23 -// │ │24 -// TODO: `tr.Tree.init` still flattens the entire repo tree (every path) into │ │25 -// memory before we ever filter to one directory. read the directory │ │26 -// level-by-level instead, using `obj.Object` to load one tree object at a │ │27 -// time: initCommit -> root tree, then descend each `dir` segment, reading │ │28 -// only the immediate entries of the target tree. that bounds the work to │ │29 -// O(depth) tree objects rather than the whole repo. it's doable entirely in │ │30 -// haxy (no xit change) — `obj.Object(...).init` on a tree oid already returns │ │31 -// just that tree's immediate children. │ │23 const max_preloaded = 100; │ │24 │ │25 // one entry in the directory currently being viewed. │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 87 return emptyResult(aa, identity, .branch, requested_value, path); │ │ 88 }; │ │ 89 │ │ 99 - // read the tree at that commit. building the read-only state mirrors what │ │100 - // repo.status does internally, but for an arbitrary commit rather than HEAD. │ │ 90 + // read just the viewed directory of that commit's tree. building the │ │ 91 + // read-only state mirrors what repo.status does internally, but for an │ │ 92 + // arbitrary commit rather than HEAD. a path that doesn't exist in the tree │ │ 93 + // is a bad url (404). │ │ 94 var moment = repo.core.latestMoment() catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, path); │ │ 95 const state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │103 - var tree = tr.Tree(repo_kind, repo_opts).init(state, io, gpa, &resolved.oid) catch return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, path);│ │104 - defer tree.deinit(); │ │ 96 + var tree_dir = tr.TreeDir(repo_kind, repo_opts).init(state, io, gpa, &resolved.oid, path) catch |err| switch (err) { │ │ 97 + error.TreeEntryNotFound => return error.NotFound, │ │ 98 + else => return emptyResult(aa, identity, resolved.ref_or_oid, resolved.value, path), │ │ 99 + }; │ │100 + defer tree_dir.deinit(); │ │101 │ │106 - // `path` names a file when it's an exact tree entry: list its parent │ │107 - // directory and start that file selected. otherwise `path` is the directory. │ │102 + // when `path` named a file, the listing is its parent directory with that │ │103 + // file selected. │ │104 var dir = path; │ │105 var selected_file: ?[]const u8 = null; │ │110 - if (path.len != 0 and tree.entries.get(path) != null) { │ │106 + if (tree_dir.file_name != null) { │ │107 const slash = std.mem.lastIndexOfScalar(u8, path, '/'); │ │108 selected_file = try aa.dupe(u8, if (slash) |s| path[s + 1 ..] else path); │ │109 dir = if (slash) |s| path[0..s] else ""; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │114 - } │ │115 - │ │116 - // collect the immediate children of `dir`. each committed path is a full │ │117 - // file path; a child is a directory when more path follows its first │ │118 - // segment under `dir`. │ │119 - var children: std.StringArrayHashMapUnmanaged(bool) = .empty; // name -> is_dir │ │120 - defer children.deinit(gpa); │ │121 - const prefix_len = if (dir.len == 0) 0 else dir.len + 1; // skip "dir/" │ │122 - for (tree.entries.keys()) |entry_path| { │ │123 - if (dir.len != 0) { │ │124 - if (!std.mem.startsWith(u8, entry_path, dir) or entry_path.len <= dir.len or entry_path[dir.len] != '/') continue;│ │125 - } │ │126 - const rel = entry_path[prefix_len..]; │ │127 - const slash = std.mem.indexOfScalar(u8, rel, '/'); │ │128 - const name = if (slash) |s| rel[0..s] else rel; │ │129 - const is_dir = slash != null; │ │130 - const gop = try children.getOrPut(gpa, name); │ │131 - if (!gop.found_existing) gop.value_ptr.* = is_dir else if (is_dir) gop.value_ptr.* = true; │ │110 } │ │133 - │ │134 - // trees hold no empty directories, so a non-root `dir` with no children │ │135 - // doesn't exist in this ref — a bad url (404). │ │136 - if (dir.len != 0 and children.count() == 0) return error.NotFound; │ │111 │ │138 - // committed paths come out sorted (tree objects are written in sorted │ │139 - // order), so the deduped children are already in name order. just group │ │140 - // directories before files, keeping that order within each group, and dupe │ │141 - // into the page arena. each file's contents are read here (into the page │ │142 - // arena) so the detail pane can show them without another lookup. │ │143 - const entries = try aa.alloc(Entry, children.count()); │ │112 + // tree objects store their entries in name order, so the listing is │ │113 + // already sorted. group directories before files, keeping that order │ │114 + // within each group, and dupe into the page arena. file contents are read │ │115 + // here (into the page arena) so the detail pane can show them without │ │116 + // another lookup. │ │117 + const entries = try aa.alloc(Entry, tree_dir.entries.count()); │ │118 var i: usize = 0; │ │119 var preloaded: usize = 0; │ │120 for ([_]bool{ true, false }) |want_dir| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 - for (children.keys(), children.values()) |name, is_dir| { │ │148 - if (is_dir != want_dir) continue; │ │149 - var entry: Entry = .{ .name = try aa.dupe(u8, name), .is_dir = is_dir }; │ │150 - if (!is_dir) { │ │121 + for (tree_dir.entries.keys(), tree_dir.entries.values()) |name, tree_entry| { │ │122 + if (tree_entry.isTree() != want_dir) continue; │ │123 + var entry: Entry = .{ .name = try aa.dupe(u8, name), .is_dir = want_dir }; │ │124 + if (!want_dir) { │ │125 // only the route's selected file paginates; the rest show │ │126 // their first window (for the in-page detail when selected). │ │127 const is_selected = if (selected_file) |sf| std.mem.eql(u8, sf, name) else false;│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │128 // the README always loads because it's the default selection. │ │129 if (preloaded < max_preloaded or is_selected or isReadme(name)) { │ │130 + preloaded += 1; │ │131 const file_path = try childDir(aa, dir, name); │ │157 - if (tree.entries.get(file_path)) |tree_entry| { │ │158 - preloaded += 1; │ │159 - const window_start = if (is_selected) start else 0; │ │160 - const content = readFileContent(repo_kind, repo_opts, state, io, gpa, aa, file_path, tree_entry, window_start) catch│ │161 - FileContent{ .lines = &.{} }; │ │162 - entry.lines = content.lines; │ │163 - entry.is_binary = content.is_binary; │ │164 - entry.window_start = window_start; │ │165 - entry.has_more = content.has_more; │ │166 - entry.loaded = true; │ │167 - } │ │132 + const window_start = if (is_selected) start else 0; │ │133 + const content = readFileContent(repo_kind, repo_opts, state, io, gpa, aa, file_path, tree_entry, window_start) catch │ │134 + FileContent{ .lines = &.{} }; │ │135 + entry.lines = content.lines; │ │136 + entry.is_binary = content.is_binary; │ │137 + entry.window_start = window_start; │ │138 + entry.has_more = content.has_more; │ │139 + entry.loaded = true; │ │140 } │ │141 } │ │142 entries[i] = entry; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯