summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 20:53:19 -0300
committerGabriel Schneider <[email protected]>2026-10-01 20:53:19 -0300
commit778de441bc5645bf15ba8d3dff83766fbb70499e (patch)
treed6ebd1a202abc2ff5574db8ca11c417d466919eb
parent081e1b9ab9d3bb8afaae8ea040c7767cb6e4f863 (diff)
downloadpardes-778de441bc5645bf15ba8d3dff83766fbb70499e.tar.gz
pardes-778de441bc5645bf15ba8d3dff83766fbb70499e.zip
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 <[email protected]>
-rw-r--r--src/File.zig41
-rw-r--r--src/dump.zig26
-rw-r--r--src/ninep/ctl.zig30
3 files changed, 96 insertions, 1 deletions
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();