summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 15:59:00 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit767aefd1e1cfd79e2a240a59d2d16bfb326f062d (patch)
tree113c66e3abda3548056fc480d7c7eeedbdaeb025 /src
parentab1c353f65efe8741b9a4d1aaef065af0aee885a (diff)
downloadpardes-767aefd1e1cfd79e2a240a59d2d16bfb326f062d.tar.gz
pardes-767aefd1e1cfd79e2a240a59d2d16bfb326f062d.zip
Registers hold a value per range, and "<reg> names one
Registers.zig replaces the one yank buffer: every register keeps a value per range, y fills them in document order, and p, P, R and insert Ctrl-r put value i at range i, repeating the last (helix paste_impl). "<reg> names the register for the next command; _ swallows, # numbers the ranges, . is each range's text, % the file's name, / the last s/S pattern, and + and * are the system clipboard through the ClipYank and ClipPaste paths. SPC y now writes + alone, as helix's does. The acme chords and a paste into a terminal take the default register joined by newlines. The msel-yank-paste waiver is gone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/Registers.zig106
-rw-r--r--src/Text.zig2
-rw-r--r--src/config.zig2
-rw-r--r--src/edit.zig112
-rw-r--r--src/gui/gui.zig2
-rw-r--r--src/look.zig6
-rw-r--r--src/modal.zig13
-rw-r--r--src/mouse.zig12
-rw-r--r--src/normal.zig17
-rw-r--r--src/pardes.zig22
10 files changed, 258 insertions, 36 deletions
diff --git a/src/Registers.zig b/src/Registers.zig
new file mode 100644
index 00000000..d2a42434
--- /dev/null
+++ b/src/Registers.zig
@@ -0,0 +1,106 @@
+//! helix's registers: named stores of text, each holding one VALUE PER RANGE
+//! of the selection that wrote it. `"` is the default, `+` the one the
+//! system clipboard is copied from, `/` the last s/S pattern, `@` the last
+//! macro; any other character names a register of its own. The registers
+//! computed from the editor rather than stored (`_`, `#`, `.`, `%`) and the
+//! clipboard's paste side are read in edit.zig, which has the pane at hand.
+const std = @import("std");
+
+const Registers = @This();
+
+list: std.ArrayList(Reg) = .empty,
+
+pub const Reg = struct {
+ name: u21,
+ /// gpa-owned, one per range, in document order
+ values: [][]u8,
+ /// the values joined by newlines, for the readers that take one text
+ /// (the acme chords, a paste into a terminal, the clipboard); gpa-owned,
+ /// made on first read
+ joined: ?[]u8 = null,
+};
+
+pub const default = '"';
+
+pub fn deinit(r: *Registers, gpa: std.mem.Allocator) void {
+ for (r.list.items) |*reg| freeReg(gpa, reg);
+ r.list.deinit(gpa);
+ r.* = .{};
+}
+
+fn freeReg(gpa: std.mem.Allocator, reg: *Reg) void {
+ for (reg.values) |v| gpa.free(v);
+ gpa.free(reg.values);
+ if (reg.joined) |j| gpa.free(j);
+}
+
+pub fn get(r: *Registers, name: u21) ?*Reg {
+ for (r.list.items) |*reg| if (reg.name == name) return reg;
+ return null;
+}
+
+/// Value `i` of register `name`: a register holding fewer values than there
+/// are ranges repeats its last (helix paste_impl).
+pub fn value(r: *Registers, name: u21, i: usize) ?[]const u8 {
+ const reg = r.get(name) orelse return null;
+ if (reg.values.len == 0) return null;
+ return reg.values[@min(i, reg.values.len - 1)];
+}
+
+/// Store value `i` of `n`. `fresh` starts the register over with `n` empty
+/// values; the replay across ranges passes it on its first pass only, so
+/// every range fills its own slot.
+pub fn put(r: *Registers, gpa: std.mem.Allocator, name: u21, bytes: []const u8, i: usize, n: usize, fresh: bool) void {
+ const copy = gpa.dupe(u8, bytes) catch return;
+ const reg = r.get(name) orelse blk: {
+ r.list.append(gpa, .{ .name = name, .values = &.{} }) catch {
+ gpa.free(copy);
+ return;
+ };
+ break :blk &r.list.items[r.list.items.len - 1];
+ };
+ if (fresh or reg.values.len != n) {
+ const values = gpa.alloc([]u8, n) catch {
+ gpa.free(copy);
+ return;
+ };
+ for (values) |*v| v.* = &.{};
+ freeReg(gpa, reg);
+ reg.* = .{ .name = name, .values = values };
+ }
+ gpa.free(reg.values[i]);
+ reg.values[i] = copy;
+ if (reg.joined) |j| gpa.free(j);
+ reg.joined = null;
+}
+
+/// The whole register as one text: its values joined by newlines, no
+/// newline added after a value that already ends in one.
+pub fn text(r: *Registers, gpa: std.mem.Allocator, name: u21) ?[]const u8 {
+ const reg = r.get(name) orelse return null;
+ if (reg.joined) |j| return j;
+ var out: std.ArrayList(u8) = .empty;
+ for (reg.values, 0..) |v, i| {
+ if (i > 0 and !std.mem.endsWith(u8, reg.values[i - 1], "\n")) out.append(gpa, '\n') catch return null;
+ out.appendSlice(gpa, v) catch return null;
+ }
+ reg.joined = out.toOwnedSlice(gpa) catch return null;
+ return reg.joined;
+}
+
+test "a register holds a value per range and joins them for one-text readers" {
+ const gpa = std.testing.allocator;
+ var r: Registers = .{};
+ defer r.deinit(gpa);
+ try std.testing.expect(r.value(default, 0) == null);
+ // a replay fills the last range first
+ r.put(gpa, default, "two", 1, 2, true);
+ r.put(gpa, default, "one\n", 0, 2, false);
+ try std.testing.expectEqualStrings("one\n", r.value(default, 0).?);
+ try std.testing.expectEqualStrings("two", r.value(default, 1).?);
+ try std.testing.expectEqualStrings("two", r.value(default, 5).?);
+ try std.testing.expectEqualStrings("one\ntwo", r.text(gpa, default).?);
+ r.put(gpa, 'a', "x", 0, 1, true);
+ try std.testing.expectEqualStrings("x", r.text(gpa, 'a').?);
+ try std.testing.expectEqualStrings("one\ntwo", r.text(gpa, default).?);
+}
diff --git a/src/Text.zig b/src/Text.zig
index 43dd8a95..7ad243a1 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -66,6 +66,8 @@ select: bool = false,
/// non-vertical range write resets it to -1.
sticky_col: i32 = -1,
append_at: ?struct { row: i32, col: i32 } = null,
+/// insert `Ctrl-r` was pressed: the next key names the register to insert
+reg_wait: bool = false,
normal: modal.Normal.State = .{},
/// last f/F/t/T motion, for Alt-. repeat
find_op: u8 = 0,
diff --git a/src/config.zig b/src/config.zig
index 85a6bc59..4412ef0d 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -492,6 +492,7 @@ pub const match_around = 'a';
pub const surround_add = 's';
pub const surround_replace = 'r';
pub const surround_delete = 'd';
+pub const register_prefix = '"';
pub const goto_paragraph: []const Chord = &.{.{ .cp = 'p' }};
pub const add_newline: []const Chord = &.{.{ .cp = ' ' }};
@@ -557,6 +558,7 @@ pub const escape: []const Chord = &.{.{ .cp = Key.escape }};
pub const insert_backspace_alias: []const Chord = &.{.{ .cp = 'h', .ctrl = true }};
pub const insert_enter_alias: []const Chord = &.{.{ .cp = 'j', .ctrl = true }};
pub const insert_delete_alias: []const Chord = &.{.{ .cp = 'd', .ctrl = true }};
+pub const insert_register: []const Chord = &.{.{ .cp = 'r', .ctrl = true }};
pub const delete_word_backward: []const Chord = &.{ .{ .cp = 'w', .ctrl = true }, .{ .cp = Key.backspace, .alt = true } };
pub const delete_word_forward: []const Chord = &.{ .{ .cp = 'd', .alt = true }, .{ .cp = Key.delete, .alt = true } };
pub const kill_to_line_start: []const Chord = &.{.{ .cp = 'u', .ctrl = true }};
diff --git a/src/edit.zig b/src/edit.zig
index 24354df8..14d8b7ed 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -26,23 +26,75 @@ const hit = pardes.hit;
const sel_slot = pardes.sel_slot;
const Effect = pardes.Effect;
const Pardes = pardes.Pardes;
+const Registers = pardes.Registers;
+/// Write `text` as this range's value of the register the running command
+/// names (`"<reg>`), the default one otherwise. `_` swallows it, and the
+/// registers computed from the editor (`#`, `.`, `%`) cannot be written.
+/// `+` and `*` are the system clipboard: the value is sent there too.
pub fn setYank(p: *Pardes, text: []const u8) void {
- if (p.multi_on and !p.multi_first) {
- const old = p.yank orelse "";
- const sep: []const u8 = if (text.len > 0 and text[text.len - 1] == '\n') "" else "\n";
- const joined = std.fmt.allocPrint(p.gpa, "{s}{s}{s}", .{ text, sep, old }) catch return;
- if (p.yank) |y| p.gpa.free(y);
- p.yank = joined;
- return;
+ var name: u21 = if (p.register != 0) p.register else Registers.default;
+ switch (name) {
+ '_', '#', '.', '%' => return,
+ '*' => name = '+',
+ else => {},
}
- if (p.yank) |y| p.gpa.free(y);
- p.yank = p.gpa.dupe(u8, text) catch null;
+ const one = !p.multi_on;
+ p.registers.put(p.gpa, name, text, if (one) 0 else p.multi_index, if (one) 1 else p.multi_count, one or p.multi_first);
+ if (name == '+' and (one or p.multi_first)) p.emit(.{ .set_clipboard = {} });
}
pub fn setClipboard(p: *Pardes, text: []const u8) void {
+ const named = p.register;
+ p.register = '+';
+ defer p.register = named;
setYank(p, text);
- if (!p.multi_on or p.multi_first) p.emit(.{ .set_clipboard = {} });
+}
+
+/// Value `i` of register `name` as the command at range `i` reads it, the
+/// computed registers included: `#` is the range's number from 1, `.` its
+/// text, `%` the file's name, `_` nothing. Scratch- or register-owned.
+pub fn registerValue(p: *Pardes, t: *Text, name: u21) ?[]const u8 {
+ const i = if (p.multi_on) p.multi_index else 0;
+ return switch (name) {
+ '_' => null,
+ '#' => std.fmt.allocPrint(p.scratch.allocator(), "{d}", .{i + 1}) catch null,
+ '.' => selText(p, t),
+ '%' => if (t.what == .body) if (t.pane()) |pane| (if (pane.file) |f| f.path else null) else null else null,
+ else => p.registers.value(name, i),
+ };
+}
+
+/// the text of the range under the cursor: the selection, or the cursor's
+/// own character
+fn selText(p: *Pardes, t: *Text) []const u8 {
+ if (t.msel.active) return yankRows(p, t, @min(t.msel.r0, t.msel.r1), @max(t.msel.r0, t.msel.r1));
+ const save = t.vsel;
+ defer t.vsel = save;
+ if (!t.vsel.active) t.vsel = .{ .active = true, .row = t.cur_row, .col = t.cur_col, .explicit = false };
+ return vselText(p, t);
+}
+
+/// `p` / `P` / `R` from the register the command names: this range's value
+/// of it. The clipboard's is asked of the shell and pasted when it answers,
+/// once, at the primary.
+pub fn pasteRegister(p: *Pardes, t: *Text, how: enum { after, before, replace }, count: usize) void {
+ const name: u21 = if (p.register != 0) p.register else Registers.default;
+ if (name == '+' or name == '*') {
+ if (!normal.multiOnce(p)) return;
+ const pane = t.pane() orelse return;
+ return clipRequest(p, p.paneIdOf(pane) orelse return, switch (how) {
+ .after => .after,
+ .before => .before,
+ .replace => .replace,
+ });
+ }
+ const y = registerValue(p, t, name) orelse return;
+ switch (how) {
+ .after => pasteText(p, t, y, false, count),
+ .before => pasteText(p, t, y, true, count),
+ .replace => replaceWithText(p, t, y),
+ }
}
/// Where a `SPC p` / `SPC P` / `SPC R` goes once the shell answers.
@@ -125,6 +177,10 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void {
if (pv.selection_text.len > 0) setClipboard(p, pv.selection_text);
return;
};
+ // helix's `"+y`: the clipboard register, and only it
+ const named = p.register;
+ p.register = '+';
+ defer p.register = named;
if (pane.body.nsel > 0 and !main_only) {
normal.replaySels(p, &pane.body, .{ .normal = .{ .edit = .{ .kind = .yank, .count = 1 } } });
} else {
@@ -133,7 +189,6 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void {
normalYank(p, &pane.body);
pane.body.nsel = others;
}
- p.emit(.{ .set_clipboard = {} });
}
/// tag + '\n' + body: the full selectable pane text, a row to a screen row
@@ -286,6 +341,7 @@ pub fn currentSelText(p: *Pardes, pane: *Pane) ?[]const u8 {
pub fn exitInsert(p: *Pardes, t: *Text) void {
t.mode = .normal;
+ t.reg_wait = false;
t.msel.active = false;
t.normal.clear();
if (t.append_at) |aa| {
@@ -565,6 +621,19 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
if (hit(key, config.insert_backspace_alias)) return insertKey(p, t, .{ .cp = Key.backspace });
if (hit(key, config.insert_enter_alias)) return insertKey(p, t, .{ .cp = Key.enter });
if (hit(key, config.insert_delete_alias)) return insertKey(p, t, .{ .cp = Key.delete });
+ // Ctrl-r <reg>: type this range's value of the register (helix
+ // insert_register)
+ if (t.reg_wait) {
+ t.reg_wait = false;
+ if (key.ctrl or key.alt or key.cp >= 0xF0000) return;
+ const y = registerValue(p, t, key.cp) orelse return;
+ if (y.len > 0) insertKey(p, t, .{ .cp = key.cp, .text = y });
+ return;
+ }
+ if (hit(key, config.insert_register)) {
+ t.reg_wait = true;
+ return;
+ }
t.vsel.active = false;
if (!t.cur_pinned) pinPaneCursor(p, t);
_ = clampFileCursor(t);
@@ -1132,8 +1201,9 @@ pub fn normalReplaceChar(p: *Pardes, t: *Text, ch: u21) void {
/// `R`: replace the selection (or the cursor char) with the DEFAULT
/// register. `SPC R` is the same verb over the system clipboard.
-pub fn normalReplaceYank(p: *Pardes, t: *Text) void {
- replaceWithText(p, t, p.yank orelse return);
+/// The acme chords' replace: the default register as one text.
+pub fn chordReplace(p: *Pardes, t: *Text) void {
+ replaceWithText(p, t, p.registers.text(p.gpa, Registers.default) orelse return);
}
/// The pasted text becomes the selection, in the direction the replaced one
@@ -1957,16 +2027,20 @@ test "only the SPC clipboard commands cross to the system clipboard" {
_ = drainedEffect(p, .set_clipboard);
p.update(.{ .key = .{ .cp = 'y' } });
- try std.testing.expect(p.yank != null and p.yank.?.len > 0);
+ const plain = p.registers.text(gpa, Registers.default) orelse return error.MissingYank;
+ try std.testing.expect(plain.len > 0);
try std.testing.expect(!drainedEffect(p, .set_clipboard));
p.update(.{ .key = .{ .cp = 'd' } });
try std.testing.expect(!drainedEffect(p, .set_clipboard));
- // `SPC y` is the one that does, and it puts the SAME text there that `y`
- // put in the register — it is the ordinary yank path plus the mirror.
+ // `SPC y` is the one that does: helix's `"+y`, the ordinary yank into
+ // the clipboard register instead of the default one.
p.update(.{ .key = .{ .cp = ' ' } });
p.update(.{ .key = .{ .cp = 'y' } });
- const yanked = p.yank orelse return error.MissingYank;
+ const yanked = try gpa.dupe(u8, p.registers.text(gpa, Registers.default) orelse "");
+ defer gpa.free(yanked);
+ const clip: []const u8 = p.registers.text(gpa, '+') orelse "";
+ try std.testing.expect(clip.len > 0);
try std.testing.expect(drainedEffect(p, .set_clipboard));
const before = pane.file.?.content.len;
@@ -1980,14 +2054,14 @@ test "only the SPC clipboard commands cross to the system clipboard" {
try std.testing.expect(std.mem.indexOf(u8, pane.file.?.content, "PASTED") != null);
// ...and it did NOT land in the register on its way past, nor echo back
// out to the clipboard it came from.
- try std.testing.expectEqualStrings(yanked, p.yank orelse "");
+ try std.testing.expectEqualStrings(yanked, p.registers.text(gpa, Registers.default) orelse "");
try std.testing.expect(!drainedEffect(p, .set_clipboard));
// An UNSOLICITED paste — the outer terminal's bracketed paste, a Cmd-V —
// is the same event with no request behind it, and means paste after.
p.update(.{ .paste = "UNASKED" });
try std.testing.expect(std.mem.indexOf(u8, pane.file.?.content, "UNASKED") != null);
- try std.testing.expectEqualStrings(yanked, p.yank orelse "");
+ try std.testing.expectEqualStrings(yanked, p.registers.text(gpa, Registers.default) orelse "");
// A request the shell never answers dies at the next keystroke rather
// than firing late into whatever pane is focused by then.
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 46bd5ffc..8f6248ce 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -4385,7 +4385,7 @@ test "the headless grid host round-trips a yank back as a paste" {
core.update(.{ .key = .{ .cp = ' ' } });
core.update(.{ .key = .{ .cp = 'y' } });
while (core.nextEffect()) |e| core.perform(e);
- const yanked = core.yank orelse return error.MissingYank;
+ const yanked = core.registers.text(core.gpa, '+') orelse return error.MissingYank;
try std.testing.expect(yanked.len > 0);
const before = pane.file.?.content.len;
diff --git a/src/look.zig b/src/look.zig
index d7e763d3..5e2d6068 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -502,7 +502,11 @@ const SearchStart = enum { top, cursor };
pub fn submitSearch(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
- if (normal.selRegexArmed(pane)) |a| return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| {
+ // helix keeps the pattern in `/`
+ if (a.pat.len > 0) p.registers.put(p.gpa, '/', a.pat, 0, 1, true);
+ return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split);
+ }
const armed = pane.promptText() orelse return;
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return;
if (std.mem.startsWith(u8, armed, config.rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]);
diff --git a/src/modal.zig b/src/modal.zig
index aadaab00..aec1c579 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -15,6 +15,7 @@ pub const Normal = struct {
prefix_replace,
prefix_next,
prefix_prev,
+ prefix_register,
goto_file_start,
goto_last_line,
@@ -160,6 +161,7 @@ pub const Normal = struct {
replace,
next,
prev,
+ register,
};
pub const MatchSub = enum(u8) {
@@ -176,6 +178,8 @@ pub const Normal = struct {
prefix: Prefix = .none,
match_sub: MatchSub = .none,
held_char: u21 = 0,
+ /// `"<reg>`: the register the next command names, 0 for none
+ register: u21 = 0,
pub fn clear(state: *State) void {
state.* = .{};
@@ -491,6 +495,14 @@ pub const Normal = struct {
if (key.has(.goto_diagnostic_end)) return resultAction(.{ .diagnostic = .{ .direction = direction, .endpoint = true } });
return .ignored;
},
+ .register => {
+ state.prefix = .none;
+ const char = key.literal() orelse return .ignored;
+ state.register = char;
+ // the count typed before `"` is still the command's
+ state.count = if (counted.explicit) count else 0;
+ return .pending;
+ },
.none => {},
}
@@ -505,6 +517,7 @@ pub const Normal = struct {
if (key.has(.prefix_next)) return armPrefix(state, .next, if (counted.explicit) count else 0);
if (key.has(.prefix_prev)) return armPrefix(state, .prev, if (counted.explicit) count else 0);
if (key.has(.prefix_match)) return armPrefix(state, .match, 0);
+ if (key.has(.prefix_register)) return armPrefix(state, .register, if (counted.explicit) count else 0);
if (key.has(.move_left)) return resultAction(.{ .move = .{ .motion = .left, .count = count } });
if (key.has(.move_right)) return resultAction(.{ .move = .{ .motion = .right, .count = count } });
diff --git a/src/mouse.zig b/src/mouse.zig
index f222b5da..91ec41a6 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -963,8 +963,8 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
if (cut) {
if (t.vsel.active) edit.normalDelete(p, t, true);
} else if (t.vsel.active) {
- edit.normalReplaceYank(p, t);
- } else edit.pasteText(p, t, p.yank orelse return, true, 1);
+ edit.chordReplace(p, t);
+ } else edit.pasteText(p, t, p.registers.text(p.gpa, pardes.Registers.default) orelse return, true, 1);
return;
}
if (pane.body.mode == .tty) {
@@ -973,7 +973,7 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
pane.sel[sel_slot].state = .none; // drop the sweep highlight
}
if (cut) return;
- const y = p.yank orelse return;
+ const y = p.registers.text(p.gpa, pardes.Registers.default) orelse return;
if (y.len == 0) return;
if (panes.Terminal.reportsMouse(pane)) {
const col: u16 = @intCast(std.math.clamp(pane.sel[sel_slot].c1 + 1, 1, 9999));
@@ -1016,11 +1016,11 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
if (cut) {
if (pane.body.vsel.active) edit.normalDelete(p, &pane.body, true);
} else if (pane.body.vsel.active) {
- edit.normalReplaceYank(p, &pane.body);
+ edit.chordReplace(p, &pane.body);
} else if (pane.file) |*f| {
// bare cursor: splice the register in literally (no linewise
// magic) so a paste right after a cut restores the text exactly
- const y = p.yank orelse return;
+ const y = p.registers.text(p.gpa, pardes.Registers.default) orelse return;
if (y.len == 0) return;
const at = modal.Cursor{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) };
edit.pushUndo(p, &pane.body);
@@ -1039,7 +1039,7 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
pane.body.cur_pinned = true;
pane.body.sticky_col = -1;
pane.ensureCursorVisible();
- } else edit.pasteText(p, &pane.body, p.yank orelse return, true, 1);
+ } else edit.pasteText(p, &pane.body, p.registers.text(p.gpa, pardes.Registers.default) orelse return, true, 1);
}
test "plain left click clears explicit modal selection" {
diff --git a/src/normal.zig b/src/normal.zig
index 5a6c1cd5..f5e868a4 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -35,6 +35,7 @@ fn roleBindingName(comptime role: modal.Normal.Role) []const u8 {
.prefix_replace => "replace_prefix",
.prefix_next => "next_prefix",
.prefix_prev => "prev_prefix",
+ .prefix_register => "register_prefix",
else => @tagName(role),
};
}
@@ -149,6 +150,7 @@ const KeyState = struct {
find_op: u8,
find_ch: u21,
append_at: @FieldType(Text, "append_at"),
+ reg_wait: bool,
fn of(t: *Text) KeyState {
return .{
@@ -158,6 +160,7 @@ const KeyState = struct {
.find_op = t.find_op,
.find_ch = t.find_ch,
.append_at = t.append_at,
+ .reg_wait = t.reg_wait,
};
}
@@ -168,6 +171,7 @@ const KeyState = struct {
t.find_op = s.find_op;
t.find_ch = s.find_ch;
t.append_at = s.append_at;
+ t.reg_wait = s.reg_wait;
}
};
@@ -227,6 +231,8 @@ pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void {
while (i > 0) {
i -= 1;
p.multi_first = passes == 0;
+ p.multi_index = i;
+ p.multi_count = n;
passes += 1;
saved.into(t);
t.nsel = 0; // the handler underneath sees ONE selection
@@ -284,11 +290,16 @@ pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void {
}
pub fn handleNormal(p: *Pardes, t: *Text, key: Key) void {
+ // `"<reg>` is held by the parser until the command it names arrives
+ const named = t.normal.register;
const parsed = modal.Normal.parse(&t.normal, normalInput(key));
const semantic = switch (parsed) {
.pending, .ignored, .unbound => return,
.action => |value| value,
};
+ t.normal.register = 0;
+ p.register = named;
+ defer p.register = 0;
if (t.nsel == 0 or semantic.scope() == .once)
return executeNormalAction(p, t, semantic);
replaySels(p, t, .{ .normal = semantic });
@@ -534,9 +545,9 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void
.delete_noyank => return edit.normalDelete(p, t, false),
.change => return edit.normalChange(p, t),
.yank => return edit.normalYank(p, t),
- .replace_with_yank => return edit.normalReplaceYank(p, t),
- .paste_after => return edit.pasteText(p, t, p.yank orelse return, false, op.count),
- .paste_before => return edit.pasteText(p, t, p.yank orelse return, true, op.count),
+ .replace_with_yank => return edit.pasteRegister(p, t, .replace, op.count),
+ .paste_after => return edit.pasteRegister(p, t, .after, op.count),
+ .paste_before => return edit.pasteRegister(p, t, .before, op.count),
.switch_case => return edit.normalCase(p, t, .toggle),
.lowercase => return edit.normalCase(p, t, .lower),
.uppercase => return edit.normalCase(p, t, .upper),
diff --git a/src/pardes.zig b/src/pardes.zig
index 9821839f..e4645d70 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -17,6 +17,7 @@ pub const normal = @import("normal.zig");
pub const edit = @import("edit.zig");
pub const body_layer = @import("body_layer.zig");
pub const Messages = @import("Messages.zig");
+pub const Registers = @import("Registers.zig");
pub const Pipe = @import("Pipe.zig");
pub const colors = @import("colors.zig");
pub const Theme = colors.Theme;
@@ -453,6 +454,7 @@ test {
_ = @import("regexp.zig");
_ = @import("edit.zig");
_ = @import("Messages.zig");
+ _ = @import("Registers.zig");
_ = @import("selection_pipe.zig");
_ = @import("Pipe.zig");
_ = @import("colors.zig");
@@ -2326,7 +2328,7 @@ test "a host with no methods at all is a complete in-process pardes" {
try std.testing.expectEqual(before, p.panes[0].?.terminal.?.vt.screens.active.cursor.y);
// the clipboard round-trips through the in-process one
- p.yank = try gpa.dupe(u8, "copied");
+ p.registers.put(gpa, '+', "copied", 0, 1, true);
p.perform(.set_clipboard);
try std.testing.expectEqualStrings("copied", p.fallback.clipboard.items);
@@ -4003,6 +4005,14 @@ pub const Pardes = struct {
multi_on: bool = false,
multi_first: bool = false,
multi_stop: bool = false,
+ /// While a key is replayed across the ranges: which range this pass is,
+ /// in document order, and how many there are. A register value belongs
+ /// to its range by this index.
+ multi_index: usize = 0,
+ multi_count: usize = 1,
+ /// The register the running normal command names (`"<reg>`); 0 for the
+ /// default one.
+ register: u21 = 0,
leader_on: bool = false,
leader_keys: [4]u8 = undefined,
leader_n: u8 = 0,
@@ -4085,7 +4095,7 @@ pub const Pardes = struct {
in_head: usize = 0,
in_len: usize = 0,
- yank: ?[]u8 = null,
+ registers: Registers = .{},
clip_pending: ?edit.ClipRequest = null,
/// the last serialized dump (gpa-owned), read by the write_dump effect
dump_out: ?[]u8 = null,
@@ -4256,7 +4266,7 @@ pub const Pardes = struct {
const gpa = p.gpa;
for (&p.col_tags) |*t| t.deinit(gpa);
p.global_tag.deinit(gpa);
- if (p.yank) |y| gpa.free(y);
+ p.registers.deinit(gpa);
if (p.dump_out) |d| gpa.free(d);
p.fallback.deinit();
if (p.last_dump) |d| gpa.free(d);
@@ -4826,7 +4836,7 @@ pub const Pardes = struct {
}
},
.set_clipboard => {
- const text = p.yank orelse "";
+ const text = p.registers.text(p.gpa, '+') orelse "";
if (v.set_clipboard) |f| f(p.host.ctx, text) else p.fallback.setClipboard(text);
},
// No desktop to ask: answer from the in-process clipboard at once,
@@ -5162,7 +5172,7 @@ pub const Pardes = struct {
// in it the chord is the program's -- vim's visual block,
// readline's quoted-insert -- and swallowing it would make
// Ctrl-V a black hole in every full-screen application.
- if (p.yank) |text| return edit.typeToTty(p, p.active, pane, text);
+ if (p.registers.text(p.gpa, Registers.default)) |text| return edit.typeToTty(p, p.active, pane, text);
}
return panes.Terminal.forwardKey(p, p.active, key);
}
@@ -5310,7 +5320,7 @@ pub const Pardes = struct {
switch (t.mode) {
.normal => normal.handleNormal(p, t, key),
.insert => {
- if (hit(key, config.escape)) return edit.exitInsert(p, t);
+ if (hit(key, config.escape) and !t.reg_wait) return edit.exitInsert(p, t);
edit.handleInsert(p, t, key);
},
.tty => panes.Terminal.forwardKey(p, p.active, key),