╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭─────────────────────────────────────────────╮ │38 link: []const u8, │ │39 form: []const u8, │ │40 buttons: []const DetailButton = &.{},│ │41 + undone: bool = false, │ │42 }; │ │43 │ │44 identity: []const u8, │ ╰─────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │59 var items: std.ArrayList(Item) = .empty; │ │60 var remaining = if (selected) |index| index + 1 else count; │ │61 var buffer: [opts.max_read_size]u8 = undefined; │ │62 + │ │63 + // an undo above the page discards transactions on it, so the walk starts │ │64 + // at the newest transaction. an undo discards everything from the one it │ │65 + // restored to down to the row below itself, so walking down, that row is │ │66 + // all the walk has to carry. the rows it discarded are skipped, since a │ │67 + // discarded undo discards nothing itself │ │68 + var undone_from: ?u64 = null; │ │69 + var scanned = count; │ │70 + while (scanned > remaining) { │ │71 + scanned -= 1; │ │72 + if (undone_from) |from| if (scanned >= from) { │ │73 + scanned = from; │ │74 + continue; │ │75 + }; │ │76 + const record = try xit.undo.read(opts, try repo.core.momentAt(scanned), &buffer);│ │77 + if (try undoneFrom(arena, record)) |from| undone_from = from; │ │78 + } │ │79 + │ │80 while (remaining > 0 and items.items.len < page_size) { │ │81 remaining -= 1; │ │82 const moment = try repo.core.momentAt(remaining); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │83 const record = try xit.undo.read(opts, moment, &buffer); │ │84 + const row_undone = if (undone_from) |from| remaining >= from else false; │ │85 + if (!row_undone) { │ │86 + if (try undoneFrom(arena, record)) |from| undone_from = from; │ │87 + } │ │88 const detail = if (record) |value| try eventDetail(opts, arena, &repo.core, haxy_moment, identity, value, moment, remaining) else null;│ │89 const shown = detail orelse Detail{ │ │90 .action = if (record) |value| try aa.dupe(u8, value.action_kind) else "unknown", │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │100 .timestamp = if (record) |value| try formatTimestamp(aa, value.timestamp) else "timestamp unavailable", │ │101 .link = try std.fmt.allocPrint(aa, "ai:{s}", .{url}), │ │102 .form = try std.fmt.allocPrint(aa, "form:{s}/undo", .{url}), │ │103 + .undone = row_undone, │ │104 }); │ │105 } │ │106 return .{ .identity = try aa.dupe(u8, identity), .count = count, .items = items.items, .next = if (remaining > 0) remaining - 1 else null };│ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │249 return try ids.count(); │ │250 } │ │251 │ │252 +// the oldest transaction `record` discarded, or null when it is not an undo │ │253 +fn undoneFrom(arena: *std.heap.ArenaAllocator, record: ?xit.undo.Record) !?u64 { │ │254 + const value = record orelse return null; │ │255 + if (!std.mem.eql(u8, value.action_kind, undo_action_kind)) return null; │ │256 + const parsed = std.json.parseFromSlice(xit.undo.Undo, arena.child_allocator, value.payload, .{ .ignore_unknown_fields = true }) catch |err| switch (err) {│ │257 + error.OutOfMemory => return err, │ │258 + else => return null, │ │259 + }; │ │260 + defer parsed.deinit(); │ │261 + return parsed.value.index; │ │262 +} │ │263 + │ │264 // the moment's own position in the haxy history │ │265 fn momentIndex(comptime opts: rp.RepoOpts(.xit), haxy_moment: evt.EventDB(opts.hash).HashMap(.read_only)) !?u64 { │ │266 const cursor = try haxy_moment.getCursor(hash.hashInt(opts.hash, evt.moment_index_key)) orelse return null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │382 var scroll = blk: { │ │383 var rows = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert, .stretch = true }); │ │384 errdefer rows.deinit(allocator); │ │349 - for (data.items) |item| try addText(allocator, &rows, item.action, item.link, .hidden); │ │385 + for (data.items) |item| { │ │386 + try addText(allocator, &rows, item.action, item.link, .hidden); │ │387 + // a transaction a later undo discarded says so on its border │ │388 + if (item.undone) rows.children.values()[rows.children.count() - 1].widget.text_box.options.bottom_label = "(undone)"; │ │389 + } │ │390 if (data.next) |next| { │ │391 const route = ui.RoutablePage.repoUndoRoute(data.identity, next) orelse return error.RouteTooLong; │ │392 try addText(allocator, &rows, "next →", try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}), .hidden);│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯