summaryrefslogtreecommitdiff
path: root/src/edit.zig
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/edit.zig
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/edit.zig')
-rw-r--r--src/edit.zig112
1 files changed, 93 insertions, 19 deletions
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.