From f03a4b178eacd13226eaaba5f8d10892ccf78711 Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Sat, 1 Jan 2022 23:57:41 +0200 Subject: changes --- src/Buffer.zig | 116 +++++++++++++++++------------ src/Editor.zig | 214 ++++++++++++++++++++++++++++++++++++++++-------------- src/files.zig | 21 ++++++ src/key.zig | 24 +++++- src/key_state.zig | 3 +- 5 files changed, 276 insertions(+), 102 deletions(-) create mode 100644 src/files.zig (limited to 'src') diff --git a/src/Buffer.zig b/src/Buffer.zig index f105c55..0c95e6e 100644 --- a/src/Buffer.zig +++ b/src/Buffer.zig @@ -6,6 +6,7 @@ const ArrayList = std.ArrayList; const Buffer = @This(); const Config = @import("Config.zig"); const File = std.fs.File; +const files = @import("files.zig"); const Editor = @import("Editor.zig"); const Highlight = @import("highlight.zig").Highlight; const Row = @import("Row.zig"); @@ -13,8 +14,8 @@ const Syntax = @import("Syntax.zig"); allocator: Allocator, -// TODO: Short name & file name split -name: []u8, +file_path: ?[]u8, +short_name: []u8, rows: ArrayList(Row), @@ -26,7 +27,6 @@ rowoff: usize, coloff: usize, dirty: bool, -has_file: bool, config: Config, syntax: ?Syntax, @@ -41,7 +41,8 @@ pub fn init(allocator: Allocator, name: []const u8) !Buffer { return Buffer{ .allocator = allocator, - .name = name_owned, + .file_path = null, + .short_name = name_owned, .rows = ArrayList(Row).init(allocator), @@ -53,7 +54,6 @@ pub fn init(allocator: Allocator, name: []const u8) !Buffer { .coloff = 0, .dirty = false, - .has_file = false, .config = config, .syntax = null, @@ -61,7 +61,11 @@ pub fn init(allocator: Allocator, name: []const u8) !Buffer { } pub fn deinit(self: Buffer) void { - self.allocator.free(self.name); + if (self.file_path) |file_path| { + self.allocator.free(file_path); + } + + self.allocator.free(self.short_name); for (self.rows.items) |row| { row.deinit(); @@ -218,10 +222,14 @@ pub fn drawRows(self: Buffer, writer: anytype, screenrows: usize, screencols: us 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]}) + var name = if (self.short_name.len > 20) + try std.fmt.allocPrint( + self.allocator, + "...{s}", + .{self.short_name[self.short_name.len - 17 ..]}, + ) else - try self.allocator.dupe(u8, self.name); + try self.allocator.dupe(u8, self.short_name); defer self.allocator.free(name); const modified = if (self.dirty) @@ -271,7 +279,7 @@ pub fn goToLine(self: *Buffer, editor: *Editor) !void { return; }; - self.cy = std.math.min(line - 1, self.rows.items.len); + self.cy = std.math.clamp(line, 1, self.rows.items.len + 1) - 1; if (self.cy == self.rows.items.len) { self.cx = 0; } else { @@ -422,54 +430,73 @@ pub fn recenterTopBottom(self: *Buffer, screenrows: usize) void { } pub fn save(self: *Buffer, editor: *Editor) !void { - if (!self.has_file) { - const fname = try editor.prompt("Save as"); - if (fname == null) { - return; - } + if (self.file_path == null) { + // TODO: Editor.prompt should take an Allocator + const fname = (try editor.prompt("Save as")) orelse { return; }; + defer self.allocator.free(fname); + + const file_path = try files.resolvePath(self.allocator, fname); + errdefer self.allocator.free(file_path); - self.allocator.free(self.name); - self.name = fname.?; - self.has_file = true; + const file_name = std.fs.path.basename(file_path); + const short_name = try editor.getUniqueBufferName(self.allocator, file_name); + errdefer self.allocator.free(short_name); - try self.selectSyntaxHighlighting(); + self.allocator.free(self.short_name); + self.short_name = short_name; + self.file_path = file_path; + + // TODO: Do I want to do this? + // 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 file_path = self.file_path.?; + + const tmp_path = try std.fmt.allocPrint( + self.allocator, + "{s}~{}", + .{ file_path, std.time.milliTimestamp() }, + ); + defer self.allocator.free(tmp_path); - const tmpfile = std.fs.cwd().createFile(tmpname, .{ .truncate = true }) catch |err| { - try editor.setStatusMessage("Cannot open tempfile '{s}' for writing: {}", .{ tmpname, err }); + const tmp_file = std.fs.createFileAbsolute(tmp_path, .{ .truncate = true }) catch |err| { + try editor.setStatusMessage( + "Cannot open tempfile '{s}' for writing: {}", + .{ tmp_path, err }, + ); return; }; - defer tmpfile.close(); + defer tmp_file.close(); - const stat = statFile(self.name) catch |err| { - try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{ self.name, err }); + const mode = statFileMode(file_path) catch |err| { + try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{ file_path, err }); return; }; - tmpfile.chmod(stat.mode) catch |err| { - try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{ tmpname, err }); + tmp_file.chmod(mode) catch |err| { + try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{ tmp_path, err }); return; }; // TODO: chown - self.writeToFile(tmpfile) catch |err| { - try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{ tmpname, err }); + self.writeToFile(tmp_file) catch |err| { + try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{ tmp_path, err }); return; }; - std.fs.cwd().rename(tmpname, self.name) catch |err| { - try editor.setStatusMessage("Couldn't move '{s}' to '{s}': {}", .{ tmpname, self.name, err }); + std.fs.renameAbsolute(tmp_path, file_path) catch |err| { + try editor.setStatusMessage( + "Couldn't move '{s}' to '{s}': {}", + .{ tmp_path, file_path, err }, + ); return; }; - try editor.setStatusMessage("Saved to '{s}'", .{self.name}); + try editor.setStatusMessage("Saved to '{s}'", .{file_path}); self.dirty = false; } @@ -494,12 +521,17 @@ pub fn scroll(self: *Buffer, screenrows: usize, screencols: usize) void { pub fn selectSyntaxHighlighting(self: *Buffer) !void { self.syntax = null; + const name = if (self.file_path) |file_path| + std.fs.path.basename(file_path) + else + self.short_name; - const ext = if (std.mem.lastIndexOfScalar(u8, self.name, '.')) |idx| self.name[idx..] else null; + const ext = if (std.mem.lastIndexOfScalar(u8, name, '.')) |idx| 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))) { + if ((is_ext and ext != null and std.mem.eql(u8, ext.?, filematch)) + or (!is_ext and std.mem.eql(u8, name, filematch))) { self.syntax = syntax; for (self.rows.items) |*row| { @@ -529,22 +561,14 @@ fn printWithLeftPadding( try writer.writeAll(unpadded); } -fn statFile(name: []const u8) !File.Stat { +fn statFileMode(name: []const u8) !File.Mode { 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, - }, + error.FileNotFound => return 0o644, else => return err, }; defer file.close(); - return file.stat(); + return (try file.stat()).mode; } fn writeToFile(self: Buffer, file: File) !void { diff --git a/src/Editor.zig b/src/Editor.zig index 4d45b77..42b5619 100644 --- a/src/Editor.zig +++ b/src/Editor.zig @@ -5,6 +5,7 @@ const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Buffer = @import("Buffer.zig"); const Editor = @This(); +const files = @import("files.zig"); const Key = @import("key.zig").Key; const key_state = @import("key_state.zig"); const KeyState = key_state.KeyState; @@ -25,6 +26,7 @@ statusmsg_time: i64, current_state: KeyState, +key_buffer: ArrayList(Key), should_exit: bool, pub fn init(allocator: Allocator) !Editor { @@ -42,6 +44,7 @@ pub fn init(allocator: Allocator) !Editor { .current_state = key_state.defaultState, + .key_buffer = ArrayList(Key).init(allocator), .should_exit = false, }; errdefer self.deinit(); @@ -66,6 +69,8 @@ pub fn deinit(self: *Editor) void { self.allocator.free(statusmsg); } + self.key_buffer.deinit(); + self.* = undefined; } @@ -80,6 +85,19 @@ 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) { @@ -95,6 +113,17 @@ pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer { 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; } @@ -107,7 +136,7 @@ pub fn killCurrentBuffer(self: *Editor) !bool { } } - const entry_to_kill = self.buffers.fetchRemove(self.buffer.name).?; + const entry_to_kill = self.buffers.fetchRemove(self.buffer.short_name).?; self.allocator.free(entry_to_kill.key); entry_to_kill.value.deinit(); @@ -120,26 +149,26 @@ pub fn killCurrentBuffer(self: *Editor) !bool { 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; - } +pub fn open(self: *Editor, name: []const u8) !void { + const file_path = try files.resolvePath(self.allocator, name); + defer self.allocator.free(file_path); + + if (self.getBufferByPath(file_path)) |buffer| { + self.buffer = buffer; + return; } - self.buffer = try self.getOrPutBuffer(fname); - // TODO: If already was dirty, ask again - self.buffer.has_file = true; + 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(); - self.buffer.deleteAllRows(); + std.debug.assert(self.buffer.rows.items.len == 0); - const file = std.fs.cwd().openFile(fname, .{ .read = true }) catch |err| switch (err) { + const file = std.fs.openFileAbsolute(file_path, .{ .read = true }) catch |err| switch (err) { error.FileNotFound => { - try self.setStatusMessage("Creating a new file...", .{}); + try self.setStatusMessage("Will create a new file on save...", .{}); self.buffer.dirty = true; - const file = try std.fs.cwd().createFile(fname, .{ .read = true }); - file.close(); return; }, else => return err, @@ -149,6 +178,7 @@ pub fn open(self: *Editor, fname: []const u8) !void { 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); @@ -168,7 +198,7 @@ pub fn openFile(self: *Editor) !void { } pub fn processKeypress(self: *Editor) !void { - const key = try readKey(); + const key = try self.readKey(); try self.current_state(self, self.buffer, key); } @@ -193,7 +223,7 @@ pub fn promptEx( // TODO: Navigation // TODO: Draw the cursor - const key = try readKey(); + const key = try self.readKey(); switch (key) { Key.delete, Key.backspace => _ = buf.popOrNull(), Key.ctrl('g') => { @@ -260,9 +290,21 @@ pub fn putBuffer(self: *Editor, buf_name: []const u8) !*Buffer { 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); @@ -392,37 +434,50 @@ fn parseUnsignedOptDefault(comptime T: type, buf_opt: ?[]const u8, radix: u8, de } } -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(); // :) +fn readByte(reader: std.fs.File) !?u8 { + var buf = [_]u8{undefined}; + if (1 != try reader.read(&buf)) { + return null; + } else { + return buf[0]; } +} - if (buf[0] != '\x1b') { - return @intToEnum(Key, 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 {}; // :) } - if (1 != try std_in.read(buf[0..1])) { - return Key.escape; + return char.?; +} + +fn readKey(self: *Editor) !Key { + if (self.key_buffer.items.len > 0) { + return self.key_buffer.pop(); } - if (buf[0] == '[') { - if (1 != try std_in.read(buf[1..2])) { - return Key.meta('['); - } + const std_in = std.io.getStdIn(); + + const char1 = try readByteBlocking(std_in); + if (char1 != '\x1b') { + return Key.char(char1); + } - if (buf[1] >= '0' and buf[1] <= '9') { - if (1 != try std_in.read(buf[2..3])) { - // TODO: Multiple key return support? + // 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 (buf[2] == '~') { - return switch (buf[1]) { + if (char4 == '~') { + return switch (char3) { '1' => Key.home, '3' => Key.delete, '4' => Key.end, @@ -430,37 +485,90 @@ fn readKey() !Key { '6' => Key.page_down, '7' => Key.home, '8' => Key.end, - // TODO: Multiple key return support - else => Key.meta('['), + 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 { - // TODO: Multiple key return support + 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 (buf[1]) { + return switch (char3) { '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 => { + std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); + try self.key_buffer.append(Key.char(char3)); + return Key.meta('['); + }, }; } - } else if (buf[0] == 'O') { - if (1 != try std_in.read(buf[1..2])) { - return Key.meta('O'); - } - - return switch (buf[1]) { + } else if (char2 == 'O') { + const char3 = (try readByte(std_in)) orelse { return Key.meta('O'); }; + return switch (char3) { 'F' => Key.end, 'H' => Key.home, - // TODO: Multiple key return support - else => Key.meta('O'), + 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(buf[0]); + 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 diff --git a/src/files.zig b/src/files.zig new file mode 100644 index 0000000..ed9e628 --- /dev/null +++ b/src/files.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; + +pub fn resolvePath(allocator: Allocator, name: []const u8) ![]u8 { + return std.fs.cwd().realpathAlloc(allocator, name) catch |err| switch (err) { + error.FileNotFound => if (name.len == 0) { + return error.FileNotFound; + } else if (name[0] == std.fs.path.sep) { + // Already is an absolute path + return allocator.dupe(u8, name); + } else { + // TODO: if (name[0] == '~') + const cwd = try std.process.getCwdAlloc(allocator); + defer allocator.free(cwd); + + return std.fmt.allocPrint(allocator, "{s}{s}{s}", .{cwd, std.fs.path.sep_str, name}); + }, + else => return err, + }; +} diff --git a/src/key.zig b/src/key.zig index 3a007fb..5376017 100644 --- a/src/key.zig +++ b/src/key.zig @@ -14,12 +14,22 @@ pub const Key = enum(u16) { right, up, down, - delete, home, end, + delete, page_up, page_down, + ctrl_left, + ctrl_right, + ctrl_up, + ctrl_down, + ctrl_home, + ctrl_end, + // ctrl_delete, + // ctrl_page_up, + // ctrl_page_down, + _, pub fn char(ch: u8) Key { @@ -53,12 +63,22 @@ pub const Key = enum(u16) { .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), + .delete => std.fmt.formatBuf("", options, writer), .page_up => std.fmt.formatBuf("", options, writer), .page_down => std.fmt.formatBuf("", options, writer), + .ctrl_left => std.fmt.formatBuf("C-", options, writer), + .ctrl_right => std.fmt.formatBuf("C-", options, writer), + .ctrl_up => std.fmt.formatBuf("C-", options, writer), + .ctrl_down => std.fmt.formatBuf("C-", options, writer), + .ctrl_home => std.fmt.formatBuf("C-", options, writer), + .ctrl_end => std.fmt.formatBuf("C-", options, writer), + // ctrl_delete + // ctrl_page_up + // ctrl_page_down + _ => key.formatGeneric(options, writer), }; } diff --git a/src/key_state.zig b/src/key_state.zig index d477094..f1880ae 100644 --- a/src/key_state.zig +++ b/src/key_state.zig @@ -15,7 +15,8 @@ pub const Error = error{ std.fs.File.OpenError || std.fs.File.ReadError || std.fs.File.WriteError || - std.os.SchedYieldError; + std.os.GetCwdError || + std.os.RealPathError; pub const KeyState = fn (*Editor, *Buffer, Key) Error!void; pub fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { -- cgit v1.2.3