╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────╮ │src/ui/Repo/Refs.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │38 }; │ │39 │ │40 // empty columns, for the wasm / no-repo paths. │ │39 -pub fn emptyResult(arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, from: []const u8) !Self { │ │41 +pub fn emptyResult(arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, from: []const u8, search: []const u8) !Self {│ │42 + const aa = arena.allocator(); │ │43 return .{ │ │41 - .identity = try arena.allocator().dupe(u8, identity), │ │44 + .identity = try aa.dupe(u8, identity), │ │45 .kind = kind, │ │43 - .from = try arena.allocator().dupe(u8, from), │ │46 + .from = try aa.dupe(u8, from), │ │47 + .search = if (search.len == 0) null else std.Uri.percentDecodeInPlace(try aa.dupe(u8, search)), │ │48 .branches = .{ .label = try ui.SubTitle.init(arena, "branches") }, │ │49 .tags = .{ .label = try ui.SubTitle.init(arena, "tags") }, │ │50 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │62 identity: []const u8, │ │63 kind: ui.RoutablePage.RefKind, │ │64 from: []const u8, │ │65 + search: []const u8, │ │66 ) !Self { │ │67 const aa = arena.allocator(); │ │63 - var result = try emptyResult(arena, identity, kind, from); │ │68 + var result = try emptyResult(arena, identity, kind, from, search); │ │69 + const prefix = result.search orelse ""; │ │70 // the window root only applies to its own column; a ref name arrives │ │71 // url-encoded, and the iterator seeks to the first name at or after it.│ │72 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, from)); │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │75 [_]*Column{ &result.branches, &result.tags }, │ │76 ) |col_kind, column| { │ │77 const col_from: []const u8 = if (kind == col_kind) decoded else ""; │ │72 - // an unreadable listing leaves the column empty │ │73 - var iter = listRefs(repo_kind, repo_opts, repo, io, gpa, col_kind, iterStart(col_from)) catch continue; │ │78 + // an unreadable listing leaves the column empty. the names are sorted, │ │79 + // so those with the prefix are contiguous from the prefix itself. │ │80 + var iter = listRefs(repo_kind, repo_opts, repo, io, gpa, col_kind, iterStart(laterKey(prefix, col_from))) catch continue;│ │81 defer iter.deinit(); │ │82 var names = try std.ArrayListUnmanaged([]const u8).initCapacity(aa, page_size); │ │76 - while (names.items.len < page_size) { │ │77 - const ref = try iter.next() orelse break; │ │83 + while (try iter.next()) |ref| { │ │84 + if (!std.mem.startsWith(u8, ref.name, prefix)) break; │ │85 + if (names.items.len == page_size) { │ │86 + column.next = try aa.dupe(u8, ref.name); │ │87 + break; │ │88 + } │ │89 names.appendAssumeCapacity(try aa.dupe(u8, ref.name)); │ │90 } │ │91 column.names = names.items; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │81 - column.next = if (try iter.next()) |ref| try aa.dupe(u8, ref.name) else null; │ │92 if (col_from.len != 0) { │ │83 - var prev_iter = listRefs(repo_kind, repo_opts, repo, io, gpa, col_kind, .beginning) catch continue; │ │93 + var prev_iter = listRefs(repo_kind, repo_opts, repo, io, gpa, col_kind, iterStart(prefix)) catch continue;│ │94 defer prev_iter.deinit(); │ │85 - column.prev = try prevRoot(aa, &prev_iter, col_from); │ │95 + column.prev = try prevRoot(aa, &prev_iter, prefix, col_from); │ │96 } │ │97 } │ │98 return result; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │117 return if (from.len == 0) .beginning else .{ .key = from }; │ │118 } │ │119 │ │110 -// the root of the window before the one starting at `from` (decoded): the ref │ │111 -// a page before it ("" = the first window), or null when nothing precedes it. │ │112 -fn prevRoot(aa: std.mem.Allocator, iter: anytype, from: []const u8) !?[]const u8 { │ │120 +// the later of two ref names, so a window never starts before the prefix │ │121 +fn laterKey(a: []const u8, b: []const u8) []const u8 { │ │122 + return if (std.mem.lessThan(u8, a, b)) b else a; │ │123 +} │ │124 + │ │125 +// the root of the window before the one starting at `from` (decoded) among the │ │126 +// names with `prefix`: the ref a page before it ("" = the first window), or null │ │127 +// when nothing precedes it. `iter` starts at the prefix. │ │128 +fn prevRoot(aa: std.mem.Allocator, iter: anytype, prefix: []const u8, from: []const u8) !?[]const u8 {│ │129 var ring: [page_size][]const u8 = undefined; │ │130 var count: usize = 0; │ │131 while (try iter.next()) |ref| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │116 - if (!std.mem.lessThan(u8, ref.name, from)) break; │ │132 + if (!std.mem.startsWith(u8, ref.name, prefix) or !std.mem.lessThan(u8, ref.name, from)) break;│ │133 ring[count % page_size] = try aa.dupe(u8, ref.name); │ │134 count += 1; │ │135 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │139 } │ │140 │ │141 pub const View = struct { │ │126 - // a horizontal box of two columns, each a fixed label above a Scroll of a│ │127 - // TagFlow, so the names wrap across the column and scroll beneath the │ │128 - // label. focus points at the active column and, in its flow, the selected│ │129 - // name. │ │142 + // a vertical box: the search sub-header above a horizontal box of two │ │143 + // columns. each column is a fixed label above a │ │144 + // Scroll of a TagFlow, so the names wrap across the column and scroll │ │145 + // beneath the label. focus points at the header or the active column and,│ │146 + // in its flow, the selected name. │ │147 box: wgt.Box(ui.Widget), │ │148 + data: *const Self, │ │149 session: *ui.Session, │ │150 │ │151 + const header_index = 0; │ │152 + const columns_index = 1; │ │153 const left_col = 0; │ │154 const right_col = 1; │ │155 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │156 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !View { │ │137 - var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │138 - errdefer box.deinit(allocator); │ │157 + var outer = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │158 + errdefer outer.deinit(allocator); │ │159 │ │140 - try addColumn(allocator, &box, session, data.identity, .branch, &data.branches); │ │141 - try addColumn(allocator, &box, session, data.identity, .tag, &data.tags); │ │160 + // both backends list refs in sorted order, so a prefix is just a seek │ │161 + { │ │162 + var header = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │163 + errdefer header.deinit(allocator); │ │164 + { │ │165 + var search_box = try ui.widget.SearchBox.init(allocator, session, " search ", "search", data.search); │ │166 + errdefer search_box.deinit(allocator); │ │167 + header.getFocus().child_id = search_box.getFocus().id; │ │168 + try header.children.put(allocator, search_box.getFocus().id, .{ .widget = .{ .search_box = search_box }, .rect = null, .min_size = ui.widget.SearchBox.min_size });│ │169 + } │ │170 + // the spacer keeps the box at its own width │ │171 + { │ │172 + var spacer = try ui.widget.Spacer.init(allocator); │ │173 + errdefer spacer.deinit(allocator); │ │174 + try header.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); │ │175 + } │ │176 + // a row taller than the box, leaving a blank line above the columns │ │177 + try outer.children.put(allocator, header.getFocus().id, .{ .widget = .{ .box = header }, .rect = null, .min_size = .{ .width = null, .height = 4 } }); │ │178 + } │ │179 + │ │180 + { │ │181 + var split = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │182 + errdefer split.deinit(allocator); │ │183 │ │143 - // select the first name of the first column that has one │ │144 - for (box.children.keys(), box.children.values()) |id, *child| { │ │145 - if (child.widget.box.getFocus().child_id != null) { │ │146 - box.getFocus().child_id = id; │ │147 - break; │ │184 + const search = data.search orelse ""; │ │185 + try addColumn(allocator, &split, session, data.identity, search, .branch, &data.branches); │ │186 + try addColumn(allocator, &split, session, data.identity, search, .tag, &data.tags); │ │187 + │ │188 + // select the first name of the first column that has one │ │189 + for (split.children.keys(), split.children.values()) |id, *child| { │ │190 + if (child.widget.box.getFocus().child_id != null) { │ │191 + split.getFocus().child_id = id; │ │192 + break; │ │193 + } │ │194 } │ │195 + try outer.children.put(allocator, split.getFocus().id, .{ .widget = .{ .box = split }, .rect = null, .min_size = null }); │ │196 } │ │150 - return .{ .box = box, .session = session }; │ │197 + │ │198 + // search results start in the box so the term can be refined right away │ │199 + outer.getFocus().child_id = outer.children.keys()[if (data.search != null) header_index else columns_index]; │ │200 + return .{ .box = outer, .data = data, .session = session }; │ │201 } │ │202 │ │203 fn addColumn( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────╮ │205 box: *wgt.Box(ui.Widget), │ │206 session: *ui.Session, │ │207 identity: []const u8, │ │208 + search: []const u8, │ │209 kind: ui.RoutablePage.RefKind,│ │210 data: *const Column, │ │211 ) !void { │ ╰───────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │237 { │ │238 var items: std.ArrayList(ui.widget.TagFlow.Item) = .empty; │ │239 defer items.deinit(allocator); │ │189 - if (data.prev) |p| try items.append(allocator, .{ .text = "← previous", .link = try windowLink(session.page_arena, identity, kind, p) }); │ │240 + if (data.prev) |p| try items.append(allocator, .{ .text = "← previous", .link = try windowLink(session.page_arena, identity, kind, p, search) });│ │241 for (data.names) |name| try items.append(allocator, .{ .text = name, .link = try refLink(session.page_arena, identity, kind, name) }); │ │191 - if (data.next) |n| try items.append(allocator, .{ .text = "next →", .link = try windowLink(session.page_arena, identity, kind, n) }); │ │242 + if (data.next) |n| try items.append(allocator, .{ .text = "next →", .link = try windowLink(session.page_arena, identity, kind, n, search) }); │ │243 │ │244 var scroll = blk: { │ │245 var flow = try ui.widget.TagFlow.init(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯