input: binding parser of sequences

This commit is contained in:
Mitchell Hashimoto
2024-08-15 15:29:52 -07:00
committed by Mitchell Hashimoto
parent e2913fd16f
commit a798a26063

View File

@ -22,6 +22,58 @@ pub const Error = error{
InvalidAction, InvalidAction,
}; };
/// Full binding parser. The binding parser is implemented as an iterator
/// which yields elements to support multi-key sequences without allocation.
pub const Parser = struct {
unconsumed: bool = false,
trigger_it: SequenceIterator,
action: Action,
pub const Elem = union(enum) {
/// A leader trigger in a sequence.
leader: Trigger,
/// The final trigger and action in a sequence.
binding: Binding,
};
pub fn init(raw_input: []const u8) Error!Parser {
// If our entire input is prefixed with "unconsumed:" then we are
// not consuming this keybind when the action is triggered.
const unconsumed_prefix = "unconsumed:";
const unconsumed = std.mem.startsWith(u8, raw_input, unconsumed_prefix);
const start_idx = if (unconsumed) unconsumed_prefix.len else 0;
const input = raw_input[start_idx..];
// Find the first = which splits are mapping into the trigger
// and action, respectively.
const eql_idx = std.mem.indexOf(u8, input, "=") orelse return Error.InvalidFormat;
// Sequence iterator goes up to the equal, action is after. We can
// parse the action now.
return .{
.unconsumed = unconsumed,
.trigger_it = .{ .input = input[0..eql_idx] },
.action = try Action.parse(input[eql_idx + 1 ..]),
};
}
pub fn next(self: *Parser) Error!?Elem {
// Get our trigger. If we're out of triggers then we're done.
const trigger = (try self.trigger_it.next()) orelse return null;
// If this is our last trigger then it is our final binding.
if (!self.trigger_it.done()) return .{ .leader = trigger };
// Out of triggers, yield the final action.
return .{ .binding = .{
.trigger = trigger,
.action = self.action,
.consumed = !self.unconsumed,
} };
}
};
/// An iterator that yields each trigger in a sequence of triggers. For /// An iterator that yields each trigger in a sequence of triggers. For
/// example, the sequence "ctrl+a>ctrl+b" would yield "ctrl+a" and then /// example, the sequence "ctrl+a>ctrl+b" would yield "ctrl+a" and then
/// "ctrl+b". The iterator approach allows us to parse a sequence of /// "ctrl+b". The iterator approach allows us to parse a sequence of
@ -35,12 +87,17 @@ const SequenceIterator = struct {
/// Returns the next trigger in the sequence if there is no parsing error. /// Returns the next trigger in the sequence if there is no parsing error.
pub fn next(self: *SequenceIterator) Error!?Trigger { pub fn next(self: *SequenceIterator) Error!?Trigger {
if (self.i > self.input.len) return null; if (self.done()) return null;
const rem = self.input[self.i..]; const rem = self.input[self.i..];
const idx = std.mem.indexOf(u8, rem, ">") orelse rem.len; const idx = std.mem.indexOf(u8, rem, ">") orelse rem.len;
defer self.i += idx + 1; defer self.i += idx + 1;
return try Trigger.parse(rem[0..idx]); return try Trigger.parse(rem[0..idx]);
} }
/// Returns true if there are no more triggers to parse.
pub fn done(self: *const SequenceIterator) bool {
return self.i > self.input.len;
}
}; };
/// Parse the format "ctrl+a=csi:A" into a binding. The format is /// Parse the format "ctrl+a=csi:A" into a binding. The format is
@ -751,6 +808,19 @@ pub const Set = struct {
/// Assert: trigger in this map is also in bindings. /// Assert: trigger in this map is also in bindings.
unconsumed: UnconsumedMap = .{}, unconsumed: UnconsumedMap = .{},
/// The entry type for the forward mapping of trigger to action.
const Entry = union(enum) {
/// This key is a leader key in a sequence. You must follow the given
/// set to find the next key in the sequence.
leader: *Set,
/// This trigger completes a sequence and the value is the action
/// to take. The "_unconsumed" variant is used for triggers that
/// should not consume the input.
action: Action,
action_unconsumed: Action,
};
pub fn deinit(self: *Set, alloc: Allocator) void { pub fn deinit(self: *Set, alloc: Allocator) void {
self.bindings.deinit(alloc); self.bindings.deinit(alloc);
self.reverse.deinit(alloc); self.reverse.deinit(alloc);
@ -1152,6 +1222,38 @@ test "sequence iterator" {
} }
} }
test "parse: sequences" {
const testing = std.testing;
// single character
{
var p = try Parser.init("ctrl+a=ignore");
try testing.expectEqual(Parser.Elem{ .binding = .{
.trigger = .{
.mods = .{ .ctrl = true },
.key = .{ .translated = .a },
},
.action = .{ .ignore = {} },
} }, (try p.next()).?);
try testing.expect(try p.next() == null);
}
// sequence
{
var p = try Parser.init("a>b=ignore");
try testing.expectEqual(Parser.Elem{ .leader = .{
.key = .{ .translated = .a },
} }, (try p.next()).?);
try testing.expectEqual(Parser.Elem{ .binding = .{
.trigger = .{
.key = .{ .translated = .b },
},
.action = .{ .ignore = {} },
} }, (try p.next()).?);
try testing.expect(try p.next() == null);
}
}
test "set: maintains reverse mapping" { test "set: maintains reverse mapping" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;