summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Uko Kokņevičs2022-01-03 03:16:31 +0200
committerGravatar Uko Kokņevičs2022-01-03 03:16:31 +0200
commitf8f2e8e3bedb94833bbe6cab9c435b33cfbfea14 (patch)
treee6eb2e3c1d790efd93d8fe334785e27278ffd1d7
parentAdded Allocator to Editor.prompt & Editor.promptEx (diff)
downloades-f8f2e8e3bedb94833bbe6cab9c435b33cfbfea14.tar.gz
es-f8f2e8e3bedb94833bbe6cab9c435b33cfbfea14.tar.xz
es-f8f2e8e3bedb94833bbe6cab9c435b33cfbfea14.zip
a bit improved key map
-rw-r--r--src/Buffer.zig11
-rw-r--r--src/Editor.zig14
-rw-r--r--src/KeyMap.zig290
-rw-r--r--src/key_state.zig140
-rw-r--r--src/main.zig2
5 files changed, 309 insertions, 148 deletions
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 {
394 } 394 }
395} 395}
396 396
397pub fn pageDown(self: *Buffer, screenrows: usize) void { 397pub fn pageDown(self: *Buffer, editor: Editor) void {
398 const screenrows = editor.screenrows;
398 self.cy = std.math.clamp( 399 self.cy = std.math.clamp(
399 self.rowoff + screenrows - 1 - self.config.page_overlap, 400 self.rowoff + screenrows - 1 - self.config.page_overlap,
400 0, 401 0,
@@ -406,7 +407,8 @@ pub fn pageDown(self: *Buffer, screenrows: usize) void {
406 } 407 }
407} 408}
408 409
409pub fn pageUp(self: *Buffer, screenrows: usize) void { 410pub fn pageUp(self: *Buffer, editor: Editor) void {
411 const screenrows = editor.screenrows;
410 self.cy = std.math.min(self.rows.items.len, self.rowoff + self.config.page_overlap); 412 self.cy = std.math.min(self.rows.items.len, self.rowoff + self.config.page_overlap);
411 var i: usize = 0; 413 var i: usize = 0;
412 while (i < screenrows) : (i += 1) { 414 while (i < screenrows) : (i += 1) {
@@ -423,8 +425,11 @@ pub fn previousLine(self: *Buffer) void {
423 self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx); 425 self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx);
424} 426}
425 427
426pub fn recenterTopBottom(self: *Buffer, screenrows: usize) void { 428pub fn recenterTopBottom(self: *Buffer, editor: *Editor) !void {
427 // TODO: Currently only recenters 429 // TODO: Currently only recenters
430 try editor.refreshWindowSize();
431
432 const screenrows = editor.screenrows;
428 if (self.cy >= screenrows / 2) { 433 if (self.cy >= screenrows / 2) {
429 self.rowoff = self.cy - screenrows / 2; 434 self.rowoff = self.cy - screenrows / 2;
430 } else { 435 } 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;
6const Buffer = es.Buffer; 6const Buffer = es.Buffer;
7const Editor = @This(); 7const Editor = @This();
8const Key = es.Key; 8const Key = es.Key;
9const KeyMap = es.KeyMap;
9const KeyReader = es.KeyReader; 10const KeyReader = es.KeyReader;
10const KeyState = es.key_state.KeyState;
11const StringBuilder = es.StringBuilder; 11const StringBuilder = es.StringBuilder;
12const StringHashMap = std.StringHashMap; 12const StringHashMap = std.StringHashMap;
13 13
@@ -22,12 +22,15 @@ screencols: usize,
22statusmsg: ?[]u8, 22statusmsg: ?[]u8,
23statusmsg_time: i64, 23statusmsg_time: i64,
24 24
25current_state: KeyState, 25key_map: KeyMap,
26 26
27key_reader: KeyReader, 27key_reader: KeyReader,
28should_exit: bool, 28should_exit: bool,
29 29
30pub fn init(allocator: Allocator) !Editor { 30pub fn init(allocator: Allocator) !Editor {
31 var key_map: ?KeyMap = try KeyMap.defaultMap(allocator);
32 errdefer if (key_map) |*map| map.deinit();
33
31 var self = Editor{ 34 var self = Editor{
32 .allocator = allocator, 35 .allocator = allocator,
33 36
@@ -40,12 +43,13 @@ pub fn init(allocator: Allocator) !Editor {
40 .statusmsg = null, 43 .statusmsg = null,
41 .statusmsg_time = 0, 44 .statusmsg_time = 0,
42 45
43 .current_state = es.key_state.defaultState, 46 .key_map = key_map.?,
44 47
45 .key_reader = KeyReader.init(allocator), 48 .key_reader = KeyReader.init(allocator),
46 .should_exit = false, 49 .should_exit = false,
47 }; 50 };
48 errdefer self.deinit(); 51 errdefer self.deinit();
52 key_map = null;
49 53
50 // Initializes .screenrows and .screencols 54 // Initializes .screenrows and .screencols
51 try self.refreshWindowSize(); 55 try self.refreshWindowSize();
@@ -67,6 +71,8 @@ pub fn deinit(self: *Editor) void {
67 self.allocator.free(statusmsg); 71 self.allocator.free(statusmsg);
68 } 72 }
69 73
74 self.key_map.deinit();
75
70 self.key_reader.deinit(); 76 self.key_reader.deinit();
71 77
72 self.* = undefined; 78 self.* = undefined;
@@ -197,7 +203,7 @@ pub fn openFile(self: *Editor) !void {
197 203
198pub fn processKeypress(self: *Editor) !void { 204pub fn processKeypress(self: *Editor) !void {
199 const key = try self.key_reader.readKey(); 205 const key = try self.key_reader.readKey();
200 try self.current_state(self, self.buffer, key); 206 try self.key_map.keypress(self, self.buffer, key);
201} 207}
202 208
203pub fn prompt(self: *Editor, allocator: Allocator, prompt_str: []const u8) !?[]u8 { 209pub 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 @@
1const es = @import("root");
2const std = @import("std");
3
4const Allocator = std.mem.Allocator;
5const AutoHashMap = std.AutoHashMap;
6const Buffer = es.Buffer;
7const Editor = es.Editor;
8const Key = es.Key;
9const KeyMap = @This();
10
11pub const Error = error{
12 MalformedConfig,
13 MisformedTerminalResponse,
14 StreamTooLong,
15} ||
16 std.fmt.ParseIntError ||
17 std.fs.File.OpenError ||
18 std.fs.File.ReadError ||
19 std.fs.File.WriteError ||
20 std.mem.Allocator.Error ||
21 std.os.GetCwdError ||
22 std.os.RealPathError;
23
24pub const BoundFn = fn (*Editor, *Buffer, Key) Error!void;
25
26const Value = union(enum) {
27 bound_fn: BoundFn,
28 submap: AutoHashMap(Key, Value),
29
30 pub fn deinit(self: *Value) void {
31 switch (self.*) {
32 .bound_fn => {},
33 .submap => |*map| {
34 var it = map.valueIterator();
35 while (it.next()) |value| {
36 value.deinit();
37 }
38
39 map.deinit();
40 },
41 }
42
43 self.* = undefined;
44 }
45};
46
47allocator: Allocator,
48current: ?*AutoHashMap(Key, Value),
49default: ?BoundFn,
50map: AutoHashMap(Key, Value),
51
52pub fn init(allocator: Allocator) KeyMap {
53 return .{
54 .allocator = allocator,
55 .current = null,
56 .default = null,
57 .map = AutoHashMap(Key, Value).init(allocator),
58 };
59}
60
61pub fn defaultMap(allocator: Allocator) !KeyMap {
62 var map = KeyMap.init(allocator);
63 errdefer map.deinit();
64
65 map.default = wrapFn(defaultFn);
66
67 // M-g <*>
68 try map.bind(&.{Key.meta('g'), Key.char('g')}, Buffer.goToLine);
69
70 // M-O <*>
71 try map.bind(&.{Key.meta('O'), Key.char('F')}, Buffer.moveEndOfLine);
72 try map.bind(&.{Key.meta('O'), Key.char('H')}, Buffer.moveBeginningOfLine);
73
74 // C-x C-<*>
75 try map.bind(&.{Key.ctrl('x'), Key.ctrl('b')}, Editor.switchBuffer);
76 try map.bind(&.{Key.ctrl('x'), Key.ctrl('c')}, Editor.saveBuffersExit);
77 try map.bind(&.{Key.ctrl('x'), Key.ctrl('f')}, Editor.openFile);
78 try map.bind(&.{Key.ctrl('x'), Key.ctrl('s')}, Buffer.save);
79
80 // C-x <*>
81 try map.bind(&.{Key.ctrl('x'), Key.char('b')}, Editor.switchBuffer);
82 // TODO: C-x h for help
83 try map.bind(&.{Key.ctrl('x'), Key.char('k')}, Editor.killCurrentBuffer);
84
85 // M-C-<*>
86 try map.bind(&.{Key.meta(Key.ctrl('d'))}, Buffer.backwardDeleteChar);
87
88 // M-<*>
89 // M-g is taken
90 // M-O is taken
91 try map.bind(&.{Key.meta('v')}, Buffer.pageUp);
92
93 // C-<*>
94 try map.bind(&.{Key.ctrl('a')}, Buffer.moveBeginningOfLine);
95 try map.bind(&.{Key.ctrl('b')}, Buffer.backwardChar);
96 try map.bind(&.{Key.ctrl('d')}, Buffer.deleteChar);
97 try map.bind(&.{Key.ctrl('e')}, Buffer.moveEndOfLine);
98 try map.bind(&.{Key.ctrl('f')}, Buffer.forwardChar);
99 try map.bind(&.{Key.ctrl('g')}, Editor.clearStatusMessage);
100 try map.bind(&.{Key.ctrl('i')}, Buffer.indent); // tab
101 try map.bind(&.{Key.ctrl('j')}, Buffer.insertNewline); // line feed
102 try map.bind(&.{Key.ctrl('k')}, Buffer.killLine);
103 try map.bind(&.{Key.ctrl('l')}, Buffer.recenterTopBottom);
104 try map.bind(&.{Key.ctrl('m')}, Buffer.insertNewline); // carriage return
105 try map.bind(&.{Key.ctrl('n')}, Buffer.nextLine);
106 try map.bind(&.{Key.ctrl('p')}, Buffer.previousLine);
107 try map.bind(&.{Key.ctrl('s')}, es.search);
108 // TODO: C-q quotedInsert
109 try map.bind(&.{Key.ctrl('v')}, Buffer.pageDown);
110 // C-x is taken
111
112 // <*>
113 try map.bind(&.{Key.backspace}, Buffer.backwardDeleteChar);
114 try map.bind(&.{Key.delete}, Buffer.deleteChar);
115 try map.bind(&.{Key.down}, Buffer.nextLine);
116 try map.bind(&.{Key.end}, Buffer.moveEndOfLine);
117 try map.bind(&.{Key.home}, Buffer.moveBeginningOfLine);
118 try map.bind(&.{Key.left}, Buffer.backwardChar);
119 try map.bind(&.{Key.page_down}, Buffer.pageDown);
120 try map.bind(&.{Key.page_up}, Buffer.pageUp);
121 try map.bind(&.{Key.right}, Buffer.forwardChar);
122 try map.bind(&.{Key.untab}, Buffer.unindent);
123 try map.bind(&.{Key.up}, Buffer.previousLine);
124
125 return map;
126}
127
128pub fn deinit(self: *KeyMap) void {
129 var it = self.map.valueIterator();
130 while (it.next()) |value| {
131 value.deinit();
132 }
133 self.map.deinit();
134
135 self.* = undefined;
136}
137
138pub fn bind(self: *KeyMap, keys: []const Key, comptime f: anytype) !void {
139 std.debug.assert(keys.len > 0);
140 var map: *AutoHashMap(Key, Value) = &self.map;
141 for (keys[0..keys.len - 1]) |key| {
142 const gop = try map.getOrPut(key);
143 if (!gop.found_existing) {
144 gop.value_ptr.* = .{ .submap = AutoHashMap(Key, Value).init(self.allocator) };
145 }
146
147 switch (gop.value_ptr.*) {
148 .bound_fn => {
149 std.log.err("Attempting to bind a longer chord over a shorter one ({any})", .{keys});
150 return error.KeyBindError;
151 },
152 .submap => |*next_map| map = next_map,
153 }
154 }
155
156 const gop = try map.getOrPut(keys[keys.len - 1]);
157 if (!gop.found_existing) {
158 gop.value_ptr.* = .{ .bound_fn = wrapFn(f) };
159 } else switch (gop.value_ptr.*) {
160 .bound_fn => gop.value_ptr.* = .{ .bound_fn = wrapFn(f) },
161 .submap => {
162 std.log.err("Attempting to bind a shorter chord over a longer one ({any})", .{keys});
163 return error.KeyBindError;
164 },
165 }
166}
167
168pub fn keypress(self: *KeyMap, editor: *Editor, buf: *Buffer, key: Key) !void {
169 const map = self.current orelse &self.map;
170 if (map.getPtr(key)) |value| {
171 switch (value.*) {
172 .bound_fn => |f| {
173 self.current = null;
174 return f(editor, buf, key);
175 },
176 .submap => |*submap| self.current = submap,
177 }
178 } else if (self.current != null) {
179 // TODO: Output the full chord
180 std.log.debug("Unknown chord: ... {}", .{key});
181 try editor.setStatusMessage("Unknown chord: ... {}", .{key});
182 self.current = null;
183 } else if (self.default) |default| {
184 return default(editor, buf, key);
185 } else {
186 std.log.debug("Unknown key: {}", .{key});
187 try editor.setStatusMessage("Unknown key: {}", .{key});
188 }
189}
190
191fn defaultFn(editor: *Editor, buffer: *Buffer, key: Key) !void {
192 if (@enumToInt(key) <= @enumToInt(Key.max_char)) {
193 const char = @intCast(u8, @enumToInt(key));
194 if (std.ascii.isGraph(char) or char == ' ') {
195 try buffer.insertChar(char);
196 return;
197 }
198 }
199
200 std.log.debug("Unknown key: {}", .{key});
201 try editor.setStatusMessage("Unknown key: {}", .{key});
202}
203
204fn wrapFn(comptime f: anytype) BoundFn {
205 comptime {
206 if (@TypeOf(f) == BoundFn) {
207 return f;
208 }
209
210 const fn_info = @typeInfo(@TypeOf(f)).Fn;
211 const should_try = if (fn_info.return_type) |return_type| @typeInfo(return_type) == .ErrorUnion else false;
212
213 const args_info = fn_info.args;
214 var args = [_]type { undefined } ** args_info.len;
215 for (args_info) |arg_info, idx| {
216 args[idx] = arg_info.arg_type.?;
217 }
218
219 if (std.mem.eql(type, &args, &.{*Buffer})) {
220 return struct {
221 pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void {
222 _ = e;
223 _ = k;
224 if (should_try) {
225 _ = try f(b);
226 } else {
227 _ = f(b);
228 }
229 }
230 }.wf;
231 } else if (std.mem.eql(type, &args, &.{*Editor})) {
232 return struct {
233 pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void {
234 _ = b;
235 _ = k;
236 if (should_try) {
237 _ = try f(e);
238 } else {
239 _ = f(e);
240 }
241 }
242 }.wf;
243 } else if (std.mem.eql(type, &args, &.{*Editor, *Buffer})) {
244 return struct {
245 pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void {
246 _ = k;
247 if (should_try) {
248 _ = try f(e, b);
249 } else {
250 _ = f(e, b);
251 }
252 }
253 }.wf;
254 } else if (std.mem.eql(type, &args, &.{*Buffer, *Editor})) {
255 return struct {
256 pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void {
257 _ = k;
258 if (should_try) {
259 _ = try f(b, e);
260 } else {
261 _ = f(b, e);
262 }
263 }
264 }.wf;
265 } else if (std.mem.eql(type, &args, &.{*Buffer, Editor})) {
266 return struct {
267 pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void {
268 _ = k;
269 if (should_try) {
270 _ = try f(b, e.*);
271 } else {
272 _ = f(b, e.*);
273 }
274 }
275 }.wf;
276 } else if (std.mem.eql(type, &args, &.{*Editor, *Buffer, Key})) {
277 return struct {
278 pub fn wf(e: *Editor, b: *Buffer, k: Key) Error!void {
279 if (should_try) {
280 _ = try f(e, b, k);
281 } else {
282 _ = f(e, b, k);
283 }
284 }
285 }.wf;
286 }
287
288 @compileError("How to wrap " ++ @typeName(@TypeOf(f)));
289 }
290}
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 @@
1const es = @import("root");
2const std = @import("std");
3
4const Buffer = es.Buffer;
5const Editor = es.Editor;
6const Key = es.Key;
7
8pub const Error = error{
9 MalformedConfig,
10 MisformedTerminalResponse,
11 StreamTooLong,
12} ||
13 std.mem.Allocator.Error ||
14 std.fmt.ParseIntError ||
15 std.fs.File.OpenError ||
16 std.fs.File.ReadError ||
17 std.fs.File.WriteError ||
18 std.os.GetCwdError ||
19 std.os.RealPathError;
20pub const KeyState = fn (*Editor, *Buffer, Key) Error!void;
21
22fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void {
23 editor.current_state = defaultState;
24 editor.clearStatusMessage();
25
26 switch (key) {
27 // ========== <*> ==========
28 Key.char('g') => try buf.goToLine(editor),
29
30 else => {
31 std.log.debug("Unknown chord: M-g {}", .{key});
32 try editor.setStatusMessage("Unknown chord: M-g {}", .{key});
33 },
34 }
35}
36
37fn mOState(editor: *Editor, buf: *Buffer, key: Key) Error!void {
38 editor.current_state = defaultState;
39 editor.clearStatusMessage();
40
41 switch (key) {
42 // ========== <*> ==========
43 Key.char('F') => buf.moveEndOfLine(),
44 Key.char('H') => buf.moveBeginningOfLine(),
45
46 else => {
47 std.log.debug("Unknown chord: M-O {}", .{key});
48 try editor.setStatusMessage("Unknown chord: M-O {}", .{key});
49 },
50 }
51}
52
53fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void {
54 editor.current_state = defaultState;
55 editor.clearStatusMessage();
56
57 switch (key) {
58 // ========== C-<*> ==========
59 Key.ctrl('b'), Key.char('b') => try editor.switchBuffer(),
60 Key.ctrl('c') => try editor.saveBuffersExit(),
61 Key.ctrl('f') => try editor.openFile(),
62 Key.ctrl('g') => {},
63 Key.ctrl('s') => try buf.save(editor),
64
65 // ========== <*> ==========
66 Key.char('k') => _ = try editor.killCurrentBuffer(),
67
68 else => {
69 std.log.debug("Unknown chord: C-x {}", .{key});
70 try editor.setStatusMessage("Unknown chord: C-x {}", .{key});
71 },
72 }
73}
74
75pub fn defaultState(editor: *Editor, buf: *Buffer, key: Key) Error!void {
76 switch (key) {
77 // ========== M-C-<*> ==========
78 Key.meta(Key.ctrl('d')), Key.backspace => try buf.backwardDeleteChar(),
79
80 // ========== M-<*> ==========
81 Key.meta('g') => {
82 editor.current_state = mgState;
83 try editor.setStatusMessage("M-g-", .{});
84 },
85 Key.meta('O') => editor.current_state = mOState,
86 Key.meta('v'), Key.page_up => buf.pageUp(editor.screenrows),
87
88 // ========== C-<*> ==========
89 Key.ctrl('a'), Key.home => buf.moveBeginningOfLine(),
90 Key.ctrl('b'), Key.left => buf.backwardChar(),
91 Key.ctrl('d'), Key.delete => try buf.deleteChar(),
92 Key.ctrl('e'), Key.end => buf.moveEndOfLine(),
93 Key.ctrl('f'), Key.right => buf.forwardChar(),
94 Key.ctrl('g') => editor.clearStatusMessage(),
95
96 // TODO: C-h help
97
98 // tab
99 Key.ctrl('i') => try buf.indent(),
100 // line feed
101 Key.ctrl('j') => try buf.insertNewline(),
102 Key.ctrl('k') => try buf.killLine(),
103
104 Key.ctrl('l') => {
105 try editor.refreshWindowSize();
106 buf.recenterTopBottom(editor.screenrows);
107 },
108
109 // carriage return
110 Key.ctrl('m') => try buf.insertNewline(),
111 Key.ctrl('n'), Key.down => buf.nextLine(),
112 Key.ctrl('p'), Key.up => buf.previousLine(),
113 Key.ctrl('s') => try es.search(editor, buf),
114
115 // TODO: C-q quotedInsert
116
117 Key.ctrl('v'), Key.page_down => buf.pageDown(editor.screenrows),
118
119 Key.ctrl('x') => {
120 editor.current_state = cxState;
121 try editor.setStatusMessage("C-x-", .{});
122 },
123
124 // ========== <*> ==========
125 Key.untab => try buf.unindent(),
126
127 else => {
128 if (@enumToInt(key) <= @enumToInt(Key.max_char)) {
129 const char = @intCast(u8, @enumToInt(key));
130 if (std.ascii.isGraph(char) or std.ascii.isSpace(char)) {
131 try buf.insertChar(char);
132 return;
133 }
134 }
135
136 std.log.debug("Unknown key: {}", .{key});
137 try editor.setStatusMessage("Unknown key: {}", .{key});
138 },
139 }
140}
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");
5pub const files = @import("files.zig"); 5pub const files = @import("files.zig");
6pub const Highlight = @import("highlight.zig").Highlight; 6pub const Highlight = @import("highlight.zig").Highlight;
7pub const Key = @import("key.zig").Key; 7pub const Key = @import("key.zig").Key;
8pub const KeyMap = @import("KeyMap.zig");
8pub const KeyReader = @import("KeyReader.zig"); 9pub const KeyReader = @import("KeyReader.zig");
9pub const key_state = @import("key_state.zig");
10pub const RawMode = @import("RawMode.zig"); 10pub const RawMode = @import("RawMode.zig");
11pub const Row = @import("Row.zig"); 11pub const Row = @import("Row.zig");
12pub const search = @import("search.zig").search; 12pub const search = @import("search.zig").search;