summaryrefslogtreecommitdiff
path: root/src/Macro.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Macro.zig')
-rw-r--r--src/Macro.zig176
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);
+}