refactor: add logging to keybind matcher and clean-up match state

This commit is contained in:
CJ van den Berg 2024-11-14 22:12:39 +01:00
parent 00597ce93f
commit cf0cf7aaa6
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
3 changed files with 105 additions and 127 deletions

View file

@ -177,6 +177,7 @@ pub fn build(b: *std.Build) void {
.{ .name = "EventHandler", .module = EventHandler_mod }, .{ .name = "EventHandler", .module = EventHandler_mod },
.{ .name = "renderer", .module = renderer_mod }, .{ .name = "renderer", .module = renderer_mod },
.{ .name = "thespian", .module = thespian_mod }, .{ .name = "thespian", .module = thespian_mod },
.{ .name = "log", .module = log_mod },
}, },
}); });
const keybind_mod = if (dynamic_keybind) keybind_dynamic_mod else keybind_static_mod; const keybind_mod = if (dynamic_keybind) keybind_dynamic_mod else keybind_static_mod;
@ -192,6 +193,7 @@ pub fn build(b: *std.Build) void {
tests.root_module.addImport("EventHandler", EventHandler_mod); tests.root_module.addImport("EventHandler", EventHandler_mod);
tests.root_module.addImport("renderer", renderer_mod); tests.root_module.addImport("renderer", renderer_mod);
tests.root_module.addImport("thespian", thespian_mod); tests.root_module.addImport("thespian", thespian_mod);
tests.root_module.addImport("log", log_mod);
// b.installArtifact(tests); // b.installArtifact(tests);
break :blk b.addRunArtifact(tests); break :blk b.addRunArtifact(tests);
}; };

View file

@ -6,6 +6,7 @@ const std = @import("std");
const tp = @import("thespian"); const tp = @import("thespian");
const cbor = @import("cbor"); const cbor = @import("cbor");
const builtin = @import("builtin"); const builtin = @import("builtin");
const log = @import("log");
const renderer = @import("renderer"); const renderer = @import("renderer");
const key = @import("renderer").input.key; const key = @import("renderer").input.key;
@ -91,13 +92,6 @@ const KeyEvent = struct {
fn eql(self: @This(), other: @This()) bool { fn eql(self: @This(), other: @This()) bool {
return std.meta.eql(self, other); return std.meta.eql(self, other);
} }
fn toString(self: @This(), allocator: std.mem.Allocator) String {
//TODO implement
_ = self;
_ = allocator;
return "";
}
}; };
fn peek(str: []const u8, i: usize) !u8 { fn peek(str: []const u8, i: usize) !u8 {
@ -106,9 +100,7 @@ fn peek(str: []const u8, i: usize) !u8 {
} else return error.outOfBounds; } else return error.outOfBounds;
} }
const Sequence = std.ArrayList(KeyEvent); pub fn parse_key_events(allocator: std.mem.Allocator, str: []const u8) ![]KeyEvent {
pub fn parseKeySequence(result: *Sequence, str: []const u8) !void {
const State = enum { const State = enum {
base, base,
escape_sequence_start, escape_sequence_start,
@ -130,6 +122,8 @@ pub fn parseKeySequence(result: *Sequence, str: []const u8) !void {
var state: State = .base; var state: State = .base;
var function_key_number: u8 = 0; var function_key_number: u8 = 0;
var modifiers: u32 = 0; var modifiers: u32 = 0;
var result = std.ArrayList(KeyEvent).init(allocator);
defer result.deinit();
var i: usize = 0; var i: usize = 0;
while (i < str.len) { while (i < str.len) {
@ -212,7 +206,7 @@ pub fn parseKeySequence(result: *Sequence, str: []const u8) !void {
} }
}, },
else => { else => {
std.debug.print("str: {s}, i: {}\n", .{ str, i }); std.debug.print("str: {s}, i: {} c: {c}\n", .{ str, i, str[i] });
return error.parseEscapeSequenceStart; return error.parseEscapeSequenceStart;
}, },
} }
@ -363,59 +357,38 @@ pub fn parseKeySequence(result: *Sequence, str: []const u8) !void {
}, },
} }
} }
return result.toOwnedSlice();
} }
const String = std.ArrayList(u8);
//An association of an command with a triggering key chord //An association of an command with a triggering key chord
const Binding = struct { const Binding = struct {
keys: Sequence, keys: []KeyEvent,
command: String, command: []const u8,
args: std.ArrayList(u8), args: []const u8,
fn deinit(self: *const @This(), allocator: std.mem.Allocator) void {
allocator.free(self.keys);
allocator.free(self.command);
allocator.free(self.args);
}
fn len(self: Binding) usize { fn len(self: Binding) usize {
return self.keys.items.len; return self.keys.items.len;
} }
fn execute(self: @This()) !void { fn execute(self: @This()) !void {
try command.executeName(self.command.items, .{ .args = .{ .buf = self.args.items } }); try command.executeName(self.command, .{ .args = .{ .buf = self.args } });
} }
const MatchResult = enum { match_impossible, match_possible, matched }; const MatchResult = enum { match_impossible, match_possible, matched };
fn match(self: *const @This(), keys: []const KeyEvent) MatchResult { fn match(self: *const @This(), match_keys: []const KeyEvent) MatchResult {
return matchKeySequence(self.keys.items, keys); if (self.keys.len == 0) return .match_impossible;
} for (self.keys, 0..) |key_event, i| {
if (match_keys.len <= i) return .match_possible;
fn matchKeySequence(self: []const KeyEvent, keys: []const KeyEvent) MatchResult { if (!key_event.eql(match_keys[i])) return .match_impossible;
if (self.len == 0) {
return .match_impossible;
} }
for (keys, 0..) |key_event, i| { return if (self.keys.len == match_keys.len) .matched else .match_possible;
if (!key_event.eql(self[i])) {
return .match_impossible;
}
}
if (keys.len >= self.len) {
return .matched;
} else {
return .match_possible;
}
}
fn init(allocator: std.mem.Allocator) @This() {
return .{
.keys = Sequence.init(allocator),
.command = String.init(allocator),
.args = std.ArrayList(u8).init(allocator),
};
}
fn deinit(self: *const @This()) void {
self.keys.deinit();
self.command.deinit();
self.args.deinit();
} }
}; };
@ -434,6 +407,7 @@ const BindingSet = struct {
current_sequence_egc: std.ArrayList(u8), current_sequence_egc: std.ArrayList(u8),
last_key_event_timestamp_ms: i64 = 0, last_key_event_timestamp_ms: i64 = 0,
input_buffer: std.ArrayList(u8), input_buffer: std.ArrayList(u8),
logger: log.Logger,
const OnMatchFailure = enum { insert, ignore }; const OnMatchFailure = enum { insert, ignore };
@ -466,13 +440,14 @@ const BindingSet = struct {
.last_key_event_timestamp_ms = std.time.milliTimestamp(), .last_key_event_timestamp_ms = std.time.milliTimestamp(),
.input_buffer = try std.ArrayList(u8).initCapacity(allocator, 16), .input_buffer = try std.ArrayList(u8).initCapacity(allocator, 16),
.bindings = std.ArrayList(Binding).init(allocator), .bindings = std.ArrayList(Binding).init(allocator),
.logger = if (!builtin.is_test) log.logger("keybind") else undefined,
}; };
try self.load_json(json_string, namespace_name, mode_name); try self.load_json(json_string, namespace_name, mode_name);
return self; return self;
} }
fn deinit(self: *const BindingSet) void { fn deinit(self: *const BindingSet) void {
for (self.bindings.items) |binding| binding.deinit(); for (self.bindings.items) |binding| binding.deinit(self.allocator);
self.bindings.deinit(); self.bindings.deinit();
self.current_sequence.deinit(); self.current_sequence.deinit();
self.current_sequence_egc.deinit(); self.current_sequence_egc.deinit();
@ -484,6 +459,7 @@ const BindingSet = struct {
defer parsed.deinit(); defer parsed.deinit();
if (parsed.value != .object) return error.NotAnObject; if (parsed.value != .object) return error.NotAnObject;
var namespaces = parsed.value.object.iterator(); var namespaces = parsed.value.object.iterator();
if (!builtin.is_test) self.logger.print("load_json namespace:{s} mode:{s}", .{ namespace_name, mode_name });
while (namespaces.next()) |*namespace_entry| { while (namespaces.next()) |*namespace_entry| {
if (namespace_entry.value_ptr.* != .object) return error.NotAnObject; if (namespace_entry.value_ptr.* != .object) return error.NotAnObject;
if (!std.mem.eql(u8, namespace_entry.key_ptr.*, namespace_name)) continue; if (!std.mem.eql(u8, namespace_entry.key_ptr.*, namespace_name)) continue;
@ -495,75 +471,71 @@ const BindingSet = struct {
} }
} }
fn load_set_from_json(self_: *BindingSet, mode_bindings: std.json.Value) !void { fn load_set_from_json(self: *BindingSet, mode_bindings: std.json.Value) !void {
const JsonConfig = struct { const JsonConfig = struct {
bindings: []const []const []const u8, bindings: []const []const []const u8,
on_match_failure: OnMatchFailure, on_match_failure: OnMatchFailure,
}; };
const parsed = try std.json.parseFromValue(JsonConfig, self_.allocator, mode_bindings, .{}); const parsed = try std.json.parseFromValue(JsonConfig, self.allocator, mode_bindings, .{});
defer parsed.deinit(); defer parsed.deinit();
self_.on_match_failure = parsed.value.on_match_failure; self.on_match_failure = parsed.value.on_match_failure;
var state: enum { key_event, command, args } = .key_event; if (!builtin.is_test) self.logger.print("load_set_from_json bindings:{d}", .{parsed.value.bindings.len});
for (parsed.value.bindings) |entry| { for (parsed.value.bindings) |entry| {
var binding = Binding.init(self_.allocator); var state: enum { key_event, command, args } = .key_event;
var args = std.ArrayList(String).init(self_.allocator); var keys: ?[]KeyEvent = null;
var command_: ?[]const u8 = null;
var args = std.ArrayListUnmanaged([]const u8){};
defer { defer {
for (args.items) |arg| arg.deinit(); if (keys) |p| self.allocator.free(p);
args.deinit(); if (command_) |p| self.allocator.free(p);
for (args.items) |p| self.allocator.free(p);
args.deinit(self.allocator);
} }
for (entry) |token| { for (entry) |token| {
switch (state) { switch (state) {
.key_event => { .key_event => {
try parseKeySequence(&binding.keys, token); keys = try parse_key_events(self.allocator, token);
state = .command; state = .command;
}, },
.command => { .command => {
binding.command = String.init(self_.allocator); command_ = try self.allocator.dupe(u8, token);
try binding.command.appendSlice(token);
state = .args; state = .args;
}, },
.args => { .args => {
var arg = String.init(self_.allocator); try args.append(self.allocator, try self.allocator.dupe(u8, token));
try arg.appendSlice(token);
try args.append(arg);
}, },
} }
} }
var args_cbor = std.ArrayList(u8).init(self_.allocator); if (state != .args) {
defer args_cbor.deinit(); if (builtin.is_test) @panic("invalid state in load_set_from_json");
const writer = args_cbor.writer(); continue;
}
var args_cbor = std.ArrayListUnmanaged(u8){};
defer args_cbor.deinit(self.allocator);
const writer = args_cbor.writer(self.allocator);
try cbor.writeArrayHeader(writer, args.items.len); try cbor.writeArrayHeader(writer, args.items.len);
for (args.items) |arg| try cbor.writeValue(writer, arg.items); for (args.items) |arg| try cbor.writeValue(writer, arg);
try binding.args.appendSlice(args_cbor.items);
try self_.bindings.append(binding);
}
}
// fn parseBindingList(self: *@This(), str: []const u8) !void { try self.bindings.append(.{
// var iter = std.mem.tokenizeAny(u8, str, &.{'\n'}); .keys = keys.?,
// while (iter.next()) |token| { .command = command_.?,
// try self.bindings.append(try parseBinding(self.allocator, token)); .args = try args_cbor.toOwnedSlice(self.allocator),
// } });
// } keys = null;
command_ = null;
fn cmd(self: *@This(), name_: []const u8, ctx: command.Context) tp.result {
try self.flushInputBuffer();
self.last_cmd = name_;
if (builtin.is_test == false) {
try command.executeName(name_, ctx);
} }
} }
const max_key_sequence_time_interval = 750; const max_key_sequence_time_interval = 750;
const max_input_buffer_size = 1024; const max_input_buffer_size = 1024;
fn insertBytes(self: *@This(), bytes: []const u8) !void { fn insert_bytes(self: *@This(), bytes: []const u8) !void {
if (self.input_buffer.items.len + 4 > max_input_buffer_size) if (self.input_buffer.items.len + 4 > max_input_buffer_size)
try self.flushInputBuffer(); try self.flush();
try self.input_buffer.appendSlice(bytes); try self.input_buffer.appendSlice(bytes);
} }
fn flushInputBuffer(self: *@This()) !void { fn flush(self: *@This()) !void {
const Static = struct { const Static = struct {
var insert_chars_id: ?command.ID = null; var insert_chars_id: ?command.ID = null;
}; };
@ -573,7 +545,7 @@ const BindingSet = struct {
command.get_id_cache("insert_chars", &Static.insert_chars_id) orelse { command.get_id_cache("insert_chars", &Static.insert_chars_id) orelse {
return tp.exit_error(error.InputTargetNotFound, null); return tp.exit_error(error.InputTargetNotFound, null);
}; };
if (builtin.is_test == false) { if (!builtin.is_test) {
try command.execute(id, command.fmt(.{self.input_buffer.items})); try command.execute(id, command.fmt(.{self.input_buffer.items}));
} }
} }
@ -594,28 +566,28 @@ const BindingSet = struct {
tp.string, tp.string,
tp.extract(&modifiers), tp.extract(&modifiers),
})) { })) {
self.registerKeyEvent(egc, .{ self.process_key_event(egc, .{
.event_type = evtype, .event_type = evtype,
.key = keypress, .key = keypress,
.modifiers = modifiers, .modifiers = modifiers,
}) catch |e| return tp.exit_error(e, @errorReturnTrace()); }) catch |e| return tp.exit_error(e, @errorReturnTrace());
} else if (try m.match(.{"F"})) { } else if (try m.match(.{"F"})) {
self.flushInputBuffer() catch |e| return tp.exit_error(e, @errorReturnTrace()); self.flush() catch |e| return tp.exit_error(e, @errorReturnTrace());
} else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) { } else if (try m.match(.{ "system_clipboard", tp.extract(&text) })) {
self.flushInputBuffer() catch |e| return tp.exit_error(e, @errorReturnTrace()); self.flush() catch |e| return tp.exit_error(e, @errorReturnTrace());
self.insertBytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace()); self.insert_bytes(text) catch |e| return tp.exit_error(e, @errorReturnTrace());
self.flushInputBuffer() catch |e| return tp.exit_error(e, @errorReturnTrace()); self.flush() catch |e| return tp.exit_error(e, @errorReturnTrace());
} }
return false; return false;
} }
//register a key press and try to match it with a binding //register a key press and try to match it with a binding
fn registerKeyEvent(self: *BindingSet, egc: u32, event: KeyEvent) !void { fn process_key_event(self: *BindingSet, egc: u32, event: KeyEvent) !void {
//clear key history if enough time has passed since last key press //clear key history if enough time has passed since last key press
const timestamp = std.time.milliTimestamp(); const timestamp = std.time.milliTimestamp();
if (self.last_key_event_timestamp_ms - timestamp > max_key_sequence_time_interval) { if (self.last_key_event_timestamp_ms - timestamp > max_key_sequence_time_interval) {
try self.abortCurrentSequence(.timeout, egc, event); try self.terminate_sequence(.timeout, egc, event);
} }
self.last_key_event_timestamp_ms = timestamp; self.last_key_event_timestamp_ms = timestamp;
@ -625,10 +597,20 @@ const BindingSet = struct {
try self.current_sequence_egc.appendSlice(buf[0..bytes]); try self.current_sequence_egc.appendSlice(buf[0..bytes]);
var all_matches_impossible = true; var all_matches_impossible = true;
defer if (!builtin.is_test) self.logger.print("process_key_event all_matches_impossible:{any} event:{any} egc:{d} text:'{s}' sequence:'{s}' bindings:{d}", .{
all_matches_impossible,
event,
egc,
buf[0..bytes],
self.current_sequence_egc.items,
self.bindings.items.len,
});
for (self.bindings.items) |binding| blk: { for (self.bindings.items) |binding| blk: {
switch (binding.match(self.current_sequence.items)) { switch (binding.match(self.current_sequence.items)) {
.matched => { .matched => {
if (!builtin.is_test) { if (!builtin.is_test) {
self.logger.print("matched binding -> {s}", .{binding.command});
if (!builtin.is_test) self.logger.print("execute '{s}'", .{binding.command});
try binding.execute(); try binding.execute();
} }
self.current_sequence.clearRetainingCapacity(); self.current_sequence.clearRetainingCapacity();
@ -636,24 +618,27 @@ const BindingSet = struct {
break :blk; break :blk;
}, },
.match_possible => { .match_possible => {
if (!builtin.is_test) self.logger.print("match possible for binding -> {s}", .{binding.command});
all_matches_impossible = false; all_matches_impossible = false;
}, },
.match_impossible => {}, .match_impossible => {
if (!builtin.is_test) self.logger.print("match impossible for binding -> {s}", .{binding.command});
},
} }
} }
if (all_matches_impossible) { if (all_matches_impossible) {
try self.abortCurrentSequence(.match_impossible, egc, event); try self.terminate_sequence(.match_impossible, egc, event);
} }
} }
const AbortType = enum { timeout, match_impossible }; const AbortType = enum { timeout, match_impossible };
fn abortCurrentSequence(self: *@This(), abort_type: AbortType, egc: u32, key_event: KeyEvent) anyerror!void { fn terminate_sequence(self: *@This(), abort_type: AbortType, egc: u32, key_event: KeyEvent) anyerror!void {
_ = egc; _ = egc;
_ = key_event; _ = key_event;
if (abort_type == .match_impossible) { if (abort_type == .match_impossible) {
switch (self.on_match_failure) { switch (self.on_match_failure) {
.insert => { .insert => {
try self.insertBytes(self.current_sequence_egc.items); try self.insert_bytes(self.current_sequence_egc.items);
self.current_sequence_egc.clearRetainingCapacity(); self.current_sequence_egc.clearRetainingCapacity();
self.current_sequence.clearRetainingCapacity(); self.current_sequence.clearRetainingCapacity();
}, },
@ -661,15 +646,9 @@ const BindingSet = struct {
self.current_sequence.clearRetainingCapacity(); self.current_sequence.clearRetainingCapacity();
self.current_sequence_egc.clearRetainingCapacity(); self.current_sequence_egc.clearRetainingCapacity();
}, },
// .fallback_mode => |fallback_mode_name| {
// _ = fallback_mode_name;
// @panic("This feature not supported yet");
//const fallback_mode = self.active_namespace().get(fallback_mode_name).?;
//try self.registerKeyEvent(fallback_mode, egc, key_event);
// },
} }
} else if (abort_type == .timeout) { } else if (abort_type == .timeout) {
try self.insertBytes(self.current_sequence_egc.items); try self.insert_bytes(self.current_sequence_egc.items);
self.current_sequence_egc.clearRetainingCapacity(); self.current_sequence_egc.clearRetainingCapacity();
self.current_sequence.clearRetainingCapacity(); self.current_sequence.clearRetainingCapacity();
} }
@ -719,11 +698,10 @@ const parse_test_cases = .{
test "parse" { test "parse" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
inline for (parse_test_cases) |case| { inline for (parse_test_cases) |case| {
var parsed = Sequence.init(alloc); const parsed = try parse_key_events(alloc, case[0]);
defer parsed.deinit(); defer alloc.free(parsed);
try parseKeySequence(&parsed, case[0]);
const expected: []const KeyEvent = case[1]; const expected: []const KeyEvent = case[1];
const actual: []const KeyEvent = parsed.items; const actual: []const KeyEvent = parsed;
try expectEqual(expected.len, actual.len); try expectEqual(expected.len, actual.len);
for (expected, 0..) |expected_event, i| { for (expected, 0..) |expected_event, i| {
try expectEqual(expected_event, actual[i]); try expectEqual(expected_event, actual[i]);
@ -746,25 +724,26 @@ const match_test_cases = .{
test "match" { test "match" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
inline for (match_test_cases) |case| { inline for (match_test_cases) |case| {
var input = Sequence.init(alloc); const input = try parse_key_events(alloc, case[0]);
defer input.deinit(); defer alloc.free(input);
var binding = Sequence.init(alloc); const binding: Binding = .{
defer binding.deinit(); .keys = try parse_key_events(alloc, case[1]),
.command = undefined,
.args = undefined,
};
defer alloc.free(binding.keys);
try parseKeySequence(&input, case[0]); try expectEqual(case[2], binding.match(input));
try parseKeySequence(&binding, case[1]);
try expectEqual(case[2], Binding.matchKeySequence(binding.items, input.items));
} }
} }
test "json" { test "json" {
const alloc = std.testing.allocator; const alloc = std.testing.allocator;
var bindings = try BindingSet.init(alloc); var bindings = try BindingSet.init(alloc, @embedFile("keybindings.json"), "vim", "normal");
defer bindings.deinit(); defer bindings.deinit();
try bindings.load_json(@embedFile("keybindings.json"), "vim", "normal"); try bindings.process_key_event('j', .{ .key = 'j' });
try bindings.registerKeyEvent('j', .{ .key = 'j' }); try bindings.process_key_event('k', .{ .key = 'k' });
try bindings.registerKeyEvent('k', .{ .key = 'k' }); try bindings.process_key_event('g', .{ .key = 'g' });
try bindings.registerKeyEvent('g', .{ .key = 'g' }); try bindings.process_key_event('i', .{ .key = 'i' });
try bindings.registerKeyEvent('i', .{ .key = 'i' }); try bindings.process_key_event(0, .{ .key = 'i', .modifiers = mod.CTRL });
try bindings.registerKeyEvent(0, .{ .key = 'i', .modifiers = mod.CTRL });
} }

View file

@ -13,7 +13,6 @@
["v", "change_mode", "visual"], ["v", "change_mode", "visual"],
["gg", "goto_page_begin"], ["gg", "goto_page_begin"],
["<S-g>", "goto_page_end"], ["<S-g>", "goto_page_end"],
["<S-:>", "open_command_palette"],
["<C-u>", "cursor_half_page_up"], ["<C-u>", "cursor_half_page_up"],
["<C-d>", "cursor_half_page_down"] ["<C-d>", "cursor_half_page_down"]
] ]
@ -55,14 +54,12 @@
["<C-e>", "open_recent"], ["<C-e>", "open_recent"],
["<C-r>", "open_recent_project"], ["<C-r>", "open_recent_project"],
["<C-p>", "open_command_palette"], ["<C-p>", "open_command_palette"],
["<C-/>", "open_help"],
["<C-k><C-t>", "change_theme"], ["<C-k><C-t>", "change_theme"],
["<C-S-P>", "open_command_palette"], ["<C-S-P>", "open_command_palette"],
["<C-S-Q>", "quit_without_saving"], ["<C-S-Q>", "quit_without_saving"],
["<C-S-R>", "restart"], ["<C-S-R>", "restart"],
["<C-S-F>", "find_in_files"], ["<C-S-F>", "find_in_files"],
["<C-S-L>", "toggle_panel"], ["<C-S-L>", "toggle_panel"],
["<C-S-/>", "open_help"],
["<A-S-p>", "open_command_palette"], ["<A-S-p>", "open_command_palette"],
["<A-n>", "goto_next_file_or_diagnostic"], ["<A-n>", "goto_next_file_or_diagnostic"],
["<A-p>", "goto_prev_file_or_diagnostic"], ["<A-p>", "goto_prev_file_or_diagnostic"],