╭ changed ───────────╮ │src/ui/Repo/Refs.zig│ ╰────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │16 │ │17 // "owner/name", needed to build the columns' window-navigation links. │ │18 identity: []const u8, │ │19 -// only one column paginates at a time: `after` is the window start of `kind`'s │ │20 -// column; the other column always starts at 0. │ │19 +// only one column windows at a time: `from` (a url-encoded ref name, "" = the │ │20 +// first window) roots `kind`'s column; the other always shows its first window.│ │21 kind: ui.RoutablePage.RefKind, │ │22 -after: usize, │ │23 -// the current window of each column, plus the next window's start (null at the │ │24 -// end) so the view can decide whether to show a "next →" row. │ │25 -branches: []const []const u8, │ │26 -tags: []const []const u8, │ │27 -branches_next_after: ?usize, │ │28 -tags_next_after: ?usize, │ │29 -// the columns' headers, in the half-height SubTitle font. │ │30 -branches_label: ui.SubTitle, │ │31 -tags_label: ui.SubTitle, │ │22 +from: []const u8, │ │23 +branches: Column, │ │24 +tags: Column, │ │25 │ │26 const Self = @This(); │ │27 │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │35 -const Window = struct { names: []const []const u8, next_after: ?usize }; │ │28 +// one ref column: the current window of names, the raw ref names its │ │29 +// "← previous" / "next →" rows window from (null = no row; a "" prev means │ │30 +// the first window), and the header label. │ │31 +pub const Column = struct { │ │32 + names: []const []const u8 = &.{}, │ │33 + prev: ?[]const u8 = null, │ │34 + next: ?[]const u8 = null, │ │35 + label: ui.SubTitle, │ │36 +}; │ │37 │ │38 // empty columns, for the wasm / no-repo paths. │ │38 -pub fn emptyResult(arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, after: usize) !Self { │ │39 +pub fn emptyResult(arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, from: []const u8) !Self {│ │40 return .{ │ │41 .identity = try arena.allocator().dupe(u8, identity), │ │42 .kind = kind, │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │42 - .after = after, │ │43 - .branches = &.{}, │ │44 - .tags = &.{}, │ │45 - .branches_next_after = null, │ │46 - .tags_next_after = null, │ │47 - .branches_label = try ui.SubTitle.init(arena, "branches"), │ │48 - .tags_label = try ui.SubTitle.init(arena, "tags"), │ │43 + .from = try arena.allocator().dupe(u8, from), │ │44 + .branches = .{ .label = try ui.SubTitle.init(arena, "branches") },│ │45 + .tags = .{ .label = try ui.SubTitle.init(arena, "tags") }, │ │46 }; │ │47 } │ │48 │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 57 gpa: std.mem.Allocator, │ │ 58 identity: []const u8, │ │ 59 kind: ui.RoutablePage.RefKind, │ │ 63 - after: usize, │ │ 60 + from: []const u8, │ │ 61 ) !Self { │ │ 62 const aa = arena.allocator(); │ │ 66 - // the offset only applies to its own column; the other stays at 0. │ │ 67 - const branches_after: usize = if (kind == .branch) after else 0; │ │ 68 - const tags_after: usize = if (kind == .tag) after else 0; │ │ 69 - var result = try emptyResult(arena, identity, kind, after); │ │ 70 - │ │ 71 - var branch_iter = repo.listBranches(io, gpa, .{ .index = branches_after }) catch return result; │ │ 72 - defer branch_iter.deinit(); │ │ 73 - const branches = try collectWindow(aa, &branch_iter, branches_after); │ │ 63 + var result = try emptyResult(arena, identity, kind, from); │ │ 64 + // the window root only applies to its own column; a ref name arrives │ │ 65 + // url-encoded, and the iterator seeks to the first name at or after it. │ │ 66 + const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, from)); │ │ 67 + for ( │ │ 68 + [_]ui.RoutablePage.RefKind{ .branch, .tag }, │ │ 69 + [_]*Column{ &result.branches, &result.tags }, │ │ 70 + ) |col_kind, column| { │ │ 71 + 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;│ │ 74 + defer iter.deinit(); │ │ 75 + 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; │ │ 78 + names.appendAssumeCapacity(try aa.dupe(u8, ref.name)); │ │ 79 + } │ │ 80 + column.names = names.items; │ │ 81 + column.next = if (try iter.next()) |ref| try aa.dupe(u8, ref.name) else null; │ │ 82 + if (col_from.len != 0) { │ │ 83 + var prev_iter = listRefs(repo_kind, repo_opts, repo, io, gpa, col_kind, .beginning) catch continue;│ │ 84 + defer prev_iter.deinit(); │ │ 85 + column.prev = try prevRoot(aa, &prev_iter, col_from); │ │ 86 + } │ │ 87 + } │ │ 88 + return result; │ │ 89 +} │ │ 90 │ │ 75 - var tag_iter = repo.listTags(io, gpa, .{ .index = tags_after }) catch return result; │ │ 76 - defer tag_iter.deinit(); │ │ 77 - const tags = try collectWindow(aa, &tag_iter, tags_after); │ │ 91 +fn listRefs( │ │ 92 + comptime repo_kind: rp.RepoKind, │ │ 93 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │ 94 + repo: *rp.Repo(repo_kind, repo_opts), │ │ 95 + io: std.Io, │ │ 96 + gpa: std.mem.Allocator, │ │ 97 + kind: ui.RoutablePage.RefKind, │ │ 98 + start: xit.ref.RefIteratorStart, │ │ 99 +) !xit.ref.RefIterator(repo_kind, repo_opts) { │ │100 + return switch (kind) { │ │101 + .branch => repo.listBranches(io, gpa, start), │ │102 + .tag => repo.listTags(io, gpa, start), │ │103 + }; │ │104 +} │ │105 │ │ 79 - result.branches = branches.names; │ │ 80 - result.tags = tags.names; │ │ 81 - result.branches_next_after = branches.next_after; │ │ 82 - result.tags_next_after = tags.next_after; │ │ 83 - return result; │ │106 +fn iterStart(from: []const u8) xit.ref.RefIteratorStart { │ │107 + return if (from.len == 0) .beginning else .{ .key = from }; │ │108 } │ │109 │ │ 86 -// collect one window from a ref iterator already seeked to the window start │ │ 87 -fn collectWindow(aa: std.mem.Allocator, iter: anytype, after: usize) !Window { │ │ 88 - var names = try std.ArrayListUnmanaged([]const u8).initCapacity(aa, page_size); │ │ 89 - while (names.items.len < page_size) { │ │ 90 - const ref = try iter.next() orelse break; │ │ 91 - names.appendAssumeCapacity(try aa.dupe(u8, ref.name)); │ │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 { │ │113 + var ring: [page_size][]const u8 = undefined; │ │114 + var count: usize = 0; │ │115 + while (try iter.next()) |ref| { │ │116 + if (!std.mem.lessThan(u8, ref.name, from)) break; │ │117 + ring[count % page_size] = try aa.dupe(u8, ref.name); │ │118 + count += 1; │ │119 } │ │ 93 - const has_more = (try iter.next()) != null; │ │ 94 - return .{ .names = names.items, .next_after = if (has_more) after + page_size else null }; │ │120 + if (count == 0) return null; │ │121 + if (count <= page_size) return ""; │ │122 + return ring[count % page_size]; │ │123 } │ │124 │ │125 pub const View = struct { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │138 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │139 errdefer box.deinit(allocator); │ │140 │ │113 - // the offset only applies to its own column; the other starts at 0. │ │114 - const branches_after: usize = if (data.kind == .branch) data.after else 0; │ │115 - const tags_after: usize = if (data.kind == .tag) data.after else 0; │ │116 - try addColumn(allocator, &box, session, data.identity, .branch, &data.branches_label, data.branches, branches_after, data.branches_next_after);│ │117 - try addColumn(allocator, &box, session, data.identity, .tag, &data.tags_label, data.tags, tags_after, data.tags_next_after); │ │141 + try addColumn(allocator, &box, session, data.identity, .branch, &data.branches); │ │142 + try addColumn(allocator, &box, session, data.identity, .tag, &data.tags); │ │143 │ │144 var self = View{ .box = box, .data = data }; │ │145 // select the first row of the first column that has one. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │158 session: *ui.Session, │ │159 identity: []const u8, │ │160 kind: ui.RoutablePage.RefKind, │ │136 - label: *const ui.SubTitle, │ │137 - names: []const []const u8, │ │138 - after: usize, │ │139 - next_after: ?usize, │ │161 + data: *const Column, │ │162 ) !void { │ │163 var column = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert });│ │164 errdefer column.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │176 try header_box.children.put(allocator, space.getFocus().id, .{ .widget = .{ .text = space }, .rect = null, .min_size = null }); │ │177 } │ │178 { │ │157 - var header = try ui.SubTitle.View.init(allocator, label); │ │179 + var header = try ui.SubTitle.View.init(allocator, &data.label); │ │180 errdefer header.deinit(allocator); │ │181 try header_box.children.put(allocator, header.getFocus().id, .{ .widget = .{ .sub_title = header }, .rect = null, .min_size = null });│ │182 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │192 │ │193 // window-navigation rows bracket the names: "← previous" off the │ │194 // first window, "next →" when more remain. each is a full reload. │ │173 - if (after > 0) try addRow(allocator, &rows, "← previous", try windowLink(session.page_arena, identity, kind, after -| page_size));│ │195 + if (data.prev) |p| try addRow(allocator, &rows, "← previous", try windowLink(session.page_arena, identity, kind, p)); │ │196 // each ref name links to the files tab at that ref's root. │ │175 - for (names) |name| try addRow(allocator, &rows, name, try refLink(session.page_arena, identity, kind, name)); │ │176 - if (next_after) |na| try addRow(allocator, &rows, "next →", try windowLink(session.page_arena, identity, kind, na)); │ │197 + for (data.names) |name| try addRow(allocator, &rows, name, try refLink(session.page_arena, identity, kind, name)); │ │198 + if (data.next) |n| try addRow(allocator, &rows, "next →", try windowLink(session.page_arena, identity, kind, n)); │ │199 │ │200 if (rows.children.count() > 0) rows.getFocus().child_id = rows.children.keys()[0]; │ │201 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │347 } │ │348 }; │ │349 │ │328 -// the "a:" link to `kind`'s column paginated to `after` within `identity` │ │329 -// ("owner/name"). the other column resets to its first window. │ │330 -fn windowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, after: usize) ![]const u8 { │ │331 - const route = ui.RoutablePage.repoRefsRoute(identity, kind, after) orelse return error.RouteTooLong; │ │350 +// the "a:" link to `kind`'s column windowed from ref `name` (raw; "" = the │ │351 +// first window) within `identity` ("owner/name"). the other column resets to │ │352 +// its first window. │ │353 +fn windowLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, kind: ui.RoutablePage.RefKind, name: []const u8) ![]const u8 {│ │354 + const encoded = try ui.urlEncodeRef(page_arena.allocator(), name); │ │355 + const route = ui.RoutablePage.repoRefsRoute(identity, kind, encoded) orelse return error.RouteTooLong; │ │356 const url = try route.urlAlloc(page_arena); │ │357 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{url}); │ │358 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯