╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │373 // copied by their text boxes. │ │374 const aa = session.page_arena.allocator(); │ │375 if (data.dir.len != 0) { │ │290 - try addRow(allocator, &list_box, "..", try dirLink(session.page_arena, data, parentDir(data.dir))); │ │376 + try addRow(allocator, &list_box, "..", "", try dirLink(session.page_arena, data, parentDir(data.dir)));│ │377 } │ │378 for (data.entries) |entry| { │ │379 const path = try childDir(aa, data.dir, entry.name); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │294 - const label = if (entry.is_dir) try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) else entry.name;│ │380 + // a search result shows its file name, with its directory │ │381 + // in the bottom label. │ │382 + const label = if (data.find != null) │ │383 + std.fs.path.basenamePosix(entry.name) │ │384 + else if (entry.is_dir) │ │385 + try std.fmt.allocPrint(aa, "{s}/", .{entry.name}) │ │386 + else │ │387 + entry.name; │ │388 + const bottom_label = if (data.find != null) try resultDirLabel(aa, entry.name) else ""; │ │389 const link = if (entry.is_dir) │ │390 try dirLink(session.page_arena, data, path) │ │391 else │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │392 try fileLink(session.page_arena, data, path, entry.loaded, lineNumber(entry.window_start));│ │299 - try addRow(allocator, &list_box, label, link); │ │393 + try addRow(allocator, &list_box, label, bottom_label, link); │ │394 } │ │395 + if (data.capped) try addRow(allocator, &list_box, "(list truncated)", "", ""); │ │396 // select the file the route named, else prefer a README so its │ │397 // contents greet the visitor in the detail pane, else the first │ │398 // row. a non-root directory has a leading ".." row, so the entry │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │304 - // index is offset by one there. │ │399 + // index is offset by one there. search results have no README│ │400 + // default: the term decides what matters. │ │401 var sel: usize = 0; │ │402 if (list_box.children.count() > 0) { │ │403 const base: usize = if (data.dir.len != 0) 1 else 0; │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯