summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/selections.md6
-rw-r--r--src/edit.zig3
-rw-r--r--src/mouse.zig6
-rw-r--r--src/pardes.zig2
-rw-r--r--test/panes.zig38
5 files changed, 54 insertions, 1 deletions
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();