╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │124 // bad url (NotFound -> 404); the default-branch path falls through to empty. │ │125 const resolved = (try ui.ResolvedRefOrOid(repo_kind, repo_opts).init(repo, io, aa, requested_ref_or_oid, requested_value)) orelse {│ │114 - if (requested_ref_or_oid != null) return error.NotFound; │ │126 + if (requested_ref_or_oid != null or term != null) return error.NotFound; │ │127 return emptyResult(aa, location, .branch, requested_value, path); │ │128 }; │ │129 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 // is a bad url (404). │ │134 var moment = repo.core.latestMoment() catch return emptyResult(aa, location, resolved.ref_or_oid, resolved.value, path); │ │135 const state = rp.Repo(repo_kind, repo_opts).State(.read_only){ .core = &repo.core, .extra = .{ .moment = &moment } }; │ │136 + │ │137 + var entries: std.ArrayList(Entry) = .empty; │ │138 + var capped = false; │ │139 + var preloaded_bytes: usize = 0; │ │140 + var find_available = false; │ │141 + │ │142 + // the index covers branch tips only, so any other view gets no search box │ │143 + // and refuses a find: url. │ │144 + if (comptime repo_kind == .xit) index: { │ │145 + if (resolved.ref_or_oid != .branch) break :index; │ │146 + const branch = std.Uri.percentDecodeInPlace(try aa.dupe(u8, resolved.value)); │ │147 + const tree_oid = try fnd.rootTreeOid(repo_opts, state, io, gpa, &resolved.oid); │ │148 + const files = (try fnd.lookup(repo_opts, moment, branch, &tree_oid)) orelse break :index; │ │149 + find_available = true; │ │150 + const value = term orelse break :index; │ │151 + // a result's name is its full path, so the listing has no directory and │ │152 + // the selected row is named in full. │ │153 + const selected: ?[]const u8 = if (path.len == 0) null else try aa.dupe(u8, path); │ │154 + const results = try fnd.search(repo_opts, files, aa, value, max_list_entries); │ │155 + capped = results.capped; │ │156 + var found = false; │ │157 + for (results.matches) |match| { │ │158 + if (selected) |sf| found = found or std.mem.eql(u8, sf, match.path); │ │159 + try entries.append(aa, try fileEntry(repo_kind, repo_opts, state, io, gpa, aa, "", match.path, &match.oid, null, selected, line, &preloaded_bytes));│ │160 + } │ │161 + // the cap can cut off the row the url names, so bring it back │ │162 + if (capped and !found) { │ │163 + if (selected) |sf| if (try fnd.get(repo_opts, files, aa, sf)) |*oid| { │ │164 + try entries.append(aa, try fileEntry(repo_kind, repo_opts, state, io, gpa, aa, "", sf, oid, null, selected, line, &preloaded_bytes)); │ │165 + }; │ │166 + } │ │167 + return .{ │ │168 + .location = try location.dupe(aa), │ │169 + .ref_or_oid = resolved.ref_or_oid, │ │170 + .ref_or_oid_value = resolved.value, │ │171 + .dir = "", │ │172 + .selected_file = selected, │ │173 + .entries = entries.items, │ │174 + .capped = capped, │ │175 + .find = value, │ │176 + .find_available = true, │ │177 + }; │ │178 + } │ │179 + if (term != null) return error.NotFound; │ │180 + │ │181 var tree_dir = tr.TreeDir(repo_kind, repo_opts).init(state, io, gpa, &resolved.oid, path) catch |err| switch (err) { │ │182 error.TreeEntryNotFound => return error.NotFound, │ │183 else => return emptyResult(aa, location, resolved.ref_or_oid, resolved.value, path), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │199 // within each group, and dupe into the page arena. file contents are read │ │200 // here (into the page arena) so the detail pane can show them without │ │201 // another lookup. │ │145 - const entries = try aa.alloc(Entry, tree_dir.entries.count()); │ │146 - var i: usize = 0; │ │147 - var preloaded_bytes: usize = 0; │ │202 + try entries.ensureTotalCapacity(aa, @min(tree_dir.entries.count(), max_list_entries));│ │203 for ([_]bool{ true, false }) |want_dir| { │ │204 for (tree_dir.entries.keys(), tree_dir.entries.values()) |name, tree_entry| { │ │205 if (tree_entry.isTree() != want_dir) continue; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 - var entry: Entry = .{ .name = try aa.dupe(u8, name), .is_dir = want_dir }; │ │152 - if (!want_dir) { │ │153 - // only the route's selected file paginates; the rest show │ │154 - // their first window (for the in-page detail when selected). │ │155 - const is_selected = if (selected_file) |sf| std.mem.eql(u8, sf, name) else false; │ │156 - // the README always loads because it's the default selection. │ │157 - if (preloaded_bytes < max_preloaded_bytes or is_selected or isReadme(name)) { │ │158 - const file_path = try childDir(aa, dir, name); │ │159 - // the url's `line` is 1-based (0 = unset); the window │ │160 - // index is 0-based. │ │161 - const window_start = if (is_selected) line -| 1 else 0; │ │162 - const content = readFileContent(repo_kind, repo_opts, state, io, gpa, aa, file_path, tree_entry, window_start) catch │ │163 - FileContent{ .lines = &.{} }; │ │164 - for (content.lines) |l| preloaded_bytes += l.len; │ │165 - entry.lines = content.lines; │ │166 - entry.is_binary = content.is_binary; │ │167 - entry.window_start = window_start; │ │168 - entry.has_more = content.has_more; │ │169 - entry.loaded = true; │ │206 + if (want_dir) { │ │207 + if (entries.items.len >= max_list_entries) { │ │208 + capped = true; │ │209 + continue; │ │210 + } │ │211 + try entries.append(aa, .{ .name = try aa.dupe(u8, name), .is_dir = true }); │ │212 + } else { │ │213 + // the rows the route and the default selection need are exempt │ │214 + // from the cap, so a url naming a file always gets its row. │ │215 + const exempt = isReadme(name) or (if (selected_file) |sf| std.mem.eql(u8, sf, name) else false); │ │216 + if (entries.items.len >= max_list_entries and !exempt) { │ │217 + capped = true; │ │218 + continue; │ │219 } │ │220 + try entries.append(aa, try fileEntry(repo_kind, repo_opts, state, io, gpa, aa, dir, name, &tree_entry.oid, tree_entry.mode, selected_file, line, &preloaded_bytes));│ │221 } │ │172 - entries[i] = entry; │ │173 - i += 1; │ │222 } │ │223 } │ │224 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │228 .ref_or_oid_value = resolved.value, │ │229 .dir = try aa.dupe(u8, dir), │ │230 .selected_file = selected_file, │ │183 - .entries = entries, │ │184 - .header = try Header.init(aa, resolved.ref_or_oid, resolved.value), │ │231 + .entries = entries.items, │ │232 + .capped = capped, │ │233 + .find_available = find_available, │ │234 }; │ │235 +} │ │236 + │ │237 +// build one file's row, reading its content window when the preload budget (or │ │238 +// the route) calls for it. only the route's selected file paginates; the rest │ │239 +// show their first window, for the in-page detail when selected. │ │240 +fn fileEntry( │ │241 + comptime repo_kind: rp.RepoKind, │ │242 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │243 + state: rp.Repo(repo_kind, repo_opts).State(.read_only), │ │244 + io: std.Io, │ │245 + gpa: std.mem.Allocator, │ │246 + aa: std.mem.Allocator, │ │247 + dir: []const u8, │ │248 + name: []const u8, │ │249 + oid: *const [xit.hash.byteLen(repo_opts.hash)]u8, │ │250 + // null for a search result, which the index only holds files for │ │251 + mode: ?xit.fs.Mode, │ │252 + selected_file: ?[]const u8, │ │253 + line: usize, │ │254 + preloaded_bytes: *usize, │ │255 +) !Entry { │ │256 + var entry: Entry = .{ .name = try aa.dupe(u8, name), .is_dir = false }; │ │257 + const is_selected = if (selected_file) |sf| std.mem.eql(u8, sf, name) else false; │ │258 + // the README always loads because it's the default selection. │ │259 + if (preloaded_bytes.* >= max_preloaded_bytes and !is_selected and !isReadme(name)) return entry; │ │260 + const file_path = try childDir(aa, dir, name); │ │261 + // the url's `line` is 1-based (0 = unset); the window index is 0-based. │ │262 + const window_start = if (is_selected) line -| 1 else 0; │ │263 + const content = readFileContent(repo_kind, repo_opts, state, io, gpa, aa, file_path, oid, mode, window_start) catch│ │264 + FileContent{ .lines = &.{} }; │ │265 + for (content.lines) |l| preloaded_bytes.* += l.len; │ │266 + entry.lines = content.lines; │ │267 + entry.is_binary = content.is_binary; │ │268 + entry.window_start = window_start; │ │269 + entry.has_more = content.has_more; │ │270 + entry.loaded = true; │ │271 + return entry; │ │272 } │ │273 │ │274 const FileContent = struct { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │278 }; │ │279 │ │280 // read the window [start, start+file_page) of a committed file's contents at │ │195 -// `tree_entry` into `arena`-owned lines, flagging whether more follow. xit's │ │281 +// `oid` into `arena`-owned lines, flagging whether more follow. xit's │ │282 // line iterator flags binary files (its source becomes `.binary`), in which case│ │283 // we report no lines and let the view show a placeholder. │ │284 fn readFileContent( │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │289 gpa: std.mem.Allocator, │ │290 arena: std.mem.Allocator, │ │291 path: []const u8, │ │206 - tree_entry: tr.TreeEntry(repo_opts.hash), │ │292 + oid: *const [xit.hash.byteLen(repo_opts.hash)]u8, │ │293 + mode: ?xit.fs.Mode, │ │294 start: usize, │ │295 ) !FileContent { │ │209 - var line_iter = try df.LineIterator(repo_kind, repo_opts).initFromTree(state, io, gpa, path, tree_entry);│ │296 + var line_iter = try df.LineIterator(repo_kind, repo_opts).initFromOid(state, io, gpa, path, oid, mode); │ │297 defer line_iter.deinit(); │ │298 if (line_iter.source == .binary) return .{ .lines = &.{}, .is_binary = true }; │ │299 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │322 .ref_or_oid_value = try aa.dupe(u8, ref_or_oid_value), │ │323 .dir = try aa.dupe(u8, dir), │ │324 .entries = &.{}, │ │238 - .header = try Header.init(aa, ref_or_oid, ref_or_oid_value),│ │325 }; │ │326 } │ │327 │ ╰─────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │328 pub const View = struct { │ │243 - // a vertical stack: the "viewing <ref>" banner on top, then a horizontal │ │329 + // a vertical stack: the search box and clone url on top, then a horizontal │ │330 // split with the file/directory list on the left and a detail pane on the │ │331 // right showing the selected file's contents. │ │246 - box: wgt.Box(ui.Widget), // vert: [header_index] = banner, [content_index] = split │ │332 + box: wgt.Box(ui.Widget), // vert: [header_index] = header row, [content_index] = split│ │333 data: *const Self, │ │334 session: *ui.Session, │ │335 // the list row whose contents the detail pane currently shows. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │352 var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │353 errdefer outer.deinit(allocator); │ │354 │ │269 - // the ref banner at the top. │ │355 + // the search box and the clone url at the top. │ │356 { │ │271 - var header_view = try Header.View.init(allocator, &data.header, session, data.location); │ │357 + var header_view = try Header.View.init(allocator, data, session); │ │358 errdefer header_view.deinit(allocator); │ │359 try outer.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .repo_files_header = header_view }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │360 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯