From 2580dd5ea85d86262c94363dfea9f990eaf56196 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 19:21:22 -0300 Subject: Everything a B1 chord does while B1 is down is one undo step B1-B2 then B1-B3 cut and pasted as two undo steps. acme marks the file once per B1 hold (text.c:881, textselect), so the pair is a copy. The chord now takes one undo point when it begins and its edits share it until B1 comes up. Co-Authored-By: Claude Opus 5.5 --- docs/selections.md | 6 ++++++ src/edit.zig | 3 ++- src/mouse.zig | 6 ++++++ src/pardes.zig | 2 ++ test/panes.zig | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/docs/selections.md b/docs/selections.md index 4e26f5ab..7ec58f2b 100644 --- a/docs/selections.md +++ b/docs/selections.md @@ -135,3 +135,9 @@ counted; at a line's start or end the whole line; just inside `' " \`` the quoted text; anywhere else the word (letters, digits, `_` and any non-ASCII character). It works in bodies, tags and a terminal in normal mode; a terminal whose program has the tty gets its own clicks. + +A B2 or B3 pressed while B1 holds a sweep is acme's chord: B1-B2 cuts, +B1-B3 pastes over it. Everything done while B1 stays down is one undo +step, as acme marks the file once per B1 hold (text.c:881, textselect), so +B1-B2 then B1-B3 is a copy: the text cut and pasted back, the cut text +left in the register. diff --git a/src/edit.zig b/src/edit.zig index 8d3a746e..1e480785 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -2012,7 +2012,8 @@ pub fn pushUndo(p: *Pardes, t: *Text) void { // one keystroke, one undo step — even when it edited at ten cursors. if (p.multi_on and !p.multi_first) return; switch (t.what) { - .body => {}, + // A B1 chord took its one undo point when it began (mouse.zig). + .body => if (p.chord_hold) return, .tag, .column, .workspace => return t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null), // a prompt's answer is typed once and gone; it keeps no history .input => return, diff --git a/src/mouse.zig b/src/mouse.zig index ac3e206b..61bd30e0 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -790,6 +790,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { // stream; only the button which began this gesture may end it. if (p.drag.button() != button) return; defer p.drag = .none; + defer p.chord_hold = false; switch (p.drag) { .border_v => |d| { const c = d.left_col; @@ -1027,6 +1028,11 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { } if (!s.chorded) { s.chorded = true; + // Everything done while B1 stays down is one undo step, as acme + // marks the file once per B1 hold (text.c:881 textselect, `state` + // and its one filemark): B1-B2 then B1-B3 is a copy. + edit.pushUndo(p, &pane.body); + p.chord_hold = true; const sl = pane.sel[sel_slot]; pane.sel[sel_slot].state = .none; const pfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; diff --git a/src/pardes.zig b/src/pardes.zig index 60d0f51b..20d46dc3 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4118,6 +4118,8 @@ pub const Pardes = struct { /// The last plain B1 click, for acme's double-click (text.c:881, /// textselect: a second click at the same point within 500 ms). last_click: ?struct { id: usize, on_tag: bool, row: i32, col: i32, ms: i64 } = null, + /// A B1 chord's edits share the undo point it took (mouse.zig). + chord_hold: bool = false, look_hover_wait: ?look.LookHoverWait = null, look_hover_preview: ?look.LookHoverPreview = null, pdf_hover_preview: if (pdf_enabled) ?look.PdfWordPreview else void = if (pdf_enabled) null else {}, diff --git a/test/panes.zig b/test/panes.zig index 166e12c9..d8dab298 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -1057,6 +1057,44 @@ const ClickCursorTests = struct { p.update(.{ .mouse = .{ .button = button, .kind = .release, .col = x, .row = y } }); } + test "everything a B1 chord does while B1 is down is one undo step" { + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); + defer p.deinit(); + const pane = try p.setTestFile("one two three\n"); + var frame = std.heap.ArenaAllocator.init(p.gpa); + defer frame.deinit(); + _ = try p.render(frame.allocator()); + const rect = p.rects[0]; + const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); + const y = p.bodyTop(pane, rect); + const M = struct { + fn ev(pp: *Pardes, b: pardes.Mouse.Button, k: pardes.Mouse.Kind, cx: u16, cy: u16) void { + pp.update(.{ .mouse = .{ .button = b, .kind = k, .col = cx, .row = cy } }); + } + }; + // B1-B2 then B1-B3: cut, then paste back: a copy, one step. + const before = pane.file.?.history.undo_len; + M.ev(p, .left, .press, x + 4, y); + M.ev(p, .left, .drag, x + 6, y); + M.ev(p, .middle, .press, x + 6, y); + M.ev(p, .middle, .release, x + 6, y); + M.ev(p, .right, .press, x + 6, y); + M.ev(p, .right, .release, x + 6, y); + M.ev(p, .left, .release, x + 6, y); + try std.testing.expectEqualStrings("one two three\n", pane.file.?.content); + try std.testing.expectEqualStrings("two", p.registers.text(p.gpa, '"').?); + try std.testing.expect(pane.file.?.history.undo_len <= before + 1); + // A cut alone comes back with one Undo. + M.ev(p, .left, .press, x + 4, y); + M.ev(p, .left, .drag, x + 6, y); + M.ev(p, .middle, .press, x + 6, y); + M.ev(p, .middle, .release, x + 6, y); + M.ev(p, .left, .release, x + 6, y); + try std.testing.expectEqualStrings("one three\n", pane.file.?.content); + try std.testing.expect(p.executeBuiltinLine(0, "Undo")); + try std.testing.expectEqualStrings("one two three\n", pane.file.?.content); + } + test "a double-click selects a word, a line, a bracketed or quoted text, as acme's" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 12 }); defer p.deinit(); -- cgit v1.3