summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 19:38:59 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit181170ff00a2019465b6401047407d9914b3b631 (patch)
tree6b49f04a1efe29491fa9d4cf23c3aecf5df84197 /src
parentb2886edda1a1630f38732975c04bdf4d6f6ded29 (diff)
downloadpardes-181170ff00a2019465b6401047407d9914b3b631.tar.gz
pardes-181170ff00a2019465b6401047407d9914b3b631.zip
An edit that changes nothing, an empty range deleted or a range replaced by what it holds, makes no undo step
A shell's : > data on an empty address, echo abc > data over abc, or Edit s/d/d/ each pushed an undo step and a new revision, so Undo spent a press on nothing and the pane read as edited. The splice of the 9P writes and the Edit builtin now see the text is unchanged and stop. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/builtins.zig7
-rw-r--r--src/ninep/pane.zig38
2 files changed, 43 insertions, 2 deletions
diff --git a/src/builtins.zig b/src/builtins.zig
index edeb7ac8..cb588ba3 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -1140,8 +1140,11 @@ pub const Edit = struct {
}
if (res.ops.len > 0) {
const new = sam.apply(c.p.gpa, f.content, res.ops) catch |err| return c.p.reportError(c.id, "Edit", err);
- panes.File.pushUndo(c.p, c.pane);
- panes.File.setContent(c.p, f, new);
+ // `s/a/a/` changes nothing: no undo step, no new revision.
+ if (std.mem.eql(u8, new, f.content)) c.p.gpa.free(new) else {
+ panes.File.pushUndo(c.p, c.pane);
+ panes.File.setContent(c.p, f, new);
+ }
}
ninep_pane.setDot(c.pane, sam.moveDot(res.dot, res.ops));
if (res.printed.len > 0) _ = ninep_pane.appendErrors(c.p, c.id, res.printed);
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 71484538..ce6d0422 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -290,6 +290,9 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const
const take = if (bytes.len == 0) 0 else wholeUtf8(bytes);
const lo = @min(q0, f.content.len);
const hi = @max(lo, @min(q1, f.content.len));
+ // An empty range deleted, or a range replaced by what it holds: no
+ // edit, so no undo step and no new revision.
+ if (std.mem.eql(u8, f.content[lo..hi], bytes[0..take])) return take;
const new = p.gpa.alloc(u8, f.content.len - (hi - lo) + take) catch return null;
@memcpy(new[0..lo], f.content[0..lo]);
@memcpy(new[lo..][0..take], bytes[0..take]);
@@ -301,6 +304,41 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const
return take;
}
+test "a write that changes nothing, an empty range deleted or a range replaced by itself, makes no undo step" {
+ const p = try withFile(testing.allocator, "abc\ndef\n");
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ const serial = pane.serial;
+ const f = &pane.file.?;
+ const steps = f.history.undo_len;
+ const revision = f.revision;
+ const data = Node.of(serial, .data);
+ // Each as a shell does it, on an open of its own: `: > data` after
+ // an empty address, then `echo abc > data` over the line it holds.
+ _ = wr(p, Node.of(serial, .addr), "#2,#2");
+ const h = call(p, .{ .tag = 1, .op = .open, .node = data, .omode = 1 }).reply.handle;
+ try testing.expectEqual(tree.Status.ok, call(p, .{ .tag = 2, .op = .setattr, .node = data, .truncate = true }).reply.status);
+ _ = call(p, .{ .tag = 3, .op = .release, .node = data, .handle = h, .opened = true });
+ for ([_][]const u8{ "abc\n", "abd\n" }) |text| {
+ _ = wr(p, Node.of(serial, .addr), "1");
+ const w = call(p, .{ .tag = 4, .op = .open, .node = data, .omode = 1 }).reply.handle;
+ try testing.expectEqual(tree.Status.ok, call(p, .{ .tag = 5, .op = .write, .node = data, .handle = w, .data = text }).reply.status);
+ _ = call(p, .{ .tag = 6, .op = .release, .node = data, .handle = w, .opened = true });
+ if (text[2] == 'c') {
+ try testing.expectEqualStrings("abc\ndef\n", f.content);
+ try testing.expectEqual(steps, f.history.undo_len);
+ try testing.expectEqual(revision, f.revision);
+ }
+ }
+ // Edit's `s/d/d/` is the same nothing.
+ const undone = f.history.undo_len;
+ try testing.expectEqual(tree.Status.ok, wr(p, Node.of(serial, .ctl), "Edit ,s/d/d/\n").reply.status);
+ try testing.expectEqual(undone, f.history.undo_len);
+ // A real change is still one.
+ try testing.expectEqualStrings("abd\ndef\n", f.content);
+ try testing.expectEqual(steps + 1, f.history.undo_len);
+}
+
test "rows through the line index are the ones counting newlines gives, at every offset" {
const p = try withFile(testing.allocator, "héllo\nwo\u{301}rld\n\n last line é\ntail");
defer p.deinit();