diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:20:24 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 3992e9def8692710590deacbc20bd925e0f8d64a (patch) | |
| tree | ab47f821d993903fedef10425eda547e26ae82b8 /src/Macro.zig | |
| parent | 06fe2290db7d0eb654b9a908358942cfde298e29 (diff) | |
| download | pardes-3992e9def8692710590deacbc20bd925e0f8d64a.tar.gz pardes-3992e9def8692710590deacbc20bd925e0f8d64a.zip | |
Q records keys into a register and q types them again; . repeats the last insert
Macro.zig keeps keys to be typed again. Q records into @ (or "<reg>) in
helix's key notation until the next Q; q replays a register's keys count
times through the modal handling, where Esc does not hop panes and Enter,
Tab and Space neither look, execute nor open the leader. . replays the
normal command that entered the last insert session once and what was typed
in it count times (helix repeat_last_insert). a at the end of the text now
adds the newline helix's append_mode does, which a terminal's text does not.
helix-golf's invert_dictionary passes.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/Macro.zig')
| -rw-r--r-- | src/Macro.zig | 176 |
1 files changed, 176 insertions, 0 deletions
diff --git a/src/Macro.zig b/src/Macro.zig new file mode 100644 index 00000000..7475992e --- /dev/null +++ b/src/Macro.zig @@ -0,0 +1,176 @@ +//! Keys kept to be typed again. `Q` records the keys that follow into a +//! register until the next `Q`, and `q` types them again (helix macros); +//! `.` types again the last insert session together with the normal command +//! that began it (helix repeat_last_insert). A macro is kept in its register +//! as helix writes one, in key notation (`xt,S=<ret>_<A-(>`), so it can be +//! pasted, edited and yanked back like any text. +const std = @import("std"); +const pardes = @import("pardes.zig"); +const Key = pardes.Key; + +const Macro = @This(); + +/// The register `Q` is recording into, 0 when it is not. +recording: u21 = 0, +recorded: std.ArrayList(Stored) = .empty, +/// The keys of the normal command under way (a count, `"<reg>`, a prefix +/// and the key that completes it), dropped when it completes without +/// entering insert mode. +command: std.ArrayList(Stored) = .empty, +/// `.`'s: the command that began the last insert session and every key +/// typed in it, the one that left it included. +last_insert: std.ArrayList(Stored) = .empty, +/// Where in `last_insert` the typed keys begin, after the command's. +typed_from: usize = 0, +/// Keys are going into `last_insert`. +inserting: bool = false, +/// How deep macros are being typed again; `.`'s replay is `repeating`. +replaying: u8 = 0, +repeating: bool = false, + +/// A key as it can be typed again: what it was, not the text it came with. +pub const Stored = struct { + cp: u21, + ctrl: bool = false, + alt: bool = false, + shift: bool = false, + + pub fn of(key: Key) Stored { + return .{ .cp = key.cp, .ctrl = key.ctrl, .alt = key.alt, .shift = key.shift }; + } + + /// The key again, its text made from its codepoint into `buf`. + pub fn typed(s: Stored, buf: *[4]u8) Key { + var k: Key = .{ .cp = s.cp, .ctrl = s.ctrl, .alt = s.alt, .shift = s.shift }; + if (!s.ctrl and !s.alt and s.cp >= 0x20 and s.cp != Key.backspace and s.cp < 0xF0000) { + const n = std.unicode.utf8Encode(s.cp, buf) catch return k; + k.text = buf[0..n]; + } + return k; + } +}; + +pub fn deinit(m: *Macro, gpa: std.mem.Allocator) void { + m.recorded.deinit(gpa); + m.command.deinit(gpa); + m.last_insert.deinit(gpa); + m.* = .{}; +} + +/// A typed key, each of its characters one key: what a host delivers as +/// one key event with a run of text is that run typed. +pub fn append(list: *std.ArrayList(Stored), gpa: std.mem.Allocator, key: Key) void { + const view = std.unicode.Utf8View.init(key.text) catch null; + if (key.text.len == 0 or key.ctrl or key.alt or view == null) { + list.append(gpa, .of(key)) catch {}; + return; + } + var it = view.?.iterator(); + while (it.nextCodepoint()) |cp| list.append(gpa, .{ .cp = cp, .shift = key.shift }) catch {}; +} + +const names = [_]struct { cp: u21, name: []const u8 }{ + .{ .cp = Key.escape, .name = "esc" }, + .{ .cp = Key.enter, .name = "ret" }, + .{ .cp = Key.tab, .name = "tab" }, + .{ .cp = Key.backspace, .name = "backspace" }, + .{ .cp = Key.delete, .name = "del" }, + .{ .cp = ' ', .name = "space" }, + .{ .cp = '<', .name = "lt" }, + .{ .cp = '>', .name = "gt" }, + .{ .cp = '-', .name = "minus" }, + .{ .cp = Key.up, .name = "up" }, + .{ .cp = Key.down, .name = "down" }, + .{ .cp = Key.left, .name = "left" }, + .{ .cp = Key.right, .name = "right" }, + .{ .cp = Key.home, .name = "home" }, + .{ .cp = Key.end, .name = "end" }, + .{ .cp = Key.page_up, .name = "pageup" }, + .{ .cp = Key.page_down, .name = "pagedown" }, +}; + +/// Keys in helix's notation: a plain character as itself, anything else +/// in angle brackets with its modifiers (`<C-r>`, `<A-(>`, `<ret>`). +pub fn encode(gpa: std.mem.Allocator, keys: []const Stored) ![]u8 { + var out: std.ArrayList(u8) = .empty; + errdefer out.deinit(gpa); + for (keys) |k| { + var name: ?[]const u8 = null; + for (names) |n| if (n.cp == k.cp) { + name = n.name; + }; + // `-` is only spelled out after a modifier, where helix needs it + if (k.cp == '-' and !k.ctrl and !k.alt) name = null; + var buf: [4]u8 = undefined; + const bare = if (name) |n| n else buf[0..(std.unicode.utf8Encode(k.cp, &buf) catch continue)]; + if (!k.ctrl and !k.alt and name == null) { + try out.appendSlice(gpa, bare); + continue; + } + try out.append(gpa, '<'); + if (k.ctrl) try out.appendSlice(gpa, "C-"); + if (k.alt) try out.appendSlice(gpa, "A-"); + try out.appendSlice(gpa, bare); + try out.append(gpa, '>'); + } + return out.toOwnedSlice(gpa); +} + +/// The keys helix notation names; what it cannot read ends the list. +pub fn decode(gpa: std.mem.Allocator, text: []const u8) ![]Stored { + var out: std.ArrayList(Stored) = .empty; + errdefer out.deinit(gpa); + var i: usize = 0; + while (i < text.len) { + const len = std.unicode.utf8ByteSequenceLength(text[i]) catch break; + if (i + len > text.len) break; + if (text[i] != '<') { + try out.append(gpa, .{ .cp = std.unicode.utf8Decode(text[i..][0..len]) catch break }); + i += len; + continue; + } + const close = std.mem.indexOfScalarPos(u8, text, i + 1, '>') orelse break; + var name = text[i + 1 .. close]; + i = close + 1; + var k: Stored = .{ .cp = 0 }; + while (name.len > 2 and name[1] == '-') : (name = name[2..]) switch (name[0]) { + 'C' => k.ctrl = true, + 'A' => k.alt = true, + 'S' => k.shift = true, + else => break, + }; + for (names) |n| if (std.mem.eql(u8, n.name, name)) { + k.cp = n.cp; + }; + if (k.cp == 0) k.cp = std.unicode.utf8Decode(name) catch break; + try out.append(gpa, k); + } + return out.toOwnedSlice(gpa); +} + +test "a macro's keys go into helix notation and come back the same" { + const gpa = std.testing.allocator; + const keys = [_]Stored{ + .{ .cp = 'x' }, + .{ .cp = 't' }, + .{ .cp = ',' }, + .{ .cp = 'S' }, + .{ .cp = '=' }, + .{ .cp = Key.enter }, + .{ .cp = '_' }, + .{ .cp = '(', .alt = true }, + .{ .cp = 'r', .ctrl = true }, + .{ .cp = '<' }, + .{ .cp = ' ' }, + .{ .cp = '-' }, + .{ .cp = '-', .alt = true }, + .{ .cp = 'λ' }, + .{ .cp = Key.escape }, + }; + const text = try encode(gpa, &keys); + defer gpa.free(text); + try std.testing.expectEqualStrings("xt,S=<ret>_<A-(><C-r><lt><space>-<A-minus>λ<esc>", text); + const back = try decode(gpa, text); + defer gpa.free(back); + try std.testing.expectEqualSlices(Stored, &keys, back); +} |
