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))); } }