diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 19:50:32 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 19:57:27 -0300 |
| commit | dce94f4453735a6f71385cc020ee79c7604609e7 (patch) | |
| tree | 266e031b92a282187467f5e35c1b0824b4b7a5f2 /src/ninep | |
| parent | 2868516c215a96d9e1e933ded7ac5744ec16c368 (diff) | |
| download | pardes-dce94f4453735a6f71385cc020ee79c7604609e7.tar.gz pardes-dce94f4453735a6f71385cc020ee79c7604609e7.zip | |
Zerox opens a second pane on a file pane's buffer: one text, undo and unsaved state, a scroll and a cursor each; an edit in either is spliced into both at File.setContentSpan, O(the edit) more, and what a Save, a get, the disk watch or a rename changes on one is copied to the rest after each step
Twins share the content buffer, the History and, while it is one, the line
index; the other twin's cursor and scroll move past the splice as acme
moves a clone's q0. Del of one twin asks nothing while another holds the
text, Exit lists the text once, and a closed twin keeps a copy of its own
until it is torn down. Both are in the index, each its serial, one name.
An event reader on one hears no edit made through another (first cut).
A dump keeps the twins as two panes on the file, not linked again.
perf: edit-twin, typing beside a twin of the 50k-line file, gated within
3x of edit-char in the same run; the three baselines re-recorded for it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep')
| -rw-r--r-- | src/ninep/ctl.zig | 56 |
1 files changed, 55 insertions, 1 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 188795de..537ac4d0 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1829,6 +1829,60 @@ test "Putall saves every pane with unsaved edits to a file, one each, and leaves try testing.expectEqual(@as(usize, 1), saves); } +test "Zerox makes a second pane on one buffer: one text, undo and dirty state, its own cursor; Save cleans both, Del of one asks nothing" { + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + var dir_buf: [4096]u8 = undefined; + const dir = dir_buf[0..try tmp.dir.realPath(testing.io, &dir_buf)]; + const p = try withFile(testing.allocator, "one\ntwo\nthree\n"); + defer p.deinit(); + const first = serialOf(p); + var name: [std.fs.max_path_bytes + 8]u8 = undefined; + _ = wr(p, Node.of(first, .name), try std.fmt.bufPrint(&name, "{s}/z.txt\n", .{dir})); + try testing.expect(p.executeBuiltinLine(0, "Zerox")); + const twin_id = for (p.panes, 0..) |slot, id| { + if (id != 0) if (slot) |pane| if (pane.file != null) break id; + } else return error.NoTwin; + const a = p.panes[0].?; + const b = p.panes[twin_id].?; + // Both in the index, each its serial, one name. + const index = rd(p, @intFromEnum(tree.TopFile.index), 0, 4096).bytes; + try testing.expectEqual(@as(usize, 2), std.mem.count(u8, index, "/z.txt")); + // b's cursor on `three`; a line written above it in a moves it down. + b.body.cur_row = 2; + b.body.cur_col = 1; + _ = wr(p, Node.of(first, .addr), "0"); + try testing.expectEqual(Status.ok, wr(p, Node.of(first, .data), "zero\n").reply.status); + try testing.expectEqualStrings("zero\none\ntwo\nthree\n", b.file.?.content); + try testing.expectEqual(a.file.?.content.ptr, b.file.?.content.ptr); + try testing.expectEqual(@as(i32, 3), b.body.cur_row); + try testing.expectEqual(@as(i32, 1), b.body.cur_col); + try testing.expect(pane_files.dirtyOf(a) and pane_files.dirtyOf(b)); + // A write through b is a's too; Undo in a takes it back from both. + try testing.expectEqual(Status.ok, wr(p, Node.of(b.serial, .body), "four\n").reply.status); + try testing.expectEqualStrings("zero\none\ntwo\nthree\nfour\n", a.file.?.content); + try testing.expect(p.executeBuiltinLine(0, "Undo")); + try testing.expectEqualStrings("zero\none\ntwo\nthree\n", b.file.?.content); + // Saved from b: both come clean. + try testing.expect(p.executeBuiltinLine(twin_id, "Save")); + while (p.nextEffect()) |e| p.perform(e); + try testing.expect(!pane_files.dirtyOf(a) and !pane_files.dirtyOf(b)); + try testing.expectEqualStrings("zero\none\ntwo\nthree\n", p.fallback.get(b.file.?.path).?); + // Edited again, Del of a asks nothing: b holds the text. + try testing.expectEqual(Status.ok, wr(p, Node.of(b.serial, .body), "five\n").reply.status); + const failures = p.fs.failures; + try testing.expect(p.executeBuiltinLine(0, "Del")); + p.sync(); + try testing.expectEqual(failures, p.fs.failures); + try testing.expect(p.paneBySerial(first) == null); + try testing.expect(pane_files.dirtyOf(b)); + try testing.expectEqualStrings("zero\none\ntwo\nthree\nfive\n", b.file.?.content); + // Alone now, it is no twin; Del asks about it as about any pane. + try testing.expectEqual(@as(u32, 0), b.file.?.twin); + try testing.expect(p.executeBuiltinLine(p.paneBySerial(b.serial).?, "Del")); + try testing.expect(p.fs.failures != failures); +} + 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(); @@ -3211,7 +3265,7 @@ test "acme's words run as pardes's (Put is Save, Delete a Del that does not ask) const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); const serial = serialOf(p); - for ([_][]const u8{ "Snarf\n", "Zerox\n", "Indent\n", "Local\n", "Abort\n" }) |line| { + for ([_][]const u8{ "Snarf\n", "Indent\n", "Local\n", "Abort\n" }) |line| { for ([_]u64{ Node.of(serial, .ctl), Node.of(serial, .exec) }) |node| { const r = wr(p, node, line); try testing.expectEqual(E.INVAL, r.errno()); |
