diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Registers.zig | 106 | ||||
| -rw-r--r-- | src/Text.zig | 2 | ||||
| -rw-r--r-- | src/config.zig | 2 | ||||
| -rw-r--r-- | src/edit.zig | 112 | ||||
| -rw-r--r-- | src/gui/gui.zig | 2 | ||||
| -rw-r--r-- | src/look.zig | 6 | ||||
| -rw-r--r-- | src/modal.zig | 13 | ||||
| -rw-r--r-- | src/mouse.zig | 12 | ||||
| -rw-r--r-- | src/normal.zig | 17 | ||||
| -rw-r--r-- | src/pardes.zig | 22 |
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), |
