const linux = std.os.linux; const std = @import("std"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Buffer = @import("Buffer.zig"); const Editor = @This(); const Key = @import("key.zig").Key; const key_state = @import("key_state.zig"); const KeyState = key_state.KeyState; const STDIN_FILENO = std.os.STDIN_FILENO; const StringBuilder = @import("StringBuilder.zig"); const StringHashMap = std.StringHashMap; allocator: Allocator, buffers: StringHashMap(Buffer), buffer: *Buffer, screenrows: usize, screencols: usize, statusmsg: ?[]u8, statusmsg_time: i64, current_state: KeyState, 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 = key_state.defaultState, .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.* = 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 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 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.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, fname: []const u8) !void { if (self.hasBuffer(fname)) { if (!try self.promptYN("A file with such name is already open. Open anyways?")) { return; } } self.buffer = try self.getOrPutBuffer(fname); // TODO: If already was dirty, ask again self.buffer.has_file = true; try self.buffer.selectSyntaxHighlighting(); self.buffer.deleteAllRows(); const file = std.fs.cwd().openFile(fname, .{ .read = true }) catch |err| switch (err) { error.FileNotFound => { try self.setStatusMessage("Creating a new file...", .{}); self.buffer.dirty = true; const file = try std.fs.cwd().createFile(fname, .{ .read = true }); file.close(); return; }, else => return err, }; defer file.close(); var buffered_reader = std.io.bufferedReader(file.reader()); const reader = buffered_reader.reader(); 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 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 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.?[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(); } return self.buffers.getPtr(duped_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: linux.winsize = undefined; const rc = linux.ioctl(STDIN_FILENO, linux.T.IOCGWINSZ, @ptrToInt(&ws)); switch (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 readKey() !Key { const std_in = std.io.getStdIn(); var buf = [_]u8{undefined} ** 3; // No we do not care about possible EOF on stdin, don't run the editor with // redirected stdin while (1 != try std_in.read(buf[0..1])) { try std.os.sched_yield(); // :) } if (buf[0] != '\x1b') { return @intToEnum(Key, buf[0]); } if (1 != try std_in.read(buf[0..1])) { return Key.escape; } if (buf[0] == '[') { if (1 != try std_in.read(buf[1..2])) { return Key.meta('['); } if (buf[1] >= '0' and buf[1] <= '9') { if (1 != try std_in.read(buf[2..3])) { // TODO: Multiple key return support? return Key.meta('['); } if (buf[2] == '~') { return switch (buf[1]) { '1' => Key.home, '3' => Key.delete, '4' => Key.end, '5' => Key.page_up, '6' => Key.page_down, '7' => Key.home, '8' => Key.end, // TODO: Multiple key return support else => Key.meta('['), }; } else { // TODO: Multiple key return support return Key.meta('['); } } else { return switch (buf[1]) { 'A' => Key.up, 'B' => Key.down, 'C' => Key.right, 'D' => Key.left, 'F' => Key.end, 'H' => Key.home, // TODO: Multiple key return support else => Key.meta('['), }; } } else if (buf[0] == 'O') { if (1 != try std_in.read(buf[1..2])) { return Key.meta('O'); } return switch (buf[1]) { 'F' => Key.end, 'H' => Key.home, // TODO: Multiple key return support else => Key.meta('O'), }; } else { return Key.meta(buf[0]); } }