From f8f2e8e3bedb94833bbe6cab9c435b33cfbfea14 Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Mon, 3 Jan 2022 03:16:31 +0200 Subject: a bit improved key map --- src/Buffer.zig | 11 ++- src/Editor.zig | 14 ++- src/KeyMap.zig | 290 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/key_state.zig | 140 -------------------------- src/main.zig | 2 +- 5 files changed, 309 insertions(+), 148 deletions(-) create mode 100644 src/KeyMap.zig delete mode 100644 src/key_state.zig (limited to 'src') diff --git a/src/Buffer.zig b/src/Buffer.zig index 5bf319c..d0546c0 100644 --- a/src/Buffer.zig +++ b/src/Buffer.zig @@ -394,7 +394,8 @@ pub fn nextLine(self: *Buffer) void { } } -pub fn pageDown(self: *Buffer, screenrows: usize) void { +pub fn pageDown(self: *Buffer, editor: Editor) void { + const screenrows = editor.screenrows; self.cy = std.math.clamp( self.rowoff + screenrows - 1 - self.config.page_overlap, 0, @@ -406,7 +407,8 @@ pub fn pageDown(self: *Buffer, screenrows: usize) void { } } -pub fn pageUp(self: *Buffer, screenrows: usize) void { +pub fn pageUp(self: *Buffer, editor: Editor) void { + const screenrows = editor.screenrows; self.cy = std.math.min(self.rows.items.len, self.rowoff + self.config.page_overlap); var i: usize = 0; while (i < screenrows) : (i += 1) { @@ -423,8 +425,11 @@ pub fn previousLine(self: *Buffer) void { self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx); } -pub fn recenterTopBottom(self: *Buffer, screenrows: usize) void { +pub fn recenterTopBottom(self: *Buffer, editor: *Editor) !void { // TODO: Currently only recenters + try editor.refreshWindowSize(); + + const screenrows = editor.screenrows; if (self.cy >= screenrows / 2) { self.rowoff = self.cy - screenrows / 2; } else { diff --git a/src/Editor.zig b/src/Editor.zig index d721600..4d90760 100644 --- a/src/Editor.zig +++ b/src/Editor.zig @@ -6,8 +6,8 @@ const ArrayList = std.ArrayList; const Buffer = es.Buffer; const Editor = @This(); const Key = es.Key; +const KeyMap = es.KeyMap; const KeyReader = es.KeyReader; -const KeyState = es.key_state.KeyState; const StringBuilder = es.StringBuilder; const StringHashMap = std.StringHashMap; @@ -22,12 +22,15 @@ screencols: usize, statusmsg: ?[]u8, statusmsg_time: i64, -current_state: KeyState, +key_map: KeyMap, key_reader: KeyReader, should_exit: bool, pub fn init(allocator: Allocator) !Editor { + var key_map: ?KeyMap = try KeyMap.defaultMap(allocator); + errdefer if (key_map) |*map| map.deinit(); + var self = Editor{ .allocator = allocator, @@ -40,12 +43,13 @@ pub fn init(allocator: Allocator) !Editor { .statusmsg = null, .statusmsg_time = 0, - .current_state = es.key_state.defaultState, + .key_map = key_map.?, .key_reader = KeyReader.init(allocator), .should_exit = false, }; errdefer self.deinit(); + key_map = null; // Initializes .screenrows and .screencols try self.refreshWindowSize(); @@ -67,6 +71,8 @@ pub fn deinit(self: *Editor) void { self.allocator.free(statusmsg); } + self.key_map.deinit(); + self.key_reader.deinit(); self.* = undefined; @@ -197,7 +203,7 @@ pub fn openFile(self: *Editor) !void { pub fn processKeypress(self: *Editor) !void { const key = try self.key_reader.readKey(); - try self.current_state(self, self.buffer, key); + try self.key_map.keypress(self, self.buffer, key); } pub fn prompt(self: *Editor, allocator: Allocator, prompt_str: []const u8) !?[]u8 { diff --git a/src/KeyMap.zig b/src/KeyMap.zig new file mode 100644 index 0000000..5c866b8 --- /dev/null +++ b/src/KeyMap.zig @@ -0,0 +1,290 @@ +const es = @import("root"); +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const AutoHashMap = std.AutoHashMap; +const Buffer = es.Buffer; +const Editor = es.Editor; +const Key = es.Key; +const KeyMap = @This(); + +pub const Error = error{ + MalformedConfig, + MisformedTerminalResponse, + StreamTooLong, +} || + std.fmt.ParseIntError || + std.fs.File.OpenError || + std.fs.File.ReadError || + std.fs.File.WriteError || + std.mem.Allocator.Error || + std.os.GetCwdError || + std.os.RealPathError; + +pub const BoundFn = fn (*Editor, *Buffer, Key) Error!void; + +const Value = union(enum) { + bound_fn: BoundFn, + submap: AutoHashMap(Key, Value), + + pub fn deinit(self: *Value) void { + switch (self.*) { + .bound_fn => {}, + .submap => |*map| { + var it = map.valueIterator(); + while (it.next()) |value| { + value.deinit(); + } + + map.deinit(); + }, + } + + self.* = undefined; + } +}; + +allocator: Allocator, +current: ?*AutoHashMap(Key, Value), +default: ?BoundFn, +map: AutoHashMap(Key, Value), + +pub fn init(allocator: Allocator) KeyMap { + return .{ + .allocator = allocator, + .current = null, + .default = null, + .map = AutoHashMap(Key, Value).init(allocator), + }; +} + +pub fn defaultMap(allocator: Allocator) !KeyMap { + var map = KeyMap.init(allocator); + errdefer map.deinit(); + + map.default = wrapFn(defaultFn); + + // M-g <*> + try map.bind(&.{Key.meta('g'), Key.char('g')}, Buffer.goToLine); + + // M-O <*> + try map.bind(&.{Key.meta('O'), Key.char('F')}, Buffer.moveEndOfLine); + try map.bind(&.{Key.meta('O'), Key.char('H')}, Buffer.moveBeginningOfLine); + + // C-x C-<*> + try map.bind(&.{Key.ctrl('x'), Key.ctrl('b')}, Editor.switchBuffer); + try map.bind(&.{Key.ctrl('x'), Key.ctrl('c')}, Editor.saveBuffersExit); + try map.bind(&.{Key.ctrl('x'), Key.ctrl('f')}, Editor.openFile); + try map.bind(&.{Key.ctrl('x'), Key.ctrl('s')}, Buffer.save); + + // C-x <*> + try map.bind(&.{Key.ctrl('x'), Key.char('b')}, Editor.switchBuffer); + // TODO: C-x h for help + try map.bind(&.{Key.ctrl('x'), Key.char('k')}, Editor.killCurrentBuffer); + + // M-C-<*> + try map.bind(&.{Key.meta(Key.ctrl('d'))}, Buffer.backwardDeleteChar); + + // M-<*> + // M-g is taken + // M-O is taken + try map.bind(&.{Key.meta('v')}, Buffer.pageUp); + + // C-<*> + try map.bind(&.{Key.ctrl('a')}, Buffer.moveBeginningOfLine); + try map.bind(&.{Key.ctrl('b')}, Buffer.backwardChar); + try map.bind(&.{Key.ctrl('d')}, Buffer.deleteChar); + try map.bind(&.{Key.ctrl('e')}, Buffer.moveEndOfLine); + try map.bind(&.{Key.ctrl('f')}, Buffer.forwardChar); + try map.bind(&.{Key.ctrl('g')}, Editor.clearStatusMessage); + try map.bind(&.{Key.ctrl('i')}, Buffer.indent); // tab + try map.bind(&.{Key.ctrl('j')}, Buffer.insertNewline); // line feed + try map.bind(&.{Key.ctrl('k')}, Buffer.killLine); + try map.bind(&.{Key.ctrl('l')}, Buffer.recenterTopBottom); + try map.bind(&.{Key.ctrl('m')}, Buffer.insertNewline); // carriage return + try map.bind(&.{Key.ctrl('n')}, Buffer.nextLine); + try map.bind(&.{Key.ctrl('p')}, Buffer.previousLine); + try map.bind(&.{Key.ctrl('s')}, es.search); + // TODO: C-q quotedInsert + try map.bind(&.{Key.ctrl('v')}, Buffer.pageDown); + // C-x is taken + + // <*> + try map.bind(&.{Key.backspace}, Buffer.backwardDeleteChar); + try map.bind(&.{Key.delete}, Buffer.deleteChar); + try map.bind(&.{Key.down}, Buffer.nextLine); + try map.bind(&.{Key.end}, Buffer.moveEndOfLine); + try map.bind(&.{Key.home}, Buffer.moveBeginningOfLine); + try map.bind(&.{Key.left}, Buffer.backwardChar); + try map.bind(&.{Key.page_down}, Buffer.pageDown); + try map.bind(&.{Key.page_up}, Buffer.pageUp); + try map.bind(&.{Key.right}, Buffer.forwardChar); + try map.bind(&.{Key.untab}, Buffer.unindent); + try map.bind(&.{Key.up}, Buffer.previousLine); + + return map; +} + +pub fn deinit(self: *KeyMap) void { + var it = self.map.valueIterator(); + while (it.next()) |value| { + value.deinit(); + } + self.map.deinit(); + + self.* = undefined; +} + +pub fn bind(self: *KeyMap, keys: []const Key, comptime f: anytype) !void { + std.debug.assert(keys.len > 0); + var map: *AutoHashMap(Key, Value) = &self.map; + for (keys[0..keys.len - 1]) |key| { + const gop = try map.getOrPut(key); + if (!gop.found_existing) { + gop.value_ptr.* = .{ .submap = AutoHashMap(Key, Value).init(self.allocator) }; + } + + switch (gop.value_ptr.*) { + .bound_fn => { + std.log.err("Attempting to bind a longer chord over a shorter one ({any})", .{keys}); + return error.KeyBindError; + }, + .submap => |*next_map| map = next_map, + } + } + + const gop = try map.getOrPut(keys[keys.len - 1]); + if (!gop.found_existing) { + gop.value_ptr.* = .{ .bound_fn = wrapFn(f) }; + } else switch (gop.value_ptr.*) { + .bound_fn => gop.value_ptr.* = .{ .bound_fn = wrapFn(f) }, + .submap => { + std.log.err("Attempting to bind a shorter chord over a longer one ({any})", .{keys}); + return error.KeyBindError; + }, + } +} + +pub fn keypress(self: *KeyMap, editor: *Editor, buf: *Buffer, key: Key) !void { + const map = self.current orelse &self.map; + if (map.getPtr(key)) |value| { + switch (value.*) { + .bound_fn => |f| { + self.current = null; + return f(editor, buf, key); + }, + .submap => |*submap| self.current = submap, + } + } else if (self.current != null) { + // TODO: Output the full chord + std.log.debug("Unknown chord: ... {}", .{key}); + try editor.setStatusMessage("Unknown chord: ... {}", .{key}); + self.current = null; + } else if (self.default) |default| { + return default(editor, buf, key); + } else { + std.log.debug("Unknown key: {}", .{key}); + try editor.setStatusMessage("Unknown key: {}", .{key}); + } +} + +fn defaultFn(editor: *Editor, buffer: *Buffer, key: Key) !void { + if (@enumToInt(key) <= @enumToInt(Key.max_char)) { + const char = @intCast(u8, @enumToInt(key)); + if (std.ascii.isGraph(char) or char == ' ') { + try buffer.insertChar(char); + return; + } + } + + std.log.debug("Unknown key: {}", .{key}); + try editor.setStatusMessage("Unknown key: {}", .{key}); +} + +fn wrapFn(comptime f: anytype) BoundFn { + comptime { + if (@TypeOf(f) == BoundFn) { + return f; + } + + const fn_info = @typeInfo(@TypeOf(f)).Fn; + const should_try = if (fn_info.return_type) |return_type| @typeInfo(return_type) == .ErrorUnion else false; + + const args_info = fn_info.args; + var args = [_]type { undefined } ** args_info.len; + for (args_info) |arg_info, idx| { + args[idx] = arg_info.arg_type.?; + } + + if (std.mem.eql(type, &args, &.{*Buffer})) { + return struct { + pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void { + _ = e; + _ = k; + if (should_try) { + _ = try f(b); + } else { + _ = f(b); + } + } + }.wf; + } else if (std.mem.eql(type, &args, &.{*Editor})) { + return struct { + pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void { + _ = b; + _ = k; + if (should_try) { + _ = try f(e); + } else { + _ = f(e); + } + } + }.wf; + } else if (std.mem.eql(type, &args, &.{*Editor, *Buffer})) { + return struct { + pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void { + _ = k; + if (should_try) { + _ = try f(e, b); + } else { + _ = f(e, b); + } + } + }.wf; + } else if (std.mem.eql(type, &args, &.{*Buffer, *Editor})) { + return struct { + pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void { + _ = k; + if (should_try) { + _ = try f(b, e); + } else { + _ = f(b, e); + } + } + }.wf; + } else if (std.mem.eql(type, &args, &.{*Buffer, Editor})) { + return struct { + pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void { + _ = k; + if (should_try) { + _ = try f(b, e.*); + } else { + _ = f(b, e.*); + } + } + }.wf; + } else if (std.mem.eql(type, &args, &.{*Editor, *Buffer, Key})) { + return struct { + pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void { + if (should_try) { + _ = try f(e, b, k); + } else { + _ = f(e, b, k); + } + } + }.wf; + } + + @compileError("How to wrap " ++ @typeName(@TypeOf(f))); + } +} diff --git a/src/key_state.zig b/src/key_state.zig deleted file mode 100644 index 818dded..0000000 --- a/src/key_state.zig +++ /dev/null @@ -1,140 +0,0 @@ -const es = @import("root"); -const std = @import("std"); - -const Buffer = es.Buffer; -const Editor = es.Editor; -const Key = es.Key; - -pub const Error = error{ - MalformedConfig, - MisformedTerminalResponse, - StreamTooLong, -} || - std.mem.Allocator.Error || - std.fmt.ParseIntError || - std.fs.File.OpenError || - std.fs.File.ReadError || - std.fs.File.WriteError || - std.os.GetCwdError || - std.os.RealPathError; -pub const KeyState = fn (*Editor, *Buffer, Key) Error!void; - -fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { - editor.current_state = defaultState; - editor.clearStatusMessage(); - - switch (key) { - // ========== <*> ========== - Key.char('g') => try buf.goToLine(editor), - - else => { - std.log.debug("Unknown chord: M-g {}", .{key}); - try editor.setStatusMessage("Unknown chord: M-g {}", .{key}); - }, - } -} - -fn mOState(editor: *Editor, buf: *Buffer, key: Key) Error!void { - editor.current_state = defaultState; - editor.clearStatusMessage(); - - switch (key) { - // ========== <*> ========== - Key.char('F') => buf.moveEndOfLine(), - Key.char('H') => buf.moveBeginningOfLine(), - - else => { - std.log.debug("Unknown chord: M-O {}", .{key}); - try editor.setStatusMessage("Unknown chord: M-O {}", .{key}); - }, - } -} - -fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void { - editor.current_state = defaultState; - editor.clearStatusMessage(); - - switch (key) { - // ========== C-<*> ========== - Key.ctrl('b'), Key.char('b') => try editor.switchBuffer(), - Key.ctrl('c') => try editor.saveBuffersExit(), - Key.ctrl('f') => try editor.openFile(), - Key.ctrl('g') => {}, - Key.ctrl('s') => try buf.save(editor), - - // ========== <*> ========== - Key.char('k') => _ = try editor.killCurrentBuffer(), - - else => { - std.log.debug("Unknown chord: C-x {}", .{key}); - try editor.setStatusMessage("Unknown chord: C-x {}", .{key}); - }, - } -} - -pub fn defaultState(editor: *Editor, buf: *Buffer, key: Key) Error!void { - switch (key) { - // ========== M-C-<*> ========== - Key.meta(Key.ctrl('d')), Key.backspace => try buf.backwardDeleteChar(), - - // ========== M-<*> ========== - Key.meta('g') => { - editor.current_state = mgState; - try editor.setStatusMessage("M-g-", .{}); - }, - Key.meta('O') => editor.current_state = mOState, - Key.meta('v'), Key.page_up => buf.pageUp(editor.screenrows), - - // ========== C-<*> ========== - Key.ctrl('a'), Key.home => buf.moveBeginningOfLine(), - Key.ctrl('b'), Key.left => buf.backwardChar(), - Key.ctrl('d'), Key.delete => try buf.deleteChar(), - Key.ctrl('e'), Key.end => buf.moveEndOfLine(), - Key.ctrl('f'), Key.right => buf.forwardChar(), - Key.ctrl('g') => editor.clearStatusMessage(), - - // TODO: C-h help - - // tab - Key.ctrl('i') => try buf.indent(), - // line feed - Key.ctrl('j') => try buf.insertNewline(), - Key.ctrl('k') => try buf.killLine(), - - Key.ctrl('l') => { - try editor.refreshWindowSize(); - buf.recenterTopBottom(editor.screenrows); - }, - - // carriage return - Key.ctrl('m') => try buf.insertNewline(), - Key.ctrl('n'), Key.down => buf.nextLine(), - Key.ctrl('p'), Key.up => buf.previousLine(), - Key.ctrl('s') => try es.search(editor, buf), - - // TODO: C-q quotedInsert - - Key.ctrl('v'), Key.page_down => buf.pageDown(editor.screenrows), - - Key.ctrl('x') => { - editor.current_state = cxState; - try editor.setStatusMessage("C-x-", .{}); - }, - - // ========== <*> ========== - Key.untab => try buf.unindent(), - - else => { - if (@enumToInt(key) <= @enumToInt(Key.max_char)) { - const char = @intCast(u8, @enumToInt(key)); - if (std.ascii.isGraph(char) or std.ascii.isSpace(char)) { - try buf.insertChar(char); - return; - } - } - - std.log.debug("Unknown key: {}", .{key}); - try editor.setStatusMessage("Unknown key: {}", .{key}); - }, - } -} diff --git a/src/main.zig b/src/main.zig index 176aadd..7a3f007 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,8 +5,8 @@ pub const Editor = @import("Editor.zig"); pub const files = @import("files.zig"); pub const Highlight = @import("highlight.zig").Highlight; pub const Key = @import("key.zig").Key; +pub const KeyMap = @import("KeyMap.zig"); pub const KeyReader = @import("KeyReader.zig"); -pub const key_state = @import("key_state.zig"); pub const RawMode = @import("RawMode.zig"); pub const Row = @import("Row.zig"); pub const search = @import("search.zig").search; -- cgit v1.2.3