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/Buffer.zig | 518 ++++++++++++++++++++++++++++++++++++++++++++++++ src/Config.zig | 78 ++++++++ src/Editor.zig | 461 ++++++++++++++++++++++++++++++++++++++++++ src/RawMode.zig | 38 ++++ src/Row.zig | 273 +++++++++++++++++++++++++ src/StringBuilder.zig | 45 +++++ src/Syntax.zig | 28 +++ src/Syntax/makefile.zig | 65 ++++++ src/Syntax/zig.zig | 43 ++++ src/highlight.zig | 34 ++++ src/key.zig | 102 ++++++++++ src/key_state.zig | 96 +++++++++ src/main.zig | 40 ++++ src/search.zig | 94 +++++++++ 14 files changed, 1915 insertions(+) create mode 100644 src/Buffer.zig create mode 100644 src/Config.zig create mode 100644 src/Editor.zig create mode 100644 src/RawMode.zig create mode 100644 src/Row.zig create mode 100644 src/StringBuilder.zig create mode 100644 src/Syntax.zig create mode 100644 src/Syntax/makefile.zig create mode 100644 src/Syntax/zig.zig create mode 100644 src/highlight.zig create mode 100644 src/key.zig create mode 100644 src/key_state.zig create mode 100644 src/main.zig create mode 100644 src/search.zig (limited to 'src') diff --git a/src/Buffer.zig b/src/Buffer.zig new file mode 100644 index 0000000..1ddac45 --- /dev/null +++ b/src/Buffer.zig @@ -0,0 +1,518 @@ +const es_config = @import("es-config"); +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const Buffer = @This(); +const Config = @import("Config.zig"); +const File = std.fs.File; +const Editor = @import("Editor.zig"); +const Highlight = @import("highlight.zig").Highlight; +const Row = @import("Row.zig"); +const Syntax = @import("Syntax.zig"); + +allocator: Allocator, + +// TODO: Short name & file name split +name: []u8, + +rows: ArrayList(Row), + +cx: usize, +cy: usize, +rx: usize, + +rowoff: usize, +coloff: usize, + +dirty: bool, +has_file: bool, + +config: Config, +syntax: ?Syntax, + +pub fn init(allocator: Allocator, name: []const u8) !Buffer { + var name_owned = try allocator.dupe(u8, name); + errdefer allocator.free(name_owned); + + // TODO: buffer-specific config support + var config = try Config.readConfig(allocator); + + return Buffer { + .allocator = allocator, + + .name = name_owned, + + .rows = ArrayList(Row).init(allocator), + + .cx = 0, + .cy = 0, + .rx = 0, + + .rowoff = 0, + .coloff = 0, + + .dirty = false, + .has_file = false, + + .config = config, + .syntax = null, + }; +} + +pub fn deinit(self: Buffer) void { + self.allocator.free(self.name); + + for (self.rows.items) |row| { + row.deinit(); + } + self.rows.deinit(); +} + +pub fn appendRow(self: *Buffer, data: []const u8) !void { + try self.insertRow(self.rows.items.len, data); +} + +pub fn backwardChar(self: *Buffer) void { + if (self.cx == 0) { + if (self.cy == 0) { + return; + } else { + self.cy -= 1; + self.cx = self.rows.items[self.cy].data.items.len; + } + } else { + self.cx -= 1; + } +} + +pub fn backwardDeleteChar(self: *Buffer) !void { + self.backwardChar(); + return self.deleteChar(); +} + +pub fn cleanWhiteSpace(self: *Buffer) !void { + for (self.rows.items) |*row| { + try row.cleanWhiteSpace(self); + } + + if (self.cy == self.rows.items.len) { + return; + } + + const row = self.rows.items[self.cy]; + if (self.cx > row.data.items.len) { + self.cx = row.data.items.len; + self.rx = row.cxToRx(self.config, self.cx); + } +} + +pub fn deleteChar(self: *Buffer) !void { + if (self.cy == self.rows.items.len) { + return; + } + + if (self.cx < self.rows.items[self.cy].data.items.len) { + self.dirty = true; + try self.rows.items[self.cy].deleteChar(self, self.cx); + } else { + if (self.cy == self.rows.items.len - 1) { + return; + } + + self.dirty = true; + try self.rows.items[self.cy].appendString(self, self.rows.items[self.cy + 1].data.items); + self.deleteRow(self.cy + 1); + } +} + +pub fn deleteAllRows(self: *Buffer) void { + if (self.rows.items.len == 0) { + return; + } + + self.dirty = true; + while (self.rows.popOrNull()) |row| { + row.deinit(); + } +} + +pub fn deleteRow(self: *Buffer, at: usize) void { + self.dirty = true; + + self.rows.orderedRemove(at).deinit(); + var i = at; + while (i < self.rows.items.len) : (i += 1) { + self.rows.items[i].idx -= 1; + } +} + +pub fn drawRows(self: Buffer, writer: anytype, screenrows: usize, screencols: usize) !void { + const line_num_digits = self.lineNumberDigits(); + var y: usize = 0; + while (y < screenrows) : (y += 1) { + const filerow = y + self.rowoff; + if (filerow < self.rows.items.len) { + const row = self.rows.items[filerow]; + + try writer.writeAll(Highlight.line_no.asString()); + try printWithLeftPadding(self.allocator, writer, "{}", line_num_digits - 1, ' ', .{filerow + 1}); + try writer.writeByte(' '); + + if (row.rdata.items.len >= self.coloff) { + try writer.writeAll("\x1b[m"); + var last_hl = Highlight.normal; + + var x: usize = 0; + while (x < screencols - line_num_digits) : (x += 1) { + const rx = x + self.coloff; + if (rx >= row.rdata.items.len) { + break; + } + + const char = row.rdata.items[rx]; + if (std.ascii.isCntrl(char)) { + const sym = [_]u8{if (char <= 26) char + '@' else '?'}; + try writer.print("\x1b[7m{s}\x1b[m", .{&sym}); + last_hl = Highlight.none; + } else { + const hl = if (rx > self.config.line_limit) Highlight.overlong_line else row.hldata.items[rx]; + if (hl != last_hl) { + try writer.writeAll(hl.asString()); + last_hl = hl; + } + + try writer.writeByte(char); + } + } + } + } else if (self.rows.items.len == 0 and y == screenrows / 3) { + const welcome_data = try std.fmt.allocPrint(self.allocator, "ES -- version {}", .{ es_config.es_version }); + defer self.allocator.free(welcome_data); + var welcome = welcome_data; + if (welcome.len > screencols - 1) { + welcome = welcome[0..(screencols - 1)]; + } + + const padding = (screencols - welcome.len - 1) / 2; + try writer.print("{s}~{s}", .{Highlight.line_no.asString(), Highlight.normal.asString()}); + try printWithLeftPadding(self.allocator, writer, "{s}", welcome.len + padding, ' ', .{welcome}); + } else { + try writer.print("{s}~", .{Highlight.line_no.asString()}); + } + + try writer.writeAll("\x1b[K\r\n"); + } +} + +pub fn drawStatusBar(self: Buffer, writer: anytype, screencols: usize) !void { + try writer.writeAll("\x1b[m\x1b[7m"); + + var name = if (self.name.len > 20) + try std.fmt.allocPrint(self.allocator, "{s}...", .{self.name[0..17]}) + else + try self.allocator.dupe(u8, self.name); + defer self.allocator.free(name); + + const modified = if (self.dirty) + @as([]const u8, " (modified)") + else + @as([]const u8, ""); + + var lbuf = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{name, modified}); + defer self.allocator.free(lbuf); + + var rbuf = try std.fmt.allocPrint(self.allocator, " {s} | {}/{}", .{ + if (self.syntax) |syntax| syntax.name else "Fundamental", + self.cy + 1, + self.rows.items.len, + }); + defer self.allocator.free(rbuf); + + var rlen = if (rbuf.len > screencols) screencols else rbuf.len; + var llen = if (lbuf.len > screencols - rlen) screencols - rlen else lbuf.len; + + try writer.writeAll(lbuf[0..llen]); + try writer.writeByteNTimes(' ', screencols - llen - rlen); + try writer.writeAll(rbuf[0..rlen]); + try writer.writeAll("\x1b[m\r\n"); +} + +pub fn forwardChar(self: *Buffer) void { + if (self.rows.items.len == self.cy) { + return; + } + + if (self.cx == self.rows.items[self.cy].data.items.len) { + self.cx = 0; + self.cy += 1; + } else { + self.cx += 1; + } +} + +pub fn insertChar(self: *Buffer, char: u8) !void { + if (self.cy == self.rows.items.len) { + try self.insertRow(self.rows.items.len, ""); + } + + try self.rows.items[self.cy].insertChar(self, self.cx, char); + self.cx += 1; + + self.dirty = true; +} + +pub fn insertNewline(self: *Buffer) !void { + self.dirty = true; + + if (self.cx == 0) { + try self.insertRow(self.cy, ""); + self.cy += 1; + return; + } + + var row = &self.rows.items[self.cy]; + + const indentation = try row.indentation(self.allocator); + defer self.allocator.free(indentation); + + try self.insertRow(self.cy + 1, indentation); + row = &self.rows.items[self.cy]; + + try self.rows.items[self.cy + 1].appendString(self, row.data.items[self.cx..]); + try row.data.resize(self.cx); + + try row.update(self); + + self.cx = indentation.len; + self.cy += 1; +} + +pub fn insertRow(self: *Buffer, at: usize, data: []const u8) !void { + var row = try Row.init(self.allocator, at, data); + errdefer row.deinit(); + + try self.rows.insert(at, row); + var i: usize = at + 1; + while (i < self.rows.items.len) : (i += 1) { + self.rows.items[i].idx += 1; + } + try self.rows.items[at].update(self); + + self.dirty = true; +} + +pub fn killLine(self: *Buffer) !void { + return self.deleteRow(self.cy); +} + +pub fn lineNumberDigits(self: Buffer) usize { + if (self.rows.items.len == 0) { + return 2; + } + return 2 + std.math.log10(self.rows.items.len); +} + +pub fn moveBeginningOfLine(self: *Buffer) void { + self.cx = 0; +} + +pub fn moveEndOfLine(self: *Buffer) void { + if (self.rows.items.len == self.cy) { + self.cx = 0; + } else { + self.cx = self.rows.items[self.cy].data.items.len; + } +} + +pub fn nextLine(self: *Buffer) void { + if (self.rows.items.len == self.cy) { + return; + } + + self.cy += 1; + + if (self.rows.items.len == self.cy) { + self.cx = 0; + } else { + self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx); + } +} + +pub fn pageDown(self: *Buffer, screenrows: usize) void { + self.cy = std.math.clamp( + self.rowoff + screenrows - 1 - self.config.page_overlap, + 0, + self.rows.items.len, + ); + var i: usize = 0; + while (i < screenrows) : (i += 1) { + self.nextLine(); + } +} + +pub fn pageUp(self: *Buffer, screenrows: usize) void { + self.cy = std.math.min(self.rows.items.len, self.rowoff + self.config.page_overlap); + var i: usize = 0; + while (i < screenrows) : (i += 1) { + self.previousLine(); + } +} + +pub fn previousLine(self: *Buffer) void { + if (self.cy == 0) { + return; + } + + self.cy -= 1; + self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx); +} + +pub fn recenterTopBottom(self: *Buffer, screenrows: usize) void { + // TODO: Currently only recenters + if (self.cy >= screenrows / 2) { + self.rowoff = self.cy - screenrows / 2; + } else { + self.rowoff = 0; + } +} + +pub fn save(self: *Buffer, editor: *Editor) !void { + if (!self.has_file) { + const fname = try editor.prompt("Save as"); + if (fname == null) { + return; + } + + self.allocator.free(self.name); + self.name = fname.?; + self.has_file = true; + + try self.selectSyntaxHighlighting(); + } + + // TODO: Add a config value for this + try self.cleanWhiteSpace(); + + const tmpname = try std.fmt.allocPrint(self.allocator, "{s}~{}", .{self.name, std.time.milliTimestamp()}); + defer self.allocator.free(tmpname); + + const tmpfile = std.fs.cwd().createFile(tmpname, .{.truncate = true}) catch |err| { + try editor.setStatusMessage("Cannot open tempfile '{s}' for writing: {}", .{tmpname, err}); + return; + }; + defer tmpfile.close(); + + const stat = statFile(self.name) catch |err| { + try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{self.name, err}); + return; + }; + + tmpfile.chmod(stat.mode) catch |err| { + try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{tmpname, err}); + return; + }; + + // TODO: chown + + self.writeToFile(tmpfile) catch |err| { + try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{tmpname, err}); + return; + }; + + std.fs.cwd().rename(tmpname, self.name) catch |err| { + try editor.setStatusMessage("Couldn't move '{s}' to '{s}': {}", .{tmpname, self.name, err}); + return; + }; + + try editor.setStatusMessage("Saved to '{s}'", .{self.name}); + self.dirty = false; +} + +pub fn scroll(self: *Buffer, screenrows: usize, screencols: usize) void { + self.rx = 0; + if (self.cy < self.rows.items.len) { + self.rx = self.rows.items[self.cy].cxToRx(self.config, self.cx); + } + + if (self.cy < self.rowoff) { + self.rowoff = self.cy; + } else if (self.cy >= self.rowoff + screenrows) { + self.rowoff = self.cy + 1 - screenrows; + } + + if (self.rx < self.coloff) { + self.coloff = self.rx; + } else if (self.rx + self.lineNumberDigits() >= self.coloff + screencols) { + self.coloff = self.lineNumberDigits() + self.rx + 1 - screencols; + } +} + +pub fn selectSyntaxHighlighting(self: *Buffer) !void { + self.syntax = null; + + const ext = if (std.mem.lastIndexOfScalar(u8, self.name, '.')) |idx| self.name[idx..] else null; + for (Syntax.database) |syntax| { + for (syntax.filematch) |filematch| { + const is_ext = filematch[0] == '.'; + if ((is_ext and ext != null and std.mem.eql(u8, ext.?, filematch)) + or (!is_ext and std.mem.eql(u8, self.name, filematch)) + ) { + self.syntax = syntax; + + for (self.rows.items) |*row| { + try row.updateSyntax(self); + } + + return; + } + } + } +} + +fn printWithLeftPadding( + allocator: Allocator, + writer: anytype, + comptime fmt: []const u8, + width: usize, + padding: u8, + args: anytype, +) !void { + var unpadded = try std.fmt.allocPrint(allocator, fmt, args); + defer allocator.free(unpadded); + + std.debug.assert(unpadded.len <= width); + + try writer.writeByteNTimes(padding, width - unpadded.len); + try writer.writeAll(unpadded); +} + +fn statFile(name: []const u8) !File.Stat { + const file = std.fs.cwd().openFile(name, .{}) catch |err| switch (err) { + error.FileNotFound => return File.Stat{ + .inode = 0, + .size = 0, + .mode = 0o644, + .kind = .File, + .atime = 0, + .mtime = 0, + .ctime = 0, + }, + else => return err, + }; + defer file.close(); + + return file.stat(); +} + +fn writeToFile(self: Buffer, file: File) !void { + var buffered_writer = std.io.bufferedWriter(file.writer()); + + const writer = buffered_writer.writer(); + for (self.rows.items) |row| { + try writer.writeAll(row.data.items); + try writer.writeByte('\n'); + } + + try buffered_writer.flush(); +} diff --git a/src/Config.zig b/src/Config.zig new file mode 100644 index 0000000..b8b45a5 --- /dev/null +++ b/src/Config.zig @@ -0,0 +1,78 @@ +// TODO: Change this to proper TOML in the future :) + +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const BufMap = std.BufMap; +const Config = @This(); + +const config_path = "arkta/es/es.ini"; + +line_limit: usize = 100, +page_overlap: usize = 2, +tab_stop: usize = 8, + +pub fn readConfig(allocator: Allocator) !Config { + var config = Config {}; + + var env_map = try std.process.getEnvMap(allocator); + defer env_map.deinit(); + + if (env_map.get("XDG_CONFIG_DIRS")) |base_dirs| { + var it = std.mem.split(u8, base_dirs, ":"); + while (it.next()) |base_dir| { + try readConfigInBaseDir(allocator, &config, base_dir); + } + } else { + try readConfigInBaseDir(allocator, &config, "/etc/xdg"); + } + + if (env_map.get("XDG_CONFIG_HOME")) |base_dir| { + try readConfigInBaseDir(allocator, &config, base_dir); + } else { + // TODO: Maybe return an error instead of .? + const home = env_map.get("HOME").?; + const home_config = try std.fs.path.join(allocator, &.{home, ".config"}); + defer allocator.free(home_config); + + try readConfigInBaseDir(allocator, &config, home_config); + } + + return config; +} + +fn readConfigInBaseDir(allocator: Allocator, config: *Config, base_dir: []const u8) !void { + const filename = try std.fs.path.join(allocator, &.{base_dir, config_path}); + defer allocator.free(filename); + + const file = std.fs.openFileAbsolute(filename, .{ .read = true }) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; + defer file.close(); + + const reader = file.reader(); + while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |line_buf| { + defer allocator.free(line_buf); + + var line = std.mem.trim(u8, line_buf, &std.ascii.spaces); + if (line.len == 0 or line[0] == '#') { + continue; + } + + const split = if (std.mem.indexOfScalar(u8, line, '=')) |idx| idx else { + return error.MalformedConfig; + }; + + const key = std.mem.trimRight(u8, line[0..split], &std.ascii.spaces); + const value = std.mem.trimLeft(u8, line[(split + 1)..], &std.ascii.spaces); + if (std.mem.eql(u8, key, "line-limit")) { + config.line_limit = try std.fmt.parseUnsigned(usize, value, 0); + } else if (std.mem.eql(u8, key, "page-overlap")) { + config.page_overlap = try std.fmt.parseUnsigned(usize, value, 0); + } else if (std.mem.eql(u8, key, "line-limit")) { + config.line_limit = try std.fmt.parseUnsigned(usize, value, 0); + } + // TODO: else ?? + } +} 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]); + } +} diff --git a/src/RawMode.zig b/src/RawMode.zig new file mode 100644 index 0000000..ed71819 --- /dev/null +++ b/src/RawMode.zig @@ -0,0 +1,38 @@ +const linux = std.os.linux; +const std = @import("std"); + +const STDIN_FILENO = std.os.STDIN_FILENO; +const RawMode = @This(); +const tcflag_t = linux.tcflag_t; +const tcgetattr = std.os.tcgetattr; +const tcsetattr = std.os.tcsetattr; +const termios = std.os.termios; + +orig: termios, + +pub fn init() !RawMode { + const orig = try tcgetattr(STDIN_FILENO); + const self = RawMode{ .orig = orig }; + errdefer self.deinit(); + + var raw = orig; + + raw.iflag &= ~@as(tcflag_t, linux.BRKINT | linux.ICRNL | linux.INPCK | linux.ISTRIP | linux.IXON); + raw.lflag &= ~@as(tcflag_t, linux.ECHO | linux.ICANON | linux.IEXTEN | linux.ISIG); + raw.oflag &= ~@as(tcflag_t, linux.OPOST); + + raw.cflag |= linux.CS8; + + raw.cc[linux.V.MIN] = 0; + raw.cc[linux.V.TIME] = 1; + + try tcsetattr(STDIN_FILENO, .FLUSH, raw); + + return self; +} + +pub fn deinit(self: RawMode) void { + tcsetattr(STDIN_FILENO, .FLUSH, self.orig) catch |err| { + std.log.err("Failed to reset termios: {}", .{err}); + }; +} diff --git a/src/Row.zig b/src/Row.zig new file mode 100644 index 0000000..633e338 --- /dev/null +++ b/src/Row.zig @@ -0,0 +1,273 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const Buffer = @import("Buffer.zig"); +const Config = @import("Config.zig"); +const Highlight = @import("highlight.zig").Highlight; +const Row = @This(); +const StringBuilder = @import("StringBuilder.zig"); + +allocator: Allocator, + +idx: usize, + +data: ArrayList(u8), +rdata: ArrayList(u8), +hldata: ArrayList(Highlight), + +ends_with_open_comment: bool, + +pub fn init(allocator: Allocator, idx: usize, data: []const u8) !Row { + var self = Row{ + .allocator = allocator, + + .idx = idx, + + .data = ArrayList(u8).init(allocator), + .rdata = ArrayList(u8).init(allocator), + .hldata = ArrayList(Highlight).init(allocator), + + .ends_with_open_comment = false, + }; + errdefer self.deinit(); + + try self.data.appendSlice(data); + return self; +} + +pub fn deinit(self: Row) void { + self.data.deinit(); + self.rdata.deinit(); + self.hldata.deinit(); +} + +pub fn appendString(self: *Row, buf: *Buffer, str: []const u8) !void { + try self.data.appendSlice(str); + return self.update(buf); +} + +pub fn cleanWhiteSpace(self: *Row, buf: *Buffer) !void { + const orig_len = self.data.items.len; + while (self.data.items.len > 0) { + if (std.ascii.isBlank(self.data.items[self.data.items.len - 1])) { + _ = self.data.pop(); + } else { + break; + } + } + + if (orig_len != self.data.items.len) { + buf.dirty = true; + try self.update(buf); + } +} + +pub fn cxToRx(self: Row, config: Config, cx: usize) usize { + var rx: usize = 0; + var i: usize = 0; + while (i < cx) : (i += 1) { + if (self.data.items[i] == '\t') { + rx += config.tab_stop - (rx % config.tab_stop); + } else { + rx += 1; + } + } + + return rx; +} + +pub fn deleteChar(self: *Row, buf: *Buffer, at: usize) !void { + _ = self.data.orderedRemove(at); + try self.update(buf); +} + +pub fn indentation(self: Row, allocator: Allocator) ![]u8 { + var str = ArrayList(u8).init(allocator); + defer str.deinit(); + + var idx: usize = 0; + while (idx < self.data.items.len) : (idx += 1) { + if (!std.ascii.isBlank(self.data.items[idx])) { + break; + } + } + + try str.appendSlice(self.data.items[0..idx]); + return str.toOwnedSlice(); +} + +pub fn insertChar(self: *Row, buf: *Buffer, at: usize, char: u8) !void { + try self.data.insert(at, char); + try self.update(buf); +} + +pub fn rxToCx(self: Row, config: Config, rx: usize) usize { + if (rx == 0) { + return 0; + } + + var cur_rx: usize = 0; + for (self.data.items) |char, cx| { + if (char == '\t') { + cur_rx += config.tab_stop - (cur_rx % config.tab_stop); + } else { + cur_rx += 1; + } + + if (cur_rx >= rx) { + return cx + 1; + } + } + + return self.data.items.len; +} + +// TODO: I don't like that this is modifying both row and buffer (parent of row) +pub fn update(self: *Row, buf: *Buffer) !void { + self.rdata.clearRetainingCapacity(); + + for (self.data.items) |char| { + if (char == '\t') { + const len = buf.config.tab_stop - self.rdata.items.len % buf.config.tab_stop; + try self.rdata.appendNTimes(' ', len); + } else { + try self.rdata.append(char); + } + } + + try self.updateSyntax(buf); +} + +const UpdateSyntaxError = std.mem.Allocator.Error; +pub fn updateSyntax(self: *Row, buf: *Buffer) UpdateSyntaxError!void { + try self.hldata.resize(self.rdata.items.len); + std.mem.set(Highlight, self.hldata.items, .normal); + if (buf.syntax == null) { + return; + } + + const syntax = buf.syntax.?; + const kw1 = syntax.keywords1; + const kw2 = syntax.keywords2; + + var after_sep = true; + var in_comment = if (self.idx > 0) buf.rows.items[self.idx - 1].ends_with_open_comment else false; + var curr_str_quote: ?u8 = null; + + var i: usize = 0; + main_loop: while (i < self.rdata.items.len) { + const prev_hl = if (i > 0) self.hldata.items[i - 1] else .normal; + + if (syntax.singleline_comment_start) |scs| { + std.debug.assert(scs.len != 0); + if (curr_str_quote == null and !in_comment and scs.len + i <= self.rdata.items.len) { + if (std.mem.eql(u8, scs, self.rdata.items[i .. (i + scs.len)])) { + std.mem.set(Highlight, self.hldata.items[i..], .comment); + break; + } + } + } + + if (syntax.multiline_comment_start) |mcs| { + std.debug.assert(mcs.len != 0); + if (syntax.multiline_comment_end) |mce| { + std.debug.assert(mce.len != 0); + if (curr_str_quote == null) { + if (in_comment) { + self.hldata.items[i] = .comment_ml; + if (mce.len + i <= self.rdata.items.len + and std.mem.eql(u8, mce, self.rdata.items[i .. (i + mce.len)]) + ) { + std.mem.set(Highlight, self.hldata.items[i .. (i + mce.len)], .comment_ml); + i += mce.len; + in_comment = false; + after_sep = true; + } else { + i += 1; + continue; + } + } else if (mcs.len + i <= self.rdata.items.len + and std.mem.eql(u8, mcs, self.rdata.items[i .. (i + mcs.len)])) { + std.mem.set(Highlight, self.hldata.items[i .. (i + mcs.len)], .comment_ml); + i += mcs.len; + in_comment = true; + continue; + } + } + + } + } + + const c = self.rdata.items[i]; + + if (syntax.flags.hl_strings) { + if (curr_str_quote) |quote| { + self.hldata.items[i] = .string; + i += 1; + // Pretty dumb way of detecting \" or \' but it works \shrug/ + if (c == '\\' and i < self.rdata.items.len) { + self.hldata.items[i] = .string; + i += 1; + } else if (c == quote) { + curr_str_quote = null; + after_sep = true; + } + + continue; + } else { + // TODO: Move this to syntax struct + if (c == '"' or c == '\'') { + curr_str_quote = c; + self.hldata.items[i] = .string; + i += 1; + continue; + } + } + } + + if (syntax.flags.hl_numbers) { + if ((std.ascii.isDigit(c) and (after_sep or prev_hl == .number)) + or (c == '.' and prev_hl == .number)) { + after_sep = false; + self.hldata.items[i] = .number; + i += 1; + continue; + } + } + + if (after_sep) { + for (kw1) |kw| { + if (i + kw.len <= self.rdata.items.len + and std.mem.eql(u8, kw, self.rdata.items[i .. (i + kw.len)]) + and (i + kw.len == self.rdata.items.len or syntax.isSeparator(self.rdata.items[i + kw.len]))) { + std.mem.set(Highlight, self.hldata.items[i .. (i + kw.len)], .keyword1); + i += kw.len; + after_sep = false; + continue :main_loop; + } + } + + for (kw2) |kw| { + if (i + kw.len <= self.rdata.items.len + and std.mem.eql(u8, kw, self.rdata.items[i .. (i + kw.len)]) + and (i + kw.len == self.rdata.items.len or syntax.isSeparator(self.rdata.items[i + kw.len]))) { + std.mem.set(Highlight, self.hldata.items[i .. (i + kw.len)], .keyword2); + i += kw.len; + after_sep = false; + continue :main_loop; + } + } + } + + after_sep = syntax.isSeparator(c); + i += 1; + } + + if (in_comment != self.ends_with_open_comment) { + self.ends_with_open_comment = in_comment; + if (self.idx + 1 < buf.rows.items.len) { + try buf.rows.items[self.idx + 1].updateSyntax(buf); + } + } +} diff --git a/src/StringBuilder.zig b/src/StringBuilder.zig new file mode 100644 index 0000000..632b1b3 --- /dev/null +++ b/src/StringBuilder.zig @@ -0,0 +1,45 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const StringBuilder = @This(); + +pub const Error = std.mem.Allocator.Error; +pub const Writer = std.io.Writer(*StringBuilder, Error, writeFn); + +data: ArrayList(u8), + +pub fn init(allocator: std.mem.Allocator) StringBuilder { + return .{ + .data = ArrayList(u8).init(allocator), + }; +} + +pub fn deinit(self: StringBuilder) void { + self.data.deinit(); +} + +pub fn append(self: *StringBuilder, char: u8) Error!void { + return self.data.append(char); +} + +pub fn appendMany(self: *StringBuilder, chars: []const u8) Error!void { + return self.data.appendSlice(chars); +} + +pub fn seeSlice(self: *StringBuilder) []const u8 { + return self.data.items; +} + +pub fn toOwnedSlice(self: StringBuilder) []u8 { + return self.data.toOwnedSlice(); +} + +pub fn writer(self: *StringBuilder) Writer { + return Writer { .context = self }; +} + +fn writeFn(self: *StringBuilder, chars: []const u8) Error!usize { + try self.appendMany(chars); + return chars.len; +} diff --git a/src/Syntax.zig b/src/Syntax.zig new file mode 100644 index 0000000..ce3f432 --- /dev/null +++ b/src/Syntax.zig @@ -0,0 +1,28 @@ +pub const makefile = @import("Syntax/makefile.zig"); +pub const zig = @import("Syntax/zig.zig"); + +const std = @import("std"); + +const Syntax = @This(); + +pub const database = [_]Syntax{makefile.syntax, zig.syntax}; + +pub const Flags = struct { + hl_numbers: bool = false, + hl_strings: bool = false, +}; + +name: []const u8, +// TODO: Make these into comptime StringSets, see std.ComptimeStringMap +filematch: []const []const u8, +keywords1: []const []const u8, +keywords2: []const []const u8, +singleline_comment_start: ?[]const u8, +multiline_comment_start: ?[]const u8, +multiline_comment_end: ?[]const u8, +separators: []const u8, +flags: Flags, + +pub fn isSeparator(self: Syntax, char: u8) bool { + return std.ascii.isSpace(char) or std.mem.indexOfScalar(u8, self.separators, char) != null; +} diff --git a/src/Syntax/makefile.zig b/src/Syntax/makefile.zig new file mode 100644 index 0000000..4f73a2c --- /dev/null +++ b/src/Syntax/makefile.zig @@ -0,0 +1,65 @@ +const Syntax = @import("../Syntax.zig"); + +pub const syntax = Syntax{ + .name = "Makefile", + .filematch = &[_][]const u8{ "GNUmakefile", "makefile", "Makefile", ".mk" }, + .keywords1 = &[_][]const u8{ + "$@", "$(@D)", "$(@F)", + "$%", "$(%D)", "$(%F)", + "$<", "$( "\x1b[m", + + .normal => "\x1b[39m", + + .comment => "\x1b[36m", + .comment_ml => "\x1b[36m", + .keyword1 => "\x1b[33m", + .keyword2 => "\x1b[32m", + .number => "\x1b[31m", + .string => "\x1b[35m", + + .line_no => "\x1b[0;90m", + .match => "\x1b[34m", + .overlong_line => "\x1b[91m", + }; + } +}; diff --git a/src/key.zig b/src/key.zig new file mode 100644 index 0000000..1a68ebb --- /dev/null +++ b/src/key.zig @@ -0,0 +1,102 @@ +const std = @import("std"); + +pub const Key = enum(u16) { + return_ = 0x0d, + escape = 0x1b, + space = 0x20, + backspace = 0x7f, + max_char = 0xff, + + meta_nil = 0x100, + meta_max_char = 0x1ff, + + left, + right, + up, + down, + delete, + home, + end, + page_up, + page_down, + + _, + + pub fn char(ch: u8) Key { + return @intToEnum(Key, ch); + } + + pub fn ctrl(ch: u8) Key { + return @intToEnum(Key, ch & 0x1f); + } + + pub fn format( + key: Key, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) @TypeOf(writer).Error!void { + comptime if (fmt.len != 0) { + @compileError("Key doesn't support {" ++ fmt ++ "} format"); + }; + + return switch (key) { + .return_ => std.fmt.formatBuf("", options, writer), + .escape => std.fmt.formatBuf("", options, writer), + .space => std.fmt.formatBuf("", options, writer), + .backspace => std.fmt.formatBuf("", options, writer), + .max_char => key.formatGeneric(options, writer), + + .meta_nil, .meta_max_char => key.formatGeneric(options, writer), + + .left => std.fmt.formatBuf("", options, writer), + .right => std.fmt.formatBuf("", options, writer), + .up => std.fmt.formatBuf("", options, writer), + .down => std.fmt.formatBuf("", options, writer), + .delete => std.fmt.formatBuf("", options, writer), + .home => std.fmt.formatBuf("", options, writer), + .end => std.fmt.formatBuf("", options, writer), + .page_up => std.fmt.formatBuf("", options, writer), + .page_down => std.fmt.formatBuf("", options, writer), + + _ => key.formatGeneric(options, writer), + }; + } + + pub fn meta(ch: u8) Key { + return @intToEnum(Key, ch + @enumToInt(Key.meta_nil)); + } + + pub fn metaCtrl(ch: u8) Key { + return @intToEnum(Key, (ch & 0x1f) + @enumToInt(Key.meta_nil)); + } + + fn formatGeneric( + key: Key, + options: std.fmt.FormatOptions, + writer: anytype, + ) @TypeOf(writer).Error!void { + const key_int = @enumToInt(key); + if (key_int < @enumToInt(Key.space)) { + const ch = std.ascii.toLower(@intCast(u8, key_int + 0x40)); + const buf = [_]u8{'C', '-', ch}; + return std.fmt.formatBuf(&buf, options, writer); + } else if (key_int < @enumToInt(Key.meta_nil)) { + const buf = [_]u8{@intCast(u8, key_int)}; + // This should be printed as C-? or , it's dealt with in + // format() + std.debug.assert(buf[0] != '\x7F'); + return std.fmt.formatBuf(&buf, options, writer); + } else if (key_int <= @enumToInt(Key.meta_max_char)) { + try std.fmt.formatBuf("M-", options, writer); + return Key.format( + @intToEnum(Key, key_int - @enumToInt(Key.meta_nil)), + "", + options, + writer, + ); + } else { + unreachable; + } + } +}; diff --git a/src/key_state.zig b/src/key_state.zig new file mode 100644 index 0000000..b5b16ee --- /dev/null +++ b/src/key_state.zig @@ -0,0 +1,96 @@ +const std = @import("std"); + +const Buffer = @import("Buffer.zig"); +const Editor = @import("Editor.zig"); +const Key = @import("key.zig").Key; +const search = @import("search.zig").search; + +pub const Error = error{ + MalformedConfig, + MisformedTerminalResponse, + StreamTooLong, +} || + std.mem.Allocator.Error || + std.fmt.ParseIntError || + std.fs.File.OpenError || + std.fs.File.ReadError || + std.fs.File.WriteError || + std.os.SchedYieldError; +pub const KeyState = fn(*Editor, *Buffer, Key) Error!void; + +pub fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void { + _ = buf; + editor.current_state = defaultState; + editor.clearStatusMessage(); + + switch (key) { + // ========== C-<*> ========== + Key.ctrl('b') => try editor.switchBuffer(), + Key.ctrl('c') => try editor.saveBuffersExit(), + Key.ctrl('f') => try editor.openFile(), + Key.ctrl('g') => {}, + Key.ctrl('s') => try buf.save(editor), + + // ========== <*> ========== + Key.char('k') => _ = try editor.killCurrentBuffer(), + + else => try editor.setStatusMessage("Unknown chord: C-x {}", .{key}), + } +} + +pub fn defaultState(editor: *Editor, buf: *Buffer, key: Key) Error!void { + switch (key) { + // ========== M-C-<*> ========== + Key.metaCtrl('d'), Key.backspace => try buf.backwardDeleteChar(), + + // ========== M-<*> ========== + Key.meta('v'), Key.page_up => buf.pageUp(editor.screenrows), + + // ========== C-<*> ========== + Key.ctrl('a'), Key.home => buf.moveBeginningOfLine(), + Key.ctrl('b'), Key.left => buf.backwardChar(), + Key.ctrl('d'), Key.delete => try buf.deleteChar(), + Key.ctrl('e'), Key.end => buf.moveEndOfLine(), + Key.ctrl('f'), Key.right => buf.forwardChar(), + Key.ctrl('g') => editor.clearStatusMessage(), + + // TODO: C-h help + + // line feed + Key.ctrl('j') => try buf.insertNewline(), + Key.ctrl('k') => try buf.killLine(), + + Key.ctrl('l') => { + try editor.refreshWindowSize(); + buf.recenterTopBottom(editor.screenrows); + }, + + // carriage return + Key.ctrl('m') => try buf.insertNewline(), + Key.ctrl('n'), Key.down => buf.nextLine(), + Key.ctrl('p'), Key.up => buf.previousLine(), + Key.ctrl('s') => try search(editor, buf), + + // TODO: C-q quotedInsert + + Key.ctrl('v'), Key.page_down => buf.pageDown(editor.screenrows), + + Key.ctrl('x') => { + editor.current_state = cxState; + try editor.setStatusMessage("C-x-", .{}); + }, + + // ========== <*> ========== + else => { + if (@enumToInt(key) <= @enumToInt(Key.max_char)) { + const char = @intCast(u8, @enumToInt(key)); + if (std.ascii.isGraph(char) or std.ascii.isSpace(char)) { + try buf.insertChar(char); + return; + } + } + + try editor.setStatusMessage("Unknown key: {}", .{key}); + }, + } +} diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..9c81776 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,40 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const Editor = @import("Editor.zig"); +const GPA = std.heap.GeneralPurposeAllocator(.{}); +const RawMode = @import("RawMode.zig"); + +pub fn main() !void { + var gpa = GPA{}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const raw_mode = try RawMode.init(); + defer raw_mode.deinit(); + + var editor = try Editor.init(allocator); + defer editor.deinit(); + + try processCommandLineArgs(allocator, &editor); + + try editor.setStatusMessage("C-x C-s = Save | C-x C-c = Quit | C-s = Search", .{}); + + while (!editor.should_exit) { + try editor.refreshScreen(); + try editor.processKeypress(); + } +} + +fn processCommandLineArgs(allocator: Allocator, editor: *Editor) !void { + const args = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, args); + if (args.len <= 1) { + // Do nothing + } else if (args.len == 2) { + try editor.open(args[1]); + } else { + std.log.err("What am I to do with {} arguments?", .{args.len}); + return error.CommandLineArgs; + } +} diff --git a/src/search.zig b/src/search.zig new file mode 100644 index 0000000..bc29813 --- /dev/null +++ b/src/search.zig @@ -0,0 +1,94 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const Buffer = @import("Buffer.zig"); +const Editor = @import("Editor.zig"); +const Highlight = @import("highlight.zig").Highlight; +const Key = @import("key.zig").Key; + +const CallbackData = struct { + allocator: Allocator, + last_match: usize, + saved_hl: ?struct { + line: usize, + data: []Highlight, + }, + buffer: *Buffer, +}; + +const Error = std.mem.Allocator.Error; + +pub fn search(editor: *Editor, buffer: *Buffer) !void { + const saved_cx = buffer.cx; + const saved_cy = buffer.cy; + const saved_coloff = buffer.coloff; + const saved_rowoff = buffer.rowoff; + + var data = CallbackData{ + .allocator = editor.allocator, + .last_match = buffer.cy, + .saved_hl = null, + .buffer = buffer, + }; + + defer if (data.saved_hl) |saved_hl| { + data.allocator.free(saved_hl.data); + }; + + if (try editor.promptEx(*CallbackData, Error, "Search", searchCallback, &data)) |response| { + editor.allocator.free(response); + } else { + // Cancelled + buffer.cx = saved_cx; + buffer.cy = saved_cy; + buffer.coloff = saved_coloff; + buffer.rowoff = saved_rowoff; + } +} + +fn searchCallback(editor: *Editor, query: []const u8, last_key: Key, data: *CallbackData) Error!void { + _ = editor; + + if (data.saved_hl) |saved_hl| { + const row = &data.buffer.rows.items[saved_hl.line]; + row.hldata.shrinkRetainingCapacity(0); + row.hldata.appendSliceAssumeCapacity(saved_hl.data); + + data.allocator.free(saved_hl.data); + data.saved_hl = null; + } + + if (last_key == Key.return_ or last_key == Key.ctrl('g')) { + return; + } + + if (last_key == Key.ctrl('s') and query.len != 0) { + data.last_match += 1; + } + + var i: usize = 0; + while (i < data.buffer.rows.items.len) : (i += 1) { + const idx = (data.last_match + i) % data.buffer.rows.items.len; + + const row = &data.buffer.rows.items[idx]; + if (std.mem.indexOf(u8, row.rdata.items, query)) |match| { + data.last_match = idx; + data.buffer.cy = idx; + data.buffer.cx = row.rxToCx(data.buffer.config, match); + // TODO: data.buffer.rowoff = data.buffer.rows.items.len; + + data.saved_hl = .{ + .line = idx, + .data = try data.allocator.dupe(Highlight, row.hldata.items), + }; + + var j: usize = 0; + while (j < query.len) : (j += 1) { + row.hldata.items[match + j] = .match; + } + + return; + } + } +} -- cgit v1.2.3