//! 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=_`), 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, `"`, 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 (``, ``, ``). 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=_-λ", text); const back = try decode(gpa, text); defer gpa.free(back); try std.testing.expectEqualSlices(Stored, &keys, back); }