From 778de441bc5645bf15ba8d3dff83766fbb70499e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 1 Oct 2026 20:53:19 -0300 Subject: Zerox twins survive a Dump and Restore: their text is dumped once, in the first, each other twin names it (twin_of), and Restore puts them back on one buffer with their dots Co-Authored-By: Claude Opus 5.5 --- src/File.zig | 41 +++++++++++++++++++++++++++++++++++++++++ src/dump.zig | 26 +++++++++++++++++++++++++- src/ninep/ctl.zig | 30 ++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/File.zig b/src/File.zig index 0f1579b2..e5805134 100644 --- a/src/File.zig +++ b/src/File.zig @@ -1275,6 +1275,47 @@ pub fn leaveTwins(p: *Pardes, f: *State) void { if (n == 1) p.panes[twins[0]].?.file.?.twin = 0; } +/// Restore's: pane `id`, restored on its own, back on pane `of`'s buffer +/// as the Zerox twin it was dumped as (dump.File.twin_of). Not a file pane +/// there (a dump edited by hand), it stays on its own. +pub fn relink(p: *Pardes, of: usize, id: usize, dot: ?[2]u32) void { + const src = p.panes[of] orelse return; + const pane = p.panes[id] orelse return; + const sf = if (src.file) |*file| file else return; + const f = if (pane.file) |*file| file else return; + if (sf.output != null or f.output != null or sf.mini != null) return; + if (sf.twin == 0) { + const group = p.gpa.dupe(u8, sf.path) catch return; + p.twin_groups.append(p.gpa, .{ .id = p.next_twin, .saved_revision = sf.saved_revision, .saved_hash = sf.saved_hash, .saved_len = sf.saved_len, .disk_newer = sf.disk_newer, .disk_gone = sf.disk_gone, .path = group }) catch return p.gpa.free(group); + sf.twin = p.next_twin; + p.next_twin +%= 1; + } + p.gpa.free(f.content); + if (f.line_starts.len > 0) p.gpa.free(f.line_starts); + for (f.history.undo[0..f.history.undo_len]) |snap| snap.deinit(p.gpa); + for (f.history.redo[0..f.history.redo_len]) |snap| snap.deinit(p.gpa); + p.gpa.destroy(f.history); + f.content = sf.content; + f.line_starts = sf.line_starts; + f.history = sf.history; + f.revision = sf.revision; + f.saved_revision = sf.saved_revision; + f.saved_hash = sf.saved_hash; + f.saved_len = sf.saved_len; + f.twin = sf.twin; + if (f.highlights.len > 0) p.tree_sitter_gpa.free(f.highlights); + f.highlights = &.{}; + f.syntax_dirty = true; + // Its dot as dumped, read now that its text is back. + if (dot) |d| if (d[0] <= d[1] and d[1] <= f.content.len) { + const a = modal.positionAt(f.content, d[0]); + const h = modal.positionAt(f.content, if (d[1] > d[0]) modal.prevGrapheme(f.content, d[1]) else d[0]); + pane.body.vsel = .{ .active = d[1] > d[0], .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; + pane.body.cur_row = @intCast(h.row); + pane.body.cur_col = @intCast(h.col); + }; +} + pub const TwinGroup = struct { id: u32, saved_revision: u32, diff --git a/src/dump.zig b/src/dump.zig index b64441d3..1ded9989 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -232,6 +232,9 @@ pub const File = struct { /// Its dot, q0 and q1 in bytes, as acme's dump keeps a window's; null /// in older dumps. dot: ?[2]u32 = null, + /// A Zerox twin: the index, in `panes`, of the earlier pane whose + /// buffer it shares, its text kept there once; null for none. + twin_of: ?u32 = null, }; pub const ImagePalette = enum { @@ -767,6 +770,22 @@ pub fn dumpState(p: *Pardes) !void { dump_panes[panes_len] = dp; panes_len += 1; } + // Zerox's twins: the text once, in the first; each other names it. + for (p.panes, 0..) |slot, id| { + const f = (slot orelse continue).file orelse continue; + if (f.twin == 0) continue; + const i = slot_to_pane[id] orelse continue; + for (p.panes[0..id], 0..) |earlier, j_id| { + const of = (earlier orelse continue).file orelse continue; + if (of.twin != f.twin) continue; + const j = slot_to_pane[j_id] orelse break; + if (dump_panes[i].file) |*df| { + df.twin_of = @intCast(j); + df.content_b64 = ""; + } + break; + } + } var columns: [MAX_COLS]Column = undefined; var columns_len: usize = 0; @@ -963,7 +982,12 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo if (t.petscii and pardes.panes.terminal.petscii(restored) == false) pardes.panes.terminal.togglePetscii(restored); break :terminal restored; }, - .file => try pardes.panes.File.restore(p, i, src), + .file => file: { + const restored = try pardes.panes.File.restore(p, i, src); + // A twin: on the earlier pane's buffer again. + if (src.file.?.twin_of) |j| if (j < i) pardes.panes.File.relink(p, j, i, src.file.?.dot); + break :file restored; + }, .image => restore_image: { const im = src.image.?; if (pardes.panes.Pdf.isPath(im.path)) { diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 973f87e2..ae68ed91 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1995,6 +1995,36 @@ test "a Get file that fails changes nothing: no such file, a directory, or unsav try testing.expectEqualStrings("here\nmore\n", pane.file.?.content); } +test "Zerox twins dump their text once and come back on one buffer from a Restore" { + const p = try withFile(testing.allocator, "one\ntwo\n"); + defer p.deinit(); + const first = serialOf(p); + _ = wr(p, Node.of(first, .name), "/tmp/pardes-zerox-dump.txt\n"); + _ = wr(p, Node.of(first, .body), "unsaved " ** 64 ++ "\n"); + try testing.expect(p.executeBuiltinLine(0, "Zerox")); + try testing.expect(p.executeBuiltinLine(0, "Zerox")); + try pardes.dump.dumpState(p); + const text = p.dump_out.?; + // The text is kept once though three panes show it. + const encoded = try pardes.dump.encodeBytes(p.scratch.allocator(), p.panes[0].?.file.?.content); + try testing.expectEqual(@as(usize, 1), std.mem.count(u8, text, encoded)); + const restored = try pardes.dump.restore(p, text, "/tmp/pardes.dump.zon"); + defer restored.deinit(); + var files: [3]*pardes.panes.File.State = undefined; + var n: usize = 0; + for (restored.panes) |slot| if (slot) |pane| if (pane.file) |*f| if (std.mem.eql(u8, f.path, "/tmp/pardes-zerox-dump.txt")) { + files[n] = f; + n += 1; + }; + try testing.expectEqual(@as(usize, 3), n); + for (files[1..]) |f| { + try testing.expectEqual(files[0].content.ptr, f.content.ptr); + try testing.expectEqual(files[0].history, f.history); + try testing.expect(f.twin != 0 and f.twin == files[0].twin); + } + try testing.expect(std.mem.startsWith(u8, files[0].content, "one\ntwo\nunsaved ")); +} + test "a body write to a terminal is typed input, not a paste, even to a program that asked for bracketed paste" { const p = try Pardes.init(testing.allocator, .{ .cols = 90, .rows = 30 }); defer p.deinit(); -- cgit v1.3