summaryrefslogtreecommitdiff
path: root/src/Editor.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Editor.zig')
-rw-r--r--src/Editor.zig461
1 files changed, 461 insertions, 0 deletions
diff --git a/src/Editor.zig b/src/Editor.zig
new file mode 100644
index 0000000..eecbaa7
--- /dev/null
+++ b/src/Editor.zig
@@ -0,0 +1,461 @@
1const linux = std.os.linux;
2const std = @import("std");
3
4const Allocator = std.mem.Allocator;
5const ArrayList = std.ArrayList;
6const Buffer = @import("Buffer.zig");
7const Editor = @This();
8const Key = @import("key.zig").Key;
9const key_state = @import("key_state.zig");
10const KeyState = key_state.KeyState;
11const STDIN_FILENO = std.os.STDIN_FILENO;
12const StringBuilder = @import("StringBuilder.zig");
13const StringHashMap = std.StringHashMap;
14
15allocator: Allocator,
16
17buffers: StringHashMap(Buffer),
18buffer: *Buffer,
19
20screenrows: usize,
21screencols: usize,
22
23statusmsg: ?[]u8,
24statusmsg_time: i64,
25
26current_state: KeyState,
27
28should_exit: bool,
29
30pub fn init(allocator: Allocator) !Editor {
31 var self = Editor{
32 .allocator = allocator,
33
34 .buffers = StringHashMap(Buffer).init(allocator),
35 .buffer = undefined,
36
37 .screenrows = undefined,
38 .screencols = undefined,
39
40 .statusmsg = null,
41 .statusmsg_time = 0,
42
43 .current_state = key_state.defaultState,
44
45 .should_exit = false,
46 };
47 errdefer self.deinit();
48
49 // Initializes .screenrows and .screencols
50 try self.refreshWindowSize();
51
52 self.buffer = try self.putBuffer("*scratch*");
53
54 return self;
55}
56
57pub fn deinit(self: *Editor) void {
58 var buf_iterator = self.buffers.iterator();
59 while (buf_iterator.next()) |kv| {
60 self.allocator.free(kv.key_ptr.*);
61 kv.value_ptr.deinit();
62 }
63 self.buffers.deinit();
64
65 if (self.statusmsg) |statusmsg| {
66 self.allocator.free(statusmsg);
67 }
68
69 self.* = undefined;
70}
71
72pub fn clearStatusMessage(self: *Editor) void {
73 if (self.statusmsg) |statusmsg| {
74 self.statusmsg = null;
75 self.allocator.free(statusmsg);
76 }
77}
78
79pub fn getBuffer(self: Editor, name: []const u8) ?*Buffer {
80 return self.buffers.getPtr(name);
81}
82
83pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer {
84 const get_or_put_res = try self.buffers.getOrPut(name);
85 if (get_or_put_res.found_existing) {
86 return get_or_put_res.value_ptr;
87 }
88
89 get_or_put_res.key_ptr.* = try self.allocator.dupe(u8, name);
90 errdefer self.allocator.free(get_or_put_res.key_ptr.*);
91
92 get_or_put_res.value_ptr.* = try Buffer.init(self.allocator, name);
93 errdefer get_or_put_res.value_ptr.deinit();
94
95 return get_or_put_res.value_ptr;
96}
97
98pub fn hasBuffer(self: Editor, name: []const u8) bool {
99 return self.getBuffer(name) != null;
100}
101
102/// Returns true if killed, false if didn't.
103pub fn killCurrentBuffer(self: *Editor) !bool {
104 if (self.buffer.dirty) {
105 if (!try self.promptYN("Unsaved changes, kill anyways?")) {
106 return false;
107 }
108 }
109
110 const entry_to_kill = self.buffers.fetchRemove(self.buffer.name).?;
111 self.allocator.free(entry_to_kill.key);
112 entry_to_kill.value.deinit();
113
114 if (self.buffers.valueIterator().next()) |buffer| {
115 self.buffer = buffer;
116 } else {
117 self.buffer = try self.putBuffer("*scratch*");
118 }
119
120 return true;
121}
122
123pub fn open(self: *Editor, fname: []const u8) !void {
124 if (self.hasBuffer(fname)) {
125 if (!try self.promptYN("A file with such name is already open. Open anyways?")) {
126 return;
127 }
128 }
129
130 self.buffer = try self.getOrPutBuffer(fname);
131 // TODO: If already was dirty, ask again
132 self.buffer.has_file = true;
133
134 try self.buffer.selectSyntaxHighlighting();
135 self.buffer.deleteAllRows();
136
137 const file = std.fs.cwd().openFile(fname, .{ .read = true }) catch |err| switch (err) {
138 error.FileNotFound => {
139 try self.setStatusMessage("Creating a new file...", .{});
140 self.buffer.dirty = true;
141 const file = try std.fs.cwd().createFile(fname, .{ .read = true });
142 file.close();
143 return;
144 },
145 else => return err,
146 };
147 defer file.close();
148
149 var buffered_reader = std.io.bufferedReader(file.reader());
150 const reader = buffered_reader.reader();
151
152 while (try reader.readUntilDelimiterOrEofAlloc(self.allocator, '\n', 4096)) |line| {
153 defer self.allocator.free(line);
154
155 const trimmed = std.mem.trim(u8, line, "\r\n");
156 try self.buffer.appendRow(trimmed);
157 }
158
159 self.buffer.dirty = false;
160}
161
162pub fn openFile(self: *Editor) !void {
163 const fname_opt = try self.prompt("File name");
164 if (fname_opt) |fname| {
165 defer self.allocator.free(fname);
166 return self.open(fname);
167 }
168}
169
170pub fn processKeypress(self: *Editor) !void {
171 const key = try readKey();
172 try self.current_state(self, self.buffer, key);
173}
174
175pub fn prompt(self: *Editor, prompt_str: []const u8) !?[]u8 {
176 return self.promptEx(void, error{}, prompt_str, null, {});
177}
178
179pub fn promptEx(
180 self: *Editor,
181 comptime CallbackData: type,
182 comptime CallbackError: type,
183 prompt_str: []const u8,
184 callback: ?PromptCallback(CallbackData, CallbackError),
185 cb_data: CallbackData,
186) !?[]u8 {
187 var buf = ArrayList(u8).init(self.allocator);
188 defer buf.deinit();
189
190 while (true) {
191 try self.setStatusMessage("{s}: {s}", .{prompt_str, buf.items});
192 try self.refreshScreen();
193
194 // TODO: Navigation
195 // TODO: Draw the cursor
196 const key = try readKey();
197 switch (key) {
198 Key.delete, Key.backspace => _ = buf.popOrNull(),
199 Key.ctrl('g') => {
200 try self.setStatusMessage("Cancelled", .{});
201 if (callback) |cb| {
202 try cb(self, buf.items, key, cb_data);
203 }
204
205 return null;
206 },
207 Key.return_ => {
208 self.clearStatusMessage();
209 if (callback) |cb| {
210 try cb(self, buf.items, key, cb_data);
211 }
212
213 return buf.toOwnedSlice();
214 },
215 else => if (@enumToInt(key) < @enumToInt(Key.max_char)) {
216 const key_char = @intCast(u8, @enumToInt(key));
217 if (std.ascii.isSpace(key_char) or std.ascii.isGraph(key_char)) {
218 try buf.append(key_char);
219 }
220 } // else ??
221 }
222
223 if (callback) |cb| {
224 try cb(self, buf.items, key, cb_data);
225 }
226 }
227}
228
229pub fn PromptCallback(comptime Data: type, comptime Error: type) type {
230 return fn(*Editor, []const u8, Key, Data) Error!void;
231}
232
233pub fn promptYN(self: *Editor, prompt_str: []const u8) !bool {
234 const full_prompt = try std.fmt.allocPrint(self.allocator, "{s} (Y/N)", .{prompt_str});
235 defer self.allocator.free(full_prompt);
236
237 var response = try self.prompt(full_prompt);
238 defer if (response) |str| self.allocator.free(str);
239 // TODO: This can be improved
240 while (response == null
241 or (response.?[0] != 'y' and response.?[0] != 'Y' and response.?[0] != 'n' and response.?[0] != 'N')) {
242 if (response) |str| self.allocator.free(str);
243 response = try self.prompt(full_prompt);
244 }
245
246 return response.?[0] == 'y' or response.?[0] == 'Y';
247}
248
249pub fn putBuffer(self: *Editor, buf_name: []const u8) !*Buffer {
250 const duped_name = try self.allocator.dupe(u8, buf_name);
251 errdefer self.allocator.free(duped_name);
252
253 if (try self.buffers.fetchPut(duped_name, try Buffer.init(self.allocator, duped_name))) |prev_kv| {
254 self.allocator.free(prev_kv.key);
255 prev_kv.value.deinit();
256 }
257
258 return self.buffers.getPtr(duped_name).?;
259}
260
261pub fn refreshScreen(self: *Editor) !void {
262 self.buffer.scroll(self.screenrows, self.screencols);
263
264 var sb = StringBuilder.init(self.allocator);
265 const writer = sb.writer();
266 defer sb.deinit();
267
268 try writer.writeAll("\x1b[?25l\x1b[H");
269
270 try self.buffer.drawRows(writer, self.screenrows, self.screencols);
271 try self.buffer.drawStatusBar(writer, self.screencols);
272 try self.drawMessageBar(writer);
273
274 try writer.print("\x1b[{};{}H", .{
275 self.buffer.cy - self.buffer.rowoff + 1,
276 self.buffer.rx - self.buffer.coloff + 1 + self.buffer.lineNumberDigits(),
277 });
278 try writer.writeAll("\x1b[?25h");
279
280 try std.io.getStdOut().writeAll(sb.seeSlice());
281}
282
283pub fn refreshWindowSize(self: *Editor) !void {
284 try getWindowSize(&self.screenrows, &self.screencols);
285 self.screenrows -= 2;
286}
287
288pub fn saveBuffersExit(self: *Editor) !void {
289 while (self.buffers.count() > 1 or self.buffer.dirty) {
290 if (!try self.killCurrentBuffer()) {
291 return;
292 }
293 }
294
295 try std.io.getStdOut().writeAll("\x1b[2J\x1b[H");
296 self.should_exit = true;
297}
298
299pub fn setStatusMessage(self: *Editor, comptime fmt: []const u8, args: anytype) !void {
300 // Get new resources
301 var new_msg = try std.fmt.allocPrint(self.allocator, fmt, args);
302 errdefer self.allocator.free(new_msg);
303
304 // Get rid of old resources (no errors)
305 if (self.statusmsg) |old_msg| {
306 self.statusmsg = null;
307 self.allocator.free(old_msg);
308 }
309
310 // Assign new resources (no errors)
311 self.statusmsg = new_msg;
312 self.statusmsg_time = std.time.milliTimestamp();
313}
314
315pub fn switchBuffer(self: *Editor) !void {
316 // TODO: completion
317 const bufname_opt = try self.prompt("Switch to buffer");
318 if (bufname_opt) |bufname| {
319 defer self.allocator.free(bufname);
320
321 if (self.getBuffer(bufname)) |buffer| {
322 self.buffer = buffer;
323 } else {
324 try self.setStatusMessage("There is no buffer named '{s}'!", .{bufname});
325 }
326 }
327}
328
329fn drawMessageBar(self: Editor, writer: anytype) !void {
330 try writer.writeAll("\x1b[K");
331 if (self.statusmsg == null) {
332 return;
333 }
334
335 if (self.statusmsg.?.len != 0 and std.time.milliTimestamp() - self.statusmsg_time < 5 * std.time.ms_per_s) {
336 try writer.writeAll(self.statusmsg.?[0..(std.math.min(self.statusmsg.?.len, self.screencols))]);
337 }
338}
339
340fn getCursorPosition(row: *usize, col: *usize) !void {
341 const std_out = std.io.getStdOut();
342 try std_out.writeAll("\x1b[6n\r\n");
343
344 const std_in = std.io.getStdIn().reader();
345 var buf = [_]u8 { undefined } ** 32;
346 var response = std_in.readUntilDelimiter(&buf, 'R') catch |err| switch (err) {
347 error.EndOfStream => return error.MisformedTerminalResponse,
348 error.StreamTooLong => return error.MisformedTerminalResponse,
349 else => return @errSetCast(std.os.ReadError, err),
350 };
351
352 if (response.len < 2 or response[0] != '\x1b' or response[1] != '[') {
353 return error.MisformedTerminalResponse;
354 }
355
356 response = response[2..];
357
358 var split_it = std.mem.split(u8, response, ";");
359 row.* = parseUnsignedOptDefault(usize, split_it.next(), 10, 1) catch return error.MisformedTerminalResponse;
360 col.* = parseUnsignedOptDefault(usize, split_it.next(), 10, 1) catch return error.MisformedTerminalResponse;
361 if (split_it.next()) |_| {
362 return error.MisformedTerminalResponse;
363 }
364}
365
366fn getWindowSize(rows: *usize, cols: *usize) !void {
367 var ws: linux.winsize = undefined;
368 const rc = linux.ioctl(STDIN_FILENO, linux.T.IOCGWINSZ, @ptrToInt(&ws));
369 switch (linux.getErrno(rc)) {
370 .SUCCESS => {
371 cols.* = ws.ws_col;
372 rows.* = ws.ws_row;
373 },
374 else => {
375 const std_out = std.io.getStdOut();
376 try std_out.writeAll("\x1b[999C\x1b[999B");
377 return getCursorPosition(rows, cols);
378 },
379 }
380}
381
382fn parseUnsignedOptDefault(comptime T: type, buf_opt: ?[]const u8, radix: u8, default: T) !T {
383 if (buf_opt) |buf| {
384 return std.fmt.parseUnsigned(T, buf, radix);
385 } else {
386 return default;
387 }
388}
389
390fn readKey() !Key {
391 const std_in = std.io.getStdIn();
392
393 var buf = [_]u8{undefined} ** 3;
394 // No we do not care about possible EOF on stdin, don't run the editor with
395 // redirected stdin
396 while (1 != try std_in.read(buf[0..1])) {
397 try std.os.sched_yield(); // :)
398 }
399
400 if (buf[0] != '\x1b') {
401 return @intToEnum(Key, buf[0]);
402 }
403
404 if (1 != try std_in.read(buf[0..1])) {
405 return Key.escape;
406 }
407
408 if (buf[0] == '[') {
409 if (1 != try std_in.read(buf[1..2])) {
410 return Key.meta('[');
411 }
412
413 if (buf[1] >= '0' and buf[1] <= '9') {
414 if (1 != try std_in.read(buf[2..3])) {
415 // TODO: Multiple key return support?
416 return Key.meta('[');
417 }
418
419 if (buf[2] == '~') {
420 return switch (buf[1]) {
421 '1' => Key.home,
422 '3' => Key.delete,
423 '4' => Key.end,
424 '5' => Key.page_up,
425 '6' => Key.page_down,
426 '7' => Key.home,
427 '8' => Key.end,
428 // TODO: Multiple key return support
429 else => Key.meta('['),
430 };
431 } else {
432 // TODO: Multiple key return support
433 return Key.meta('[');
434 }
435 } else {
436 return switch (buf[1]) {
437 'A' => Key.up,
438 'B' => Key.down,
439 'C' => Key.right,
440 'D' => Key.left,
441 'F' => Key.end,
442 'H' => Key.home,
443 // TODO: Multiple key return support
444 else => Key.meta('['),
445 };
446 }
447 } else if (buf[0] == 'O') {
448 if (1 != try std_in.read(buf[1..2])) {
449 return Key.meta('O');
450 }
451
452 return switch (buf[1]) {
453 'F' => Key.end,
454 'H' => Key.home,
455 // TODO: Multiple key return support
456 else => Key.meta('O'),
457 };
458 } else {
459 return Key.meta(buf[0]);
460 }
461}