╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │292 } │ │293 │ │294 pub const View = struct { │ │299 - // a vertical stack: the ref banner on top, then a horizontal │ │295 + // a vertical stack: the clone url row on top, then a horizontal │ │296 // split with the commit list on the left and a diff pane on the right │ │297 // showing the selected commit's diff. │ │302 - box: wgt.Box(ui.Widget), // vert: [header_index] = banner, [content_index] = split │ │298 + box: wgt.Box(ui.Widget), // vert: [header_index] = clone url row, [content_index] = split│ │299 data: *const Self, │ │300 session: *ui.Session, │ │301 // the commit whose diff the pane currently shows (index into data.commits). │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │313 var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │314 errdefer outer.deinit(allocator); │ │315 │ │320 - // the ref banner at the top. │ │316 + // the clone url at the top. │ │317 { │ │322 - var header_view = try Header.View.init(allocator, &data.header, session, data.location); │ │318 + var header_view = try Header.View.init(allocator, session, data.location); │ │319 errdefer header_view.deinit(allocator); │ │320 try outer.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .repo_commits_header = header_view }, .rect = null, .min_size = .{ .width = null, .height = 3 } });│ │321 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │728 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │729 } │ │730 │ │735 -// the "<ref_or_oid> <value>" banner shown above the log. │ │731 +// the clone url shown above the log. │ │732 pub const Header = struct { │ │737 - content: []const u8, │ │738 - │ │739 - // `value` arrives url-encoded, so decode it for display. │ │740 - pub fn init(aa: std.mem.Allocator, ref_or_oid: ui.RoutablePage.RefOrOid, value: []const u8) !Header { │ │741 - const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, value)); │ │742 - return .{ │ │743 - .content = try std.fmt.allocPrint(aa, "{s} {s}", .{ @tagName(ref_or_oid), decoded }), │ │744 - }; │ │745 - } │ │746 - │ │733 pub const View = struct { │ │734 box: wgt.Box(ui.Widget), │ │749 - data: *const Header, │ │735 │ │751 - pub fn init(allocator: std.mem.Allocator, data: *const Header, session: *ui.Session, location: ui.RoutablePage.RepoLocation) !Header.View {│ │736 + pub fn init(allocator: std.mem.Allocator, session: *ui.Session, location: ui.RoutablePage.RepoLocation) !Header.View { │ │737 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │738 errdefer box.deinit(allocator); │ │739 + │ │740 + // the spacer pushes the clone url to the right edge. │ │741 + { │ │742 + var spacer = try ui.widget.Spacer.init(allocator); │ │743 + errdefer spacer.deinit(allocator); │ │744 + try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │745 + } │ │746 │ │747 if (session.data.host_kind == .server) { │ │748 if (try ui.widget.CopyableText.initClone(allocator, session, location)) |value| { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │753 try box.children.put(allocator, clone_url.getFocus().id, .{ .widget = .{ .copyable_text = clone_url }, .rect = null, .min_size = .{ .width = min_width, .height = 3 }, .max_size = .{ .width = min_width, .height = 3 } });│ │754 } │ │755 } │ │764 - │ │765 - var spacer = try ui.widget.Spacer.init(allocator); │ │766 - errdefer spacer.deinit(allocator); │ │767 - try box.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │756 │ │769 - var text_box = try wgt.TextBox.init(allocator, data.content, .{ .border_style = .hidden, .wrap_kind = .none }); │ │770 - errdefer text_box.deinit(allocator); │ │771 - try box.children.put(allocator, text_box.getFocus().id, .{ .widget = .{ .text_box = text_box }, .rect = null, .min_size = null, .flex = .shrink }); │ │772 - │ │773 - return .{ .box = box, .data = data }; │ │757 + return .{ .box = box }; │ │758 } │ │759 │ │760 pub fn deinit(self: *Header.View, allocator: std.mem.Allocator) void { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────────╮ │src/ui/Repo/Files.zig│ ╰─────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │12 const Grid = xitui.grid.Grid; │ │13 const Focus = xitui.focus.Focus; │ │14 const inp = @import("../input.zig"); │ │15 +const fnd = @import("../../find.zig"); │ │16 │ │17 // how many lines of a file's content one window shows before a "next" link.│ │18 const file_page = 5000; │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │20 // budget for the total bytes of file content preloaded (windowed) per page, │ │21 // so selecting a preloaded file shows its contents without a reload. │ │22 const max_preloaded_bytes = 2 * 1024 * 1024; │ │23 + │ │24 +// how many rows a listing holds, for a directory and for search results alike.│ │25 +const max_list_entries = 1000; │ │26 │ │27 // one entry in the directory currently being viewed. │ │28 pub const Entry = struct { │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────╮ │58 // route's path named a file rather than a directory. │ │59 selected_file: ?[]const u8 = null, │ │60 entries: []const Entry, │ │57 -// the "viewing <ref> <value>" banner shown above the listing. │ │58 -header: Header, │ │61 +// true when the listing stopped at max_list_entries. │ │62 +capped: bool = false, │ │63 +// the decoded search term this listing holds the results of, or null for a│ │64 +// plain directory listing. │ │65 +find: ?[]const u8 = null, │ │66 +// whether the viewed tree has a file index, so the search box shows and a │ │67 +// find: route resolves. travels in the page json for the wasm view. │ │68 +find_available: bool = false, │ │69 │ │70 const Self = @This(); │ │71 │ ╰───────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │84 const id = evt.parseEventId(id_hex) catch return error.NotFound; │ │85 const record = (try evt.readFromRepo(evt.PatchRev, repo_kind, repo_opts, io, gpa, arena, repo, &id)) orelse return error.NotFound;│ │86 if (record.removed) return error.NotFound; │ │77 - var data = try init(repo_kind, repo_opts, arena, repo, io, gpa, location, .object, record.head_tree_oid, path, line); │ │87 + var data = try init(repo_kind, repo_opts, arena, repo, io, gpa, location, .object, record.head_tree_oid, path, line, ""); │ │88 data.patchrev_id = try arena.allocator().dupe(u8, id_hex); │ │79 - data.header.content = try std.fmt.allocPrint(arena.allocator(), "patchrev {s}", .{id_hex}); │ │89 return data; │ │90 } │ │91 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 94 .repo => |identity| ui.RoutablePage.repoPatchRevFilesRoute(identity, id, path, line), │ │ 95 .fork => null, │ │ 96 }; │ │ 88 - return self.location.filesRoute(self.ref_or_oid, self.ref_or_oid_value, path, line); │ │ 97 + const route = self.location.filesRoute(self.ref_or_oid, self.ref_or_oid_value, path, line) orelse return null;│ │ 98 + return route.withFind(self.find orelse return route); │ │ 99 } │ │100 │ │101 // build the listing for an opened repo. generic over the repo's backend and │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │114 requested_value: []const u8, │ │115 path: []const u8, │ │116 line: usize, │ │117 + find: []const u8, │ │118 ) !Self { │ │119 const aa = arena.allocator(); │ │120 + const term: ?[]const u8 = if (find.len == 0) null else std.Uri.percentDecodeInPlace(try aa.dupe(u8, find));│ │121 │ │122 // resolve the requested ref (or the default branch) to the object oid whose │ │123 // tree we list. a ref the route named explicitly that doesn't resolve is a │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯