1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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);
}
|