const es = @import("root"); const std = @import("std"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Buffer = es.Buffer; const Editor = @This(); const Key = es.Key; const KeyState = es.key_state.KeyState; const StringBuilder = es.StringBuilder; const StringHashMap = std.StringHashMap; allocator: Allocator, buffers: StringHashMap(Buffer), buffer: *Buffer, screenrows: usize, screencols: usize, statusmsg: ?[]u8, statusmsg_time: i64, current_state: KeyState, key_buffer: ArrayList(Key), should_exit: bool, pub fn init(allocator: Allocator) !Editor { var self = Editor{ .allocator = allocator, .buffers = StringHashMap(Buffer).init(allocator), .buffer = undefined, .screenrows = undefined, .screencols = undefined, .statusmsg = null, .statusmsg_time = 0, .current_state = es.key_state.defaultState, .key_buffer = ArrayList(Key).init(allocator), .should_exit = false, }; errdefer self.deinit(); // Initializes .screenrows and .screencols try self.refreshWindowSize(); self.buffer = try self.putBuffer("*scratch*"); return self; } pub fn deinit(self: *Editor) void { var buf_iterator = self.buffers.iterator(); while (buf_iterator.next()) |kv| { self.allocator.free(kv.key_ptr.*); kv.value_ptr.deinit(); } self.buffers.deinit(); if (self.statusmsg) |statusmsg| { self.allocator.free(statusmsg); } self.key_buffer.deinit(); self.* = undefined; } pub fn clearStatusMessage(self: *Editor) void { if (self.statusmsg) |statusmsg| { self.statusmsg = null; self.allocator.free(statusmsg); } } pub fn getBuffer(self: Editor, name: []const u8) ?*Buffer { return self.buffers.getPtr(name); } pub fn getBufferByPath(self: Editor, path: []const u8) ?*Buffer { var it = self.buffers.valueIterator(); while (it.next()) |buffer| { if (buffer.file_path) |buffer_path| { if (std.mem.eql(u8, path, buffer_path)) { return buffer; } } } return null; } pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer { const get_or_put_res = try self.buffers.getOrPut(name); if (get_or_put_res.found_existing) { return get_or_put_res.value_ptr; } get_or_put_res.key_ptr.* = try self.allocator.dupe(u8, name); errdefer self.allocator.free(get_or_put_res.key_ptr.*); get_or_put_res.value_ptr.* = try Buffer.init(self.allocator, name); errdefer get_or_put_res.value_ptr.deinit(); return get_or_put_res.value_ptr; } pub fn getUniqueBufferName(self: Editor, allocator: Allocator, base_name: []const u8) ![]u8 { var unique_name = try allocator.dupe(u8, base_name); var idx: usize = 1; while (self.hasBuffer(unique_name)) : (idx += 1) { allocator.free(unique_name); unique_name = try std.fmt.allocPrint(allocator, "{s}<{}>", .{ base_name, idx }); } return unique_name; } pub fn hasBuffer(self: Editor, name: []const u8) bool { return self.getBuffer(name) != null; } /// Returns true if killed, false if didn't. pub fn killCurrentBuffer(self: *Editor) !bool { if (self.buffer.dirty) { if (!try self.promptYN("Unsaved changes, kill anyways?")) { return false; } } const entry_to_kill = self.buffers.fetchRemove(self.buffer.short_name).?; self.allocator.free(entry_to_kill.key); entry_to_kill.value.deinit(); if (self.buffers.valueIterator().next()) |buffer| { self.buffer = buffer; } else { self.buffer = try self.putBuffer("*scratch*"); } return true; } pub fn open(self: *Editor, name: []const u8) !void { const file_path = try es.files.resolvePath(self.allocator, name); defer self.allocator.free(file_path); if (self.getBufferByPath(file_path)) |buffer| { self.buffer = buffer; return; } const file_name = std.fs.path.basename(file_path); self.buffer = try self.putNewBuffer(file_name); self.buffer.file_path = try self.allocator.dupe(u8, file_path); try self.buffer.selectSyntaxHighlighting(); std.debug.assert(self.buffer.rows.items.len == 0); const file = std.fs.openFileAbsolute(file_path, .{ .read = true }) catch |err| switch (err) { error.FileNotFound => { try self.setStatusMessage("Will create a new file on save...", .{}); self.buffer.dirty = true; return; }, else => return err, }; defer file.close(); var buffered_reader = std.io.bufferedReader(file.reader()); const reader = buffered_reader.reader(); // TODO: Limiting lines to 4096 characters while (try reader.readUntilDelimiterOrEofAlloc(self.allocator, '\n', 4096)) |line| { defer self.allocator.free(line); const trimmed = std.mem.trim(u8, line, "\r\n"); try self.buffer.appendRow(trimmed); } self.buffer.dirty = false; } pub fn openFile(self: *Editor) !void { const fname_opt = try self.prompt("File name"); if (fname_opt) |fname| { defer self.allocator.free(fname); return self.open(fname); } } pub fn processKeypress(self: *Editor) !void { const key = try self.readKey(); try self.current_state(self, self.buffer, key); } pub fn prompt(self: *Editor, prompt_str: []const u8) !?[]u8 { return self.promptEx(void, error{}, prompt_str, null, {}); } pub fn promptEx( self: *Editor, comptime CallbackData: type, comptime CallbackError: type, prompt_str: []const u8, callback: ?PromptCallback(CallbackData, CallbackError), cb_data: CallbackData, ) !?[]u8 { var buf = ArrayList(u8).init(self.allocator); defer buf.deinit(); while (true) { try self.setStatusMessage("{s}: {s}", .{ prompt_str, buf.items }); try self.refreshScreen(); // TODO: Navigation // TODO: Draw the cursor const key = try self.readKey(); switch (key) { Key.delete, Key.backspace => _ = buf.popOrNull(), Key.ctrl('g') => { try self.setStatusMessage("Cancelled", .{}); if (callback) |cb| { try cb(self, buf.items, key, cb_data); } return null; }, Key.return_ => { self.clearStatusMessage(); if (callback) |cb| { try cb(self, buf.items, key, cb_data); } return buf.toOwnedSlice(); }, else => if (@enumToInt(key) < @enumToInt(Key.max_char)) { const key_char = @intCast(u8, @enumToInt(key)); if (std.ascii.isSpace(key_char) or std.ascii.isGraph(key_char)) { try buf.append(key_char); } }, // else ?? } if (callback) |cb| { try cb(self, buf.items, key, cb_data); } } } pub fn PromptCallback(comptime Data: type, comptime Error: type) type { return fn (*Editor, []const u8, Key, Data) Error!void; } pub fn promptYN(self: *Editor, prompt_str: []const u8) !bool { const full_prompt = try std.fmt.allocPrint(self.allocator, "{s} (Y/N)", .{prompt_str}); defer self.allocator.free(full_prompt); var response = try self.prompt(full_prompt); defer if (response) |str| self.allocator.free(str); // TODO: This can be improved while (response == null or response.?.len == 0 or (response.?[0] != 'y' and response.?[0] != 'Y' and response.?[0] != 'n' and response.?[0] != 'N') ) { if (response) |str| self.allocator.free(str); response = try self.prompt(full_prompt); } return response.?[0] == 'y' or response.?[0] == 'Y'; } pub fn putBuffer(self: *Editor, buf_name: []const u8) !*Buffer { const duped_name = try self.allocator.dupe(u8, buf_name); errdefer self.allocator.free(duped_name); if (try self.buffers.fetchPut(duped_name, try Buffer.init(self.allocator, duped_name))) |prev_kv| { self.allocator.free(prev_kv.key); prev_kv.value.deinit(); } // TODO: This feels inefficient return self.buffers.getPtr(duped_name).?; } /// This always adds a new buffer, name might be modified to avoid clashes. pub fn putNewBuffer(self: *Editor, name: []const u8) !*Buffer { const unique_name = try self.getUniqueBufferName(self.allocator, name); errdefer self.allocator.free(unique_name); try self.buffers.putNoClobber(unique_name, try Buffer.init(self.allocator, unique_name)); // TODO: This feels inefficient return self.buffers.getPtr(unique_name).?; } pub fn refreshScreen(self: *Editor) !void { self.buffer.scroll(self.screenrows, self.screencols); var sb = StringBuilder.init(self.allocator); const writer = sb.writer(); defer sb.deinit(); try writer.writeAll("\x1b[?25l\x1b[H"); try self.buffer.drawRows(writer, self.screenrows, self.screencols); try self.buffer.drawStatusBar(writer, self.screencols); try self.drawMessageBar(writer); try writer.print("\x1b[{};{}H", .{ self.buffer.cy - self.buffer.rowoff + 1, self.buffer.rx - self.buffer.coloff + 1 + self.buffer.lineNumberDigits(), }); try writer.writeAll("\x1b[?25h"); try std.io.getStdOut().writeAll(sb.seeSlice()); } pub fn refreshWindowSize(self: *Editor) !void { try getWindowSize(&self.screenrows, &self.screencols); self.screenrows -= 2; } pub fn saveBuffersExit(self: *Editor) !void { while (self.buffers.count() > 1 or self.buffer.dirty) { if (!try self.killCurrentBuffer()) { return; } } try std.io.getStdOut().writeAll("\x1b[2J\x1b[H"); self.should_exit = true; } pub fn setStatusMessage(self: *Editor, comptime fmt: []const u8, args: anytype) !void { // Get new resources var new_msg = try std.fmt.allocPrint(self.allocator, fmt, args); errdefer self.allocator.free(new_msg); // Get rid of old resources (no errors) if (self.statusmsg) |old_msg| { self.statusmsg = null; self.allocator.free(old_msg); } // Assign new resources (no errors) self.statusmsg = new_msg; self.statusmsg_time = std.time.milliTimestamp(); } pub fn switchBuffer(self: *Editor) !void { // TODO: completion const bufname_opt = try self.prompt("Switch to buffer"); if (bufname_opt) |bufname| { defer self.allocator.free(bufname); if (self.getBuffer(bufname)) |buffer| { self.buffer = buffer; } else { try self.setStatusMessage("There is no buffer named '{s}'!", .{bufname}); } } } fn drawMessageBar(self: Editor, writer: anytype) !void { try writer.writeAll("\x1b[K"); if (self.statusmsg == null) { return; } if (self.statusmsg.?.len != 0 and std.time.milliTimestamp() - self.statusmsg_time < 5 * std.time.ms_per_s) { try writer.writeAll(self.statusmsg.?[0..(std.math.min(self.statusmsg.?.len, self.screencols))]); } } fn getCursorPosition(row: *usize, col: *usize) !void { const std_out = std.io.getStdOut(); try std_out.writeAll("\x1b[6n\r\n"); const std_in = std.io.getStdIn().reader(); var buf = [_]u8{undefined} ** 32; var response = std_in.readUntilDelimiter(&buf, 'R') catch |err| switch (err) { error.EndOfStream => return error.MisformedTerminalResponse, error.StreamTooLong => return error.MisformedTerminalResponse, else => return @errSetCast(std.os.ReadError, err), }; if (response.len < 2 or response[0] != '\x1b' or response[1] != '[') { return error.MisformedTerminalResponse; } response = response[2..]; var split_it = std.mem.split(u8, response, ";"); row.* = parseUnsignedOptDefault(usize, split_it.next(), 10, 1) catch return error.MisformedTerminalResponse; col.* = parseUnsignedOptDefault(usize, split_it.next(), 10, 1) catch return error.MisformedTerminalResponse; if (split_it.next()) |_| { return error.MisformedTerminalResponse; } } fn getWindowSize(rows: *usize, cols: *usize) !void { var ws: std.os.linux.winsize = undefined; const rc = std.os.linux.ioctl(std.os.STDIN_FILENO, std.os.linux.T.IOCGWINSZ, @ptrToInt(&ws)); switch (std.os.linux.getErrno(rc)) { .SUCCESS => { cols.* = ws.ws_col; rows.* = ws.ws_row; }, else => { const std_out = std.io.getStdOut(); try std_out.writeAll("\x1b[999C\x1b[999B"); return getCursorPosition(rows, cols); }, } } fn parseUnsignedOptDefault(comptime T: type, buf_opt: ?[]const u8, radix: u8, default: T) !T { if (buf_opt) |buf| { return std.fmt.parseUnsigned(T, buf, radix); } else { return default; } } fn readByte(reader: std.fs.File) !?u8 { var buf = [_]u8{undefined}; if (1 != try reader.read(&buf)) { return null; } else { return buf[0]; } } fn readByteBlocking(reader: std.fs.File) !u8 { // No we do not care about possible EOF on stdin, don't run the editor with redirected stdin. var char = try readByte(reader); while (char == null) : (char = try readByte(reader)) { std.os.sched_yield() catch {}; // :) } return char.?; } fn readKey(self: *Editor) !Key { if (self.key_buffer.items.len > 0) { return self.key_buffer.pop(); } const std_in = std.io.getStdIn(); const char1 = try readByteBlocking(std_in); if (char1 != '\x1b') { return Key.char(char1); } // TODO: This is a bad way of parsing. const char2 = (try readByte(std_in)) orelse { return Key.escape; }; if (char2 == '[') { const char3 = (try readByte(std_in)) orelse { return Key.meta('['); }; if (char3 >= '0' and char3 <= '9') { const char4 = (try readByte(std_in)) orelse { std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); try self.key_buffer.append(Key.char(char3)); return Key.meta('['); }; if (char4 == '~') { return switch (char3) { '1' => Key.home, '3' => Key.delete, '4' => Key.end, '5' => Key.page_up, '6' => Key.page_down, '7' => Key.home, '8' => Key.end, else => { std.log.err("Unknown terminal sequence '^[[{c}~'", .{char3}); try self.key_buffer.append(Key.char('~')); try self.key_buffer.append(Key.char(char3)); return Key.meta('['); }, }; } else if (char4 == ';' and char3 == '1') { const char5 = (try readByte(std_in)) orelse { std.log.err("Unknown terminal sequence '^[[1;'", .{}); try self.key_buffer.append(Key.char(';')); try self.key_buffer.append(Key.char('1')); return Key.meta('['); }; if (char5 == '5') { const char6 = (try readByte(std_in)) orelse { std.log.err("Unknown terminal sequence '^[[1;5'", .{}); try self.key_buffer.append(Key.char('5')); try self.key_buffer.append(Key.char(';')); try self.key_buffer.append(Key.char('1')); return Key.meta('['); }; return switch (char6) { 'A' => Key.ctrl_up, 'B' => Key.ctrl_down, 'C' => Key.ctrl_right, 'D' => Key.ctrl_left, 'F' => Key.ctrl_end, 'H' => Key.ctrl_home, else => { std.log.err("Unknown terminal sequence '^[[1;5{c}'", .{char6}); try self.key_buffer.append(Key.char(char6)); try self.key_buffer.append(Key.char('5')); try self.key_buffer.append(Key.char(';')); try self.key_buffer.append(Key.char('1')); return Key.meta('['); }, }; } else { std.log.err("Unknown terminal sequence '^[[1;{c}'", .{char5}); try self.key_buffer.append(Key.char(char5)); try self.key_buffer.append(Key.char(';')); try self.key_buffer.append(Key.char('1')); return Key.meta('['); } } else { std.log.err("Unknown terminal sequence '^[[{c}{c}'", .{char3, char4}); try self.key_buffer.append(Key.char(char4)); try self.key_buffer.append(Key.char(char3)); return Key.meta('['); } } else { return switch (char3) { 'A' => Key.up, 'B' => Key.down, 'C' => Key.right, 'D' => Key.left, 'F' => Key.end, 'H' => Key.home, else => { std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); try self.key_buffer.append(Key.char(char3)); return Key.meta('['); }, }; } } else if (char2 == 'O') { const char3 = (try readByte(std_in)) orelse { return Key.meta('O'); }; return switch (char3) { 'F' => Key.end, 'H' => Key.home, else => { std.log.err("Unknown terminal sequence '^[O{c}'", .{char3}); try self.key_buffer.append(Key.char(char3)); return Key.meta('O'); }, }; } else { return Key.meta(char2); } } // C- = ^[[5;5~ // C- = ^[[6;5 // S- adds ;2, M- adds ;3, S-M- adds ;4, C- adds ;5, S-C- adds ;6, M-C- adds ;7, S-M-C- adds ;8