const es = @import("root"); const std = @import("std"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Buffer = es.Buffer; const Editor = es.Editor; const FormatOptions = std.fmt.FormatOptions; const HashMap = std.HashMap; const Key = es.Key; const KeyMap = @This(); const Wyhash = std.hash.Wyhash; 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; pub const BoundFnPtr = *const BoundFn; const Context = struct { pub fn hash(self: Context, key: []const Key) u64 { _ = self; var hasher = Wyhash.init(key.len); std.hash.autoHashStrat(&hasher, key, .Deep); return hasher.final(); } pub fn eql(self: Context, a: []const Key, b: []const Key) bool { _ = self; return std.mem.eql(Key, a, b); } }; const Keys = struct { keys: []const Key, pub fn format( self: Keys, comptime fmt: []const u8, options: FormatOptions, writer: anytype, ) @TypeOf(writer).Error!void { var is_first = true; for (self.keys) |key| { if (is_first) { is_first = false; } else { try std.fmt.formatBuf(" ", options, writer); } try Key.format(key, fmt, options, writer); } } }; const RawMap = HashMap([]const Key, Value, Context, std.hash_map.default_max_load_percentage); const Value = union(enum) { bound_fn: BoundFnPtr, chord: void, }; allocator: Allocator, current_chord: ArrayList(Key), default: ?BoundFnPtr, raw_map: RawMap, pub fn init(allocator: Allocator) KeyMap { return .{ .allocator = allocator, .current_chord = ArrayList(Key).init(allocator), .default = null, .raw_map = RawMap.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-<*> try map.bind(&.{Key.meta(Key.backspace)}, Buffer.backwardDeleteWord); try map.bind(&.{Key.meta(Key.delete)}, Buffer.deleteWord); try map.bind(&.{Key.meta(Key.down)}, Buffer.forwardParagraph); try map.bind(&.{Key.meta(Key.end)}, Buffer.endOfBuffer); try map.bind(&.{Key.meta(Key.home)}, Buffer.beginningOfBuffer); try map.bind(&.{Key.meta(Key.left)}, Buffer.backwardWord); try map.bind(&.{Key.meta(Key.right)}, Buffer.forwardWord); try map.bind(&.{Key.meta(Key.up)}, Buffer.backwardParagraph); try map.bind(&.{Key.meta('b')}, Buffer.backwardWord); try map.bind(&.{Key.meta('d')}, Buffer.deleteWord); try map.bind(&.{Key.meta('f')}, Buffer.forwardWord); // M-g is taken try map.bind(&.{Key.meta('n')}, Buffer.forwardParagraph); // M-O is taken try map.bind(&.{Key.meta('p')}, Buffer.backwardParagraph); try map.bind(&.{Key.meta('v')}, Buffer.pageUp); // C-<*> try map.bind(&.{Key.ctrl(Key.backspace)}, Buffer.backwardDeleteWord); try map.bind(&.{Key.ctrl(Key.delete)}, Buffer.deleteWord); try map.bind(&.{Key.ctrl(Key.down)}, Buffer.forwardParagraph); try map.bind(&.{Key.ctrl(Key.end)}, Buffer.endOfBuffer); try map.bind(&.{Key.ctrl(Key.home)}, Buffer.beginningOfBuffer); try map.bind(&.{Key.ctrl(Key.left)}, Buffer.backwardWord); try map.bind(&.{Key.ctrl(Key.right)}, Buffer.forwardWord); try map.bind(&.{Key.ctrl(Key.up)}, Buffer.backwardParagraph); 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 { self.current_chord.deinit(); var it = self.raw_map.keyIterator(); while (it.next()) |key| { self.allocator.free(key.*); } self.raw_map.deinit(); self.* = undefined; } pub fn bind(self: *KeyMap, keys: []const Key, comptime f: anytype) !void { std.debug.assert(keys.len > 0); var idx: usize = 0; while (idx < keys.len - 1) : (idx += 1) { const subseq = try self.allocator.dupe(Key, keys[0 .. idx + 1]); errdefer self.allocator.free(subseq); const gop = try self.raw_map.getOrPut(subseq); if (!gop.found_existing) { gop.value_ptr.* = .chord; } else { defer self.allocator.free(subseq); switch (gop.value_ptr.*) { .bound_fn => { const longer_keys = Keys{ .keys = keys }; const shorter_keys = Keys{ .keys = keys }; std.log.err( "Attempting to bind a longer chord ({}) over a shorter one ({})", .{ longer_keys, shorter_keys }, ); return error.KeyBindError; }, .chord => {}, } } } const seq = try self.allocator.dupe(Key, keys); errdefer self.allocator.free(seq); const gop = try self.raw_map.getOrPut(seq); if (!gop.found_existing) { gop.value_ptr.* = .{ .bound_fn = wrapFn(f) }; } else { defer self.allocator.free(seq); // TODO: Maybe I wanna error on rebinding all the time? :thinking: switch (gop.value_ptr.*) { .bound_fn => gop.value_ptr.* = .{ .bound_fn = wrapFn(f) }, .chord => { const keys_wrap = Keys{ .keys = keys }; std.log.err( "Attempting to bind a shorter chord ({}) over longer one(s)", .{keys_wrap}, ); return error.KeyBindError; }, } } } pub fn keypress(self: *KeyMap, editor: *Editor, buf: *Buffer, key: Key) !void { try self.current_chord.append(key); const keys = Keys{ .keys = self.current_chord.items }; if (self.raw_map.get(self.current_chord.items)) |value| { switch (value) { .bound_fn => |f| { self.current_chord.clearRetainingCapacity(); editor.clearStatusMessage(); try f(editor, buf, key); }, .chord => try editor.setStatusMessage("{}-", .{keys}), } } else if (self.current_chord.items.len > 1) { std.log.debug("Unknown chord: {}", .{keys}); try editor.setStatusMessage("Unknown chord: {}", .{keys}); self.current_chord.clearRetainingCapacity(); } else if (self.default) |default| { try default(editor, buf, key); self.current_chord.clearRetainingCapacity(); } else { std.log.debug("Unknown key: {}", .{key}); try editor.setStatusMessage("Unknown key: {}", .{key}); self.current_chord.clearRetainingCapacity(); } } fn defaultFn(editor: *Editor, buffer: *Buffer, key: Key) !void { if (@intFromEnum(key) <= @intFromEnum(Key.max_char)) { const char: u8 = @intCast(@intFromEnum(key)); if (std.ascii.isPrint(char)) { try buffer.insertChar(char); return; } } std.log.debug("Unknown key: {}", .{key}); try editor.setStatusMessage("Unknown key: {}", .{key}); } // I think there's a bug in Zig's std.mem.eql fn typeEql(comptime a: []const type, comptime b: []const type) bool { if (a.len != b.len) return false; if (a.len == 0 or a.ptr == b.ptr) return true; for (a, b) |a_elem, b_elem| { if (a_elem != b_elem) return false; } return true; } 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 params_info = fn_info.params; var params = [_]type{undefined} ** params_info.len; for (params_info, 0..) |param_info, idx| { params[idx] = param_info.type.?; } if (typeEql(¶ms, &.{*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 (typeEql(¶ms, &.{*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 (typeEql(¶ms, &.{ *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 (typeEql(¶ms, &.{ *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 (typeEql(¶ms, &.{ *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 (typeEql(¶ms, &.{ *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))); } }