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 File = std.fs.File; const Key = es.Key; const KeyMap = es.KeyMap; const KeyReader = es.KeyReader; const StringBuilder = es.StringBuilder; const StringHashMap = std.StringHashMap; allocator: Allocator, buffers: StringHashMap(Buffer), buffer: *Buffer, screenrows: usize, screencols: usize, statusmsg: ?[]u8, statusmsg_time: i64, key_map: KeyMap, key_reader: KeyReader, should_exit: bool, pub fn init(allocator: Allocator) !Editor { var key_map: ?KeyMap = try KeyMap.defaultMap(allocator); errdefer if (key_map) |*map| map.deinit(); var self = Editor{ .allocator = allocator, .buffers = StringHashMap(Buffer).init(allocator), .buffer = undefined, .screenrows = undefined, .screencols = undefined, .statusmsg = null, .statusmsg_time = 0, .key_map = key_map.?, .key_reader = KeyReader.init(allocator), .should_exit = false, }; errdefer self.deinit(); key_map = null; // 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_map.deinit(); self.key_reader.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.isDirty()) { 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(); var it = self.buffers.valueIterator(); if (it.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, .{ .mode = .read_only }) catch |err| switch (err) { error.FileNotFound => { try self.setStatusMessage("Will create a new file on save...", .{}); self.buffer.edited = 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.edited = false; } pub fn openFile(self: *Editor) !void { const fname_opt = try self.prompt(self.allocator, "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.key_reader.readKey(); try self.key_map.keypress(self, self.buffer, key); } pub fn prompt(self: *Editor, allocator: Allocator, prompt_str: []const u8) !?[]u8 { return self.promptEx(void, error{}, allocator, prompt_str, null, {}); } pub fn promptEx( self: *Editor, comptime CallbackData: type, comptime CallbackError: type, allocator: Allocator, prompt_str: []const u8, callback: ?PromptCallback(CallbackData, CallbackError), cb_data: CallbackData, ) !?[]u8 { var buf = ArrayList(u8).init(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.key_reader.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 try buf.toOwnedSlice(); }, else => if (@intFromEnum(key) < @intFromEnum(Key.max_char)) { const key_char: u8 = @intCast(@intFromEnum(key)); if (std.ascii.isWhitespace(key_char) or std.ascii.isPrint(key_char)) { try buf.append(key_char); } }, // TODO: 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(self.allocator, full_prompt); defer if (response) |str| self.allocator.free(str); // TODO: This can be improved while (response != null and (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(self.allocator, full_prompt); } return response != null and (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.isDirty()) { 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 const 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(self.allocator, "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..(@min(self.statusmsg.?.len, self.screencols))]); } } // TODO[zigbug]: https://github.com/ziglang/zig/issues/18177 // Replace with an inferred set const CursorPositionError = error{ MisformedTerminalResponse, } || File.WriteError; fn getCursorPosition(row: *usize, col: *usize) CursorPositionError!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 @as(CursorPositionError, @errorCast(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.system.winsize = undefined; const rc = std.os.system.ioctl(std.os.STDIN_FILENO, std.os.system.T.IOCGWINSZ, @intFromPtr(&ws)); switch (std.os.system.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; } }