╭ changed ──────────────╮ │src/ui/Repo/Commits.zig│ ╰───────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │392 var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │393 errdefer outer.deinit(allocator); │ │394 │ │316 - // the clone url at the top. │ │395 + // the search box and the clone url at the top. │ │396 { │ │318 - var header_view = try Header.View.init(allocator, session, data.location); │ │397 + var header_view = try ui.widget.SearchHeader.init(allocator, session, data.location, " search ", "search", data.search, data.search_available); │ │398 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 } });│ │399 + try outer.children.put(allocator, header_view.getFocus().id, .{ .widget = .{ .search_header = header_view }, .rect = null, .min_size = .{ .width = null, .height = 3 } }); │ │400 } │ │401 │ │402 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │415 try addRow(allocator, &list_box, firstLine(commit.message), try commitRowLink(session.page_arena, data, commit, content)); │ │416 } │ │417 if (data.next_start) |next| { │ │339 - try addRow(allocator, &list_box, "next →", try commitsLink(session.page_arena, data, next, 0, "")); │ │418 + try addRow(allocator, &list_box, "next →", try nextPageLink(session.page_arena, data, next)); │ │419 } │ │420 if (list_box.children.count() > 0) list_box.getFocus().child_id = list_box.children.keys()[0]; │ │421 break :blk try wgt.Scroll(ui.Widget).init(allocator, .{ .box = list_box }, .{ .direction = .vert, .web_native = !session.is_terminal, .fill = true });│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │447 box.getFocus().child_id = box.children.keys()[list_index]; │ │448 try outer.children.put(allocator, box.getFocus().id, .{ .widget = .{ .box = box }, .rect = null, .min_size = null });│ │449 │ │371 - // focus lives in the split; the banner isn't focusable. │ │372 - outer.getFocus().child_id = outer.children.keys()[content_index]; │ │450 + // focus lives in the split, except that search results start in the │ │451 + // search box so the query can be refined right away. │ │452 + outer.getFocus().child_id = outer.children.keys()[if (data.search != null) header_index else content_index]; │ │453 │ │454 return .{ │ │455 .box = outer, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │514 return &self.box.children.values()[content_index].widget.box; │ │515 } │ │516 │ │437 - fn header(self: *View) *Header.View { │ │438 - return &self.box.children.values()[header_index].widget.repo_commits_header;│ │517 + fn header(self: *View) *ui.widget.SearchHeader { │ │518 + return &self.box.children.values()[header_index].widget.search_header; │ │519 } │ │520 │ │521 fn headerActive(self: *View) bool { │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │688 if (self.headerActive()) { │ │689 if (key == .arrow_down) { │ │690 root_focus.setFocus(self.contentBox().getFocus().id); │ │611 - } else { │ │612 - try self.header().input(allocator, key, root_focus); │ │691 + return; │ │692 } │ │614 - return; │ │693 + if (key == .enter) if (try self.header().submittedText(allocator)) |text| { │ │694 + defer allocator.free(text); │ │695 + return self.submit(text); │ │696 + }; │ │697 + return self.header().input(allocator, key, root_focus); │ │698 } │ │616 - if (key == .arrow_up and self.contentAtTop() and self.header().focusCloneUrl(root_focus)) return;│ │699 + if (key == .arrow_up and self.contentAtTop() and self.header().focusHeader(root_focus)) return; │ │700 if (self.diffActive()) { │ │701 if (key == .arrow_left and self.diffScroll().x == 0) { │ │702 self.focusList(root_focus); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │706 } else { │ │707 try self.listInput(key, root_focus); │ │708 } │ │709 + } │ │710 + │ │711 + // navigate to the typed query's results, pinned to the list's ref. an │ │712 + // empty query leaves the results for the plain log. │ │713 + fn submit(self: *View, text: []const u8) !void { │ │714 + if (text.len == 0 and self.data.search == null) return; │ │715 + const route = self.data.location.commitsRoute(self.data.ref_or_oid, self.data.ref_or_oid_value, 0, "", self.data.base_oid) orelse return;│ │716 + try self.session.navigate(route.withSearch(text) orelse return); │ │717 } │ │718 │ │719 fn listInput(self: *View, key: Key, root_focus: *Focus) !void { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │728 .enter => if (self.selectedCommitIndex() != null) │ │729 self.focusDiff(root_focus) │ │730 else if (self.data.next_start) |next| { │ │640 - if (self.data.location.commitsRoute(.object, next, 0, "", self.data.base_oid)) |route|│ │641 - try self.session.navigate(route); │ │731 + if (pageRoute(self.data, next)) |route| try self.session.navigate(route); │ │732 }, │ │733 .arrow_right => self.focusDiff(root_focus), │ │734 else => {}, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │773 return lb.children.getIndex(cid) == 0; │ │774 } │ │775 │ │686 - // only the clone-url row sits directly below the repository header. │ │776 + // only the header's own row sits directly below the repository header. │ │777 pub fn atTop(self: *View) bool { │ │688 - return self.headerActive() or (!self.header().hasCloneUrl() and self.contentAtTop()); │ │778 + return self.headerActive() or (!self.header().hasFocusable() and self.contentAtTop());│ │779 } │ │780 │ │691 - pub fn focusCloneUrl(self: *View, root_focus: *Focus) bool { │ │692 - return self.header().focusCloneUrl(root_focus); │ │781 + pub fn focusHeader(self: *View, root_focus: *Focus) bool { │ │782 + return self.header().focusHeader(root_focus); │ │783 } │ │784 }; │ │785 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │788 // to `path` ("" = every file). │ │789 fn commitsLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, oid: []const u8, start: usize, path: []const u8) ![]const u8 {│ │790 const route = data.location.commitsRoute(.object, oid, start, path, data.base_oid) orelse return error.RouteTooLong; │ │791 + const url = try route.toUrl(page_arena); │ │792 + return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │793 +} │ │794 + │ │795 +// this list's page starting at `oid`: a repo's branch or tag keeps its ref (and │ │796 +// its query) and moves `from` along, so the search box survives paging. a fork │ │797 +// or an object view roots the page at the commit itself. │ │798 +fn pageRoute(data: *const Self, oid: []const u8) ?ui.RoutablePage { │ │799 + if (data.location != .repo or data.ref_or_oid == .object) { │ │800 + return data.location.commitsRoute(.object, oid, 0, "", data.base_oid); │ │801 + } │ │802 + const route = data.location.commitsRoute(data.ref_or_oid, data.ref_or_oid_value, 0, "", data.base_oid) orelse return null; │ │803 + const paged = route.withFrom(oid) orelse return null; │ │804 + return paged.withSearch(data.search orelse return paged); │ │805 +} │ │806 + │ │807 +fn nextPageLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, oid: []const u8) ![]const u8 { │ │808 + const route = pageRoute(data, oid) orelse return error.RouteTooLong; │ │809 const url = try route.toUrl(page_arena); │ │810 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │811 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │821 // the in-page "ai:" anchor for selecting a commit with its current pane content │ │822 // and window. the href is only followed with js off. │ │823 fn commitRowLink(page_arena: *std.heap.ArenaAllocator, data: *const Self, commit: Commit, content: Content) ![]const u8 {│ │824 + // a result roots the same results page at its commit, so the query │ │825 + // survives following the row │ │826 + if (data.search != null) { │ │827 + const route = pageRoute(data, commit.oid) orelse return error.RouteTooLong; │ │828 + return std.fmt.allocPrint(page_arena.allocator(), "ai:{s}", .{try route.toUrl(page_arena)}); │ │829 + } │ │830 const route = (switch (content) { │ │831 .message => data.location.commitMessageRoute(.object, commit.oid, data.base_oid), │ │832 .diff => |diff| data.location.commitsRoute(.object, commit.oid, commit.window.start, diff.path, data.base_oid), │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯