From 2d2278364b6186c6cdf0f0497b0498431dfe7dd1 Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Tue, 21 Dec 2021 05:56:41 +0200 Subject: Initial config --- src/Editor.zig | 461 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 461 insertions(+) create mode 100644 src/Editor.zig (limited to 'src/Editor.zig') 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 @@ +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]); + } +} -- cgit v1.2.3