╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │385 │ │386 // the selected file's contents as a single focusable multi-line text box. │ │387 // it's borderless — the border lives on the surrounding scroll frame, which │ │369 - // doubles when this content is focused. `text` lives in the page arena. │ │370 - fn addContentBox(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8) !void { │ │388 + // doubles when this content is focused. `text` lives in the page arena. a │ │389 + // non-empty `link` makes activating the box follow it, like addRow. │ │390 + fn addContentBox(self: *View, allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), text: []const u8, link: []const u8) !void {│ │391 _ = self; │ │392 var tb = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = null, .wrap_kind = .none }); │ │393 errdefer tb.deinit(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │394 tb.getFocus().focusable = true; │ │395 + if (link.len != 0) tb.getFocus().kind = .{ .custom = link }; │ │396 try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null });│ │397 } │ │398 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │466 │ │467 // mirror the selected file into the url so it updates as the selection │ │468 // moves, but only while focus is inside this view (so landing on the tab │ │448 - // keeps the page's base route). a file selection carries the file path and│ │449 - // its content window offset; a directory / ".." row (or no selection) │ │450 - // stays at the directory with no offset. the path is joined in a stack │ │451 - // buffer since build runs every frame on the web. │ │469 + // keeps the page's base route). │ │470 if (root_focus.grandchild_id) |g| { │ │471 if (self.box.getFocus().children.contains(g)) { │ │472 var buf: [ui.RoutablePage.repo_route_max_len]u8 = undefined; │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │473 var sel_path = self.data.dir; │ │474 var sel_start: usize = 0; │ │475 if (self.selectedEntry()) |entry| { │ │458 - if (!entry.is_dir) { │ │476 + if (!entry.is_dir and entry.loaded) { │ │477 sel_path = if (self.data.dir.len == 0)│ │478 entry.name │ │479 else │ ╰───────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │540 // the top-level build by re-deriving it down the selected-child chain, so │ │541 // there's nothing to fix up afterward. │ │542 try self.box.build(allocator, constraint, root_focus); │ │543 + │ │544 + // clamp the list's initial offset (a selected row near the end of the │ │545 + // list) against this build's grids, for the next frame. initRoot's │ │546 + // pre-sizing build is unbounded — its full-content "viewport" would │ │547 + // clamp the offset to zero, so only a bounded build counts. │ │548 + if (constraint.max_size.height != null) self.listScroll().clampToContent();│ │549 } │ │550 │ │551 fn refreshDetail(self: *View, allocator: std.mem.Allocator) !void { │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │573 // the "next" window link sits above the scroll, so it stays put │ │574 // while the content scrolls. │ │575 if (entry.has_more) try self.addNavLink(allocator, nav_box, "next lines →", entry, entry.window_start + file_page);│ │552 - if (entry.is_binary) { │ │553 - try self.addContentBox(allocator, inner, "(binary file)"); │ │576 + if (!entry.loaded) { │ │577 + // the placeholder carries the file's "a:" link, so enter │ │578 + // (or a click) here loads the file like on its list row. │ │579 + const path = try childDir(self.session.page_arena.allocator(), self.data.dir, entry.name); │ │580 + const link = try fileLink(self.session.page_arena, self.data, path, false); │ │581 + try self.addContentBox(allocator, inner, "(press enter to load this file)", link); │ │582 + } else if (entry.is_binary) { │ │583 + try self.addContentBox(allocator, inner, "(binary file)", ""); │ │584 } else { │ │585 const text = try numberedContent(self.session.page_arena.allocator(), entry.lines, entry.window_start); │ │556 - try self.addContentBox(allocator, inner, text); │ │586 + try self.addContentBox(allocator, inner, text, ""); │ │587 } │ │588 } │ │589 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │590 // point each box at its first child so focus can descend into it. │ │591 if (nav_box.children.count() > 0) nav_box.getFocus().child_id = nav_box.children.keys()[0];│ │592 if (inner.children.count() > 0) inner.getFocus().child_id = inner.children.keys()[0]; │ │593 + │ │594 + // reserve the nav row's height (the shrink scroll frame yields it). │ │595 + self.detailOuter().children.values()[detail_nav_index].min_size = │ │596 + if (nav_box.children.count() > 0) .{ .width = null, .height = row_height } else null; │ │597 │ │598 // reset the scroll to the top for the newly-shown file: directly on the │ │599 // terminal (the wasm offset), and via a version bump on the web (so the │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │813 // if none. used to pick the default selection at the repo root. │ │814 fn readmeIndex(entries: []const Entry) ?usize { │ │815 for (entries, 0..) |entry, i| { │ │782 - if (entry.is_dir) continue; │ │783 - if (std.ascii.eqlIgnoreCase(entry.name, "README") or std.ascii.eqlIgnoreCase(entry.name, "README.md"))│ │784 - return i; │ │816 + if (!entry.is_dir and isReadme(entry.name)) return i; │ │817 } │ │818 return null; │ │819 +} │ │820 + │ │821 +fn isReadme(name: []const u8) bool { │ │822 + return std.ascii.eqlIgnoreCase(name, "README") or std.ascii.eqlIgnoreCase(name, "README.md"); │ │823 } │ │824 │ │825 // the index of the entry the route named (data.selected_file), or null if none. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │838 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{try route.toUrl(page_arena)}); │ │839 } │ │840 │ │805 -// an "ai:" link to the file's route: crossPageLink ignores it, so a wasm click │ │806 -// selects the row in place and shows its contents in the detail pane. │ │807 -fn fileLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, path: []const u8) ![]const u8 { │ │841 +// a link to the file's route. a preloaded file gets an "ai:" link │ │842 +// (crossPageLink ignores it, so a click selects the row in place and shows its │ │843 +// contents); an unloaded file gets an "a:" link, so activating it reloads the │ │844 +// page with that file selected and its content read. │ │845 +fn fileLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, path: []const u8, loaded: bool) ![]const u8 { │ │846 const route = ui.RoutablePage.repoFilesRoute(data.identity, data.ref_or_oid, data.ref_or_oid_value, path, 0) orelse return error.RouteTooLong;│ │809 - return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{try route.toUrl(page_arena)}); │ │847 + const prefix = if (loaded) "ai:" else "a:"; │ │848 + return std.fmt.allocPrint(page_arena.allocator(), "{s}{s}", .{ prefix, try route.toUrl(page_arena) }); │ │849 } │ │850 │ │851 // `dir`/`name`, or just `name` at the root. │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯