diff options
| author | 2022-01-01 23:57:41 +0200 | |
|---|---|---|
| committer | 2022-01-01 23:57:41 +0200 | |
| commit | f03a4b178eacd13226eaaba5f8d10892ccf78711 (patch) | |
| tree | 09ebb7ac3e1750d3c03d3e7dad570a873f132907 | |
| parent | Update make mode (diff) | |
| download | es-f03a4b178eacd13226eaaba5f8d10892ccf78711.tar.gz es-f03a4b178eacd13226eaaba5f8d10892ccf78711.tar.xz es-f03a4b178eacd13226eaaba5f8d10892ccf78711.zip | |
changes
| -rw-r--r-- | src/Buffer.zig | 116 | ||||
| -rw-r--r-- | src/Editor.zig | 214 | ||||
| -rw-r--r-- | src/files.zig | 21 | ||||
| -rw-r--r-- | src/key.zig | 24 | ||||
| -rw-r--r-- | src/key_state.zig | 3 |
5 files changed, 276 insertions, 102 deletions
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; | |||
| 6 | const Buffer = @This(); | 6 | const Buffer = @This(); |
| 7 | const Config = @import("Config.zig"); | 7 | const Config = @import("Config.zig"); |
| 8 | const File = std.fs.File; | 8 | const File = std.fs.File; |
| 9 | const files = @import("files.zig"); | ||
| 9 | const Editor = @import("Editor.zig"); | 10 | const Editor = @import("Editor.zig"); |
| 10 | const Highlight = @import("highlight.zig").Highlight; | 11 | const Highlight = @import("highlight.zig").Highlight; |
| 11 | const Row = @import("Row.zig"); | 12 | const Row = @import("Row.zig"); |
| @@ -13,8 +14,8 @@ const Syntax = @import("Syntax.zig"); | |||
| 13 | 14 | ||
| 14 | allocator: Allocator, | 15 | allocator: Allocator, |
| 15 | 16 | ||
| 16 | // TODO: Short name & file name split | 17 | file_path: ?[]u8, |
| 17 | name: []u8, | 18 | short_name: []u8, |
| 18 | 19 | ||
| 19 | rows: ArrayList(Row), | 20 | rows: ArrayList(Row), |
| 20 | 21 | ||
| @@ -26,7 +27,6 @@ rowoff: usize, | |||
| 26 | coloff: usize, | 27 | coloff: usize, |
| 27 | 28 | ||
| 28 | dirty: bool, | 29 | dirty: bool, |
| 29 | has_file: bool, | ||
| 30 | 30 | ||
| 31 | config: Config, | 31 | config: Config, |
| 32 | syntax: ?Syntax, | 32 | syntax: ?Syntax, |
| @@ -41,7 +41,8 @@ pub fn init(allocator: Allocator, name: []const u8) !Buffer { | |||
| 41 | return Buffer{ | 41 | return Buffer{ |
| 42 | .allocator = allocator, | 42 | .allocator = allocator, |
| 43 | 43 | ||
| 44 | .name = name_owned, | 44 | .file_path = null, |
| 45 | .short_name = name_owned, | ||
| 45 | 46 | ||
| 46 | .rows = ArrayList(Row).init(allocator), | 47 | .rows = ArrayList(Row).init(allocator), |
| 47 | 48 | ||
| @@ -53,7 +54,6 @@ pub fn init(allocator: Allocator, name: []const u8) !Buffer { | |||
| 53 | .coloff = 0, | 54 | .coloff = 0, |
| 54 | 55 | ||
| 55 | .dirty = false, | 56 | .dirty = false, |
| 56 | .has_file = false, | ||
| 57 | 57 | ||
| 58 | .config = config, | 58 | .config = config, |
| 59 | .syntax = null, | 59 | .syntax = null, |
| @@ -61,7 +61,11 @@ pub fn init(allocator: Allocator, name: []const u8) !Buffer { | |||
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | pub fn deinit(self: Buffer) void { | 63 | pub fn deinit(self: Buffer) void { |
| 64 | self.allocator.free(self.name); | 64 | if (self.file_path) |file_path| { |
| 65 | self.allocator.free(file_path); | ||
| 66 | } | ||
| 67 | |||
| 68 | self.allocator.free(self.short_name); | ||
| 65 | 69 | ||
| 66 | for (self.rows.items) |row| { | 70 | for (self.rows.items) |row| { |
| 67 | row.deinit(); | 71 | row.deinit(); |
| @@ -218,10 +222,14 @@ pub fn drawRows(self: Buffer, writer: anytype, screenrows: usize, screencols: us | |||
| 218 | pub fn drawStatusBar(self: Buffer, writer: anytype, screencols: usize) !void { | 222 | pub fn drawStatusBar(self: Buffer, writer: anytype, screencols: usize) !void { |
| 219 | try writer.writeAll("\x1b[m\x1b[7m"); | 223 | try writer.writeAll("\x1b[m\x1b[7m"); |
| 220 | 224 | ||
| 221 | var name = if (self.name.len > 20) | 225 | var name = if (self.short_name.len > 20) |
| 222 | try std.fmt.allocPrint(self.allocator, "{s}...", .{self.name[0..17]}) | 226 | try std.fmt.allocPrint( |
| 227 | self.allocator, | ||
| 228 | "...{s}", | ||
| 229 | .{self.short_name[self.short_name.len - 17 ..]}, | ||
| 230 | ) | ||
| 223 | else | 231 | else |
| 224 | try self.allocator.dupe(u8, self.name); | 232 | try self.allocator.dupe(u8, self.short_name); |
| 225 | defer self.allocator.free(name); | 233 | defer self.allocator.free(name); |
| 226 | 234 | ||
| 227 | const modified = if (self.dirty) | 235 | const modified = if (self.dirty) |
| @@ -271,7 +279,7 @@ pub fn goToLine(self: *Buffer, editor: *Editor) !void { | |||
| 271 | return; | 279 | return; |
| 272 | }; | 280 | }; |
| 273 | 281 | ||
| 274 | self.cy = std.math.min(line - 1, self.rows.items.len); | 282 | self.cy = std.math.clamp(line, 1, self.rows.items.len + 1) - 1; |
| 275 | if (self.cy == self.rows.items.len) { | 283 | if (self.cy == self.rows.items.len) { |
| 276 | self.cx = 0; | 284 | self.cx = 0; |
| 277 | } else { | 285 | } else { |
| @@ -422,54 +430,73 @@ pub fn recenterTopBottom(self: *Buffer, screenrows: usize) void { | |||
| 422 | } | 430 | } |
| 423 | 431 | ||
| 424 | pub fn save(self: *Buffer, editor: *Editor) !void { | 432 | pub fn save(self: *Buffer, editor: *Editor) !void { |
| 425 | if (!self.has_file) { | 433 | if (self.file_path == null) { |
| 426 | const fname = try editor.prompt("Save as"); | 434 | // TODO: Editor.prompt should take an Allocator |
| 427 | if (fname == null) { | 435 | const fname = (try editor.prompt("Save as")) orelse { return; }; |
| 428 | return; | 436 | defer self.allocator.free(fname); |
| 429 | } | 437 | |
| 438 | const file_path = try files.resolvePath(self.allocator, fname); | ||
| 439 | errdefer self.allocator.free(file_path); | ||
| 430 | 440 | ||
| 431 | self.allocator.free(self.name); | 441 | const file_name = std.fs.path.basename(file_path); |
| 432 | self.name = fname.?; | 442 | const short_name = try editor.getUniqueBufferName(self.allocator, file_name); |
| 433 | self.has_file = true; | 443 | errdefer self.allocator.free(short_name); |
| 434 | 444 | ||
| 435 | try self.selectSyntaxHighlighting(); | 445 | self.allocator.free(self.short_name); |
| 446 | self.short_name = short_name; | ||
| 447 | self.file_path = file_path; | ||
| 448 | |||
| 449 | // TODO: Do I want to do this? | ||
| 450 | // try self.selectSyntaxHighlighting(); | ||
| 436 | } | 451 | } |
| 437 | 452 | ||
| 438 | // TODO: Add a config value for this | 453 | // TODO: Add a config value for this |
| 439 | try self.cleanWhiteSpace(); | 454 | try self.cleanWhiteSpace(); |
| 440 | 455 | ||
| 441 | const tmpname = try std.fmt.allocPrint(self.allocator, "{s}~{}", .{ self.name, std.time.milliTimestamp() }); | 456 | const file_path = self.file_path.?; |
| 442 | defer self.allocator.free(tmpname); | 457 | |
| 458 | const tmp_path = try std.fmt.allocPrint( | ||
| 459 | self.allocator, | ||
| 460 | "{s}~{}", | ||
| 461 | .{ file_path, std.time.milliTimestamp() }, | ||
| 462 | ); | ||
| 463 | defer self.allocator.free(tmp_path); | ||
| 443 | 464 | ||
| 444 | const tmpfile = std.fs.cwd().createFile(tmpname, .{ .truncate = true }) catch |err| { | 465 | const tmp_file = std.fs.createFileAbsolute(tmp_path, .{ .truncate = true }) catch |err| { |
| 445 | try editor.setStatusMessage("Cannot open tempfile '{s}' for writing: {}", .{ tmpname, err }); | 466 | try editor.setStatusMessage( |
| 467 | "Cannot open tempfile '{s}' for writing: {}", | ||
| 468 | .{ tmp_path, err }, | ||
| 469 | ); | ||
| 446 | return; | 470 | return; |
| 447 | }; | 471 | }; |
| 448 | defer tmpfile.close(); | 472 | defer tmp_file.close(); |
| 449 | 473 | ||
| 450 | const stat = statFile(self.name) catch |err| { | 474 | const mode = statFileMode(file_path) catch |err| { |
| 451 | try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{ self.name, err }); | 475 | try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{ file_path, err }); |
| 452 | return; | 476 | return; |
| 453 | }; | 477 | }; |
| 454 | 478 | ||
| 455 | tmpfile.chmod(stat.mode) catch |err| { | 479 | tmp_file.chmod(mode) catch |err| { |
| 456 | try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{ tmpname, err }); | 480 | try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{ tmp_path, err }); |
| 457 | return; | 481 | return; |
| 458 | }; | 482 | }; |
| 459 | 483 | ||
| 460 | // TODO: chown | 484 | // TODO: chown |
| 461 | 485 | ||
| 462 | self.writeToFile(tmpfile) catch |err| { | 486 | self.writeToFile(tmp_file) catch |err| { |
| 463 | try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{ tmpname, err }); | 487 | try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{ tmp_path, err }); |
| 464 | return; | 488 | return; |
| 465 | }; | 489 | }; |
| 466 | 490 | ||
| 467 | std.fs.cwd().rename(tmpname, self.name) catch |err| { | 491 | std.fs.renameAbsolute(tmp_path, file_path) catch |err| { |
| 468 | try editor.setStatusMessage("Couldn't move '{s}' to '{s}': {}", .{ tmpname, self.name, err }); | 492 | try editor.setStatusMessage( |
| 493 | "Couldn't move '{s}' to '{s}': {}", | ||
| 494 | .{ tmp_path, file_path, err }, | ||
| 495 | ); | ||
| 469 | return; | 496 | return; |
| 470 | }; | 497 | }; |
| 471 | 498 | ||
| 472 | try editor.setStatusMessage("Saved to '{s}'", .{self.name}); | 499 | try editor.setStatusMessage("Saved to '{s}'", .{file_path}); |
| 473 | self.dirty = false; | 500 | self.dirty = false; |
| 474 | } | 501 | } |
| 475 | 502 | ||
| @@ -494,12 +521,17 @@ pub fn scroll(self: *Buffer, screenrows: usize, screencols: usize) void { | |||
| 494 | 521 | ||
| 495 | pub fn selectSyntaxHighlighting(self: *Buffer) !void { | 522 | pub fn selectSyntaxHighlighting(self: *Buffer) !void { |
| 496 | self.syntax = null; | 523 | self.syntax = null; |
| 524 | const name = if (self.file_path) |file_path| | ||
| 525 | std.fs.path.basename(file_path) | ||
| 526 | else | ||
| 527 | self.short_name; | ||
| 497 | 528 | ||
| 498 | const ext = if (std.mem.lastIndexOfScalar(u8, self.name, '.')) |idx| self.name[idx..] else null; | 529 | const ext = if (std.mem.lastIndexOfScalar(u8, name, '.')) |idx| name[idx..] else null; |
| 499 | for (Syntax.database) |syntax| { | 530 | for (Syntax.database) |syntax| { |
| 500 | for (syntax.filematch) |filematch| { | 531 | for (syntax.filematch) |filematch| { |
| 501 | const is_ext = filematch[0] == '.'; | 532 | const is_ext = filematch[0] == '.'; |
| 502 | if ((is_ext and ext != null and std.mem.eql(u8, ext.?, filematch)) or (!is_ext and std.mem.eql(u8, self.name, filematch))) { | 533 | if ((is_ext and ext != null and std.mem.eql(u8, ext.?, filematch)) |
| 534 | or (!is_ext and std.mem.eql(u8, name, filematch))) { | ||
| 503 | self.syntax = syntax; | 535 | self.syntax = syntax; |
| 504 | 536 | ||
| 505 | for (self.rows.items) |*row| { | 537 | for (self.rows.items) |*row| { |
| @@ -529,22 +561,14 @@ fn printWithLeftPadding( | |||
| 529 | try writer.writeAll(unpadded); | 561 | try writer.writeAll(unpadded); |
| 530 | } | 562 | } |
| 531 | 563 | ||
| 532 | fn statFile(name: []const u8) !File.Stat { | 564 | fn statFileMode(name: []const u8) !File.Mode { |
| 533 | const file = std.fs.cwd().openFile(name, .{}) catch |err| switch (err) { | 565 | const file = std.fs.cwd().openFile(name, .{}) catch |err| switch (err) { |
| 534 | error.FileNotFound => return File.Stat{ | 566 | error.FileNotFound => return 0o644, |
| 535 | .inode = 0, | ||
| 536 | .size = 0, | ||
| 537 | .mode = 0o644, | ||
| 538 | .kind = .File, | ||
| 539 | .atime = 0, | ||
| 540 | .mtime = 0, | ||
| 541 | .ctime = 0, | ||
| 542 | }, | ||
| 543 | else => return err, | 567 | else => return err, |
| 544 | }; | 568 | }; |
| 545 | defer file.close(); | 569 | defer file.close(); |
| 546 | 570 | ||
| 547 | return file.stat(); | 571 | return (try file.stat()).mode; |
| 548 | } | 572 | } |
| 549 | 573 | ||
| 550 | fn writeToFile(self: Buffer, file: File) !void { | 574 | 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; | |||
| 5 | const ArrayList = std.ArrayList; | 5 | const ArrayList = std.ArrayList; |
| 6 | const Buffer = @import("Buffer.zig"); | 6 | const Buffer = @import("Buffer.zig"); |
| 7 | const Editor = @This(); | 7 | const Editor = @This(); |
| 8 | const files = @import("files.zig"); | ||
| 8 | const Key = @import("key.zig").Key; | 9 | const Key = @import("key.zig").Key; |
| 9 | const key_state = @import("key_state.zig"); | 10 | const key_state = @import("key_state.zig"); |
| 10 | const KeyState = key_state.KeyState; | 11 | const KeyState = key_state.KeyState; |
| @@ -25,6 +26,7 @@ statusmsg_time: i64, | |||
| 25 | 26 | ||
| 26 | current_state: KeyState, | 27 | current_state: KeyState, |
| 27 | 28 | ||
| 29 | key_buffer: ArrayList(Key), | ||
| 28 | should_exit: bool, | 30 | should_exit: bool, |
| 29 | 31 | ||
| 30 | pub fn init(allocator: Allocator) !Editor { | 32 | pub fn init(allocator: Allocator) !Editor { |
| @@ -42,6 +44,7 @@ pub fn init(allocator: Allocator) !Editor { | |||
| 42 | 44 | ||
| 43 | .current_state = key_state.defaultState, | 45 | .current_state = key_state.defaultState, |
| 44 | 46 | ||
| 47 | .key_buffer = ArrayList(Key).init(allocator), | ||
| 45 | .should_exit = false, | 48 | .should_exit = false, |
| 46 | }; | 49 | }; |
| 47 | errdefer self.deinit(); | 50 | errdefer self.deinit(); |
| @@ -66,6 +69,8 @@ pub fn deinit(self: *Editor) void { | |||
| 66 | self.allocator.free(statusmsg); | 69 | self.allocator.free(statusmsg); |
| 67 | } | 70 | } |
| 68 | 71 | ||
| 72 | self.key_buffer.deinit(); | ||
| 73 | |||
| 69 | self.* = undefined; | 74 | self.* = undefined; |
| 70 | } | 75 | } |
| 71 | 76 | ||
| @@ -80,6 +85,19 @@ pub fn getBuffer(self: Editor, name: []const u8) ?*Buffer { | |||
| 80 | return self.buffers.getPtr(name); | 85 | return self.buffers.getPtr(name); |
| 81 | } | 86 | } |
| 82 | 87 | ||
| 88 | pub fn getBufferByPath(self: Editor, path: []const u8) ?*Buffer { | ||
| 89 | var it = self.buffers.valueIterator(); | ||
| 90 | while (it.next()) |buffer| { | ||
| 91 | if (buffer.file_path) |buffer_path| { | ||
| 92 | if (std.mem.eql(u8, path, buffer_path)) { | ||
| 93 | return buffer; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | |||
| 98 | return null; | ||
| 99 | } | ||
| 100 | |||
| 83 | pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer { | 101 | pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer { |
| 84 | const get_or_put_res = try self.buffers.getOrPut(name); | 102 | const get_or_put_res = try self.buffers.getOrPut(name); |
| 85 | if (get_or_put_res.found_existing) { | 103 | if (get_or_put_res.found_existing) { |
| @@ -95,6 +113,17 @@ pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer { | |||
| 95 | return get_or_put_res.value_ptr; | 113 | return get_or_put_res.value_ptr; |
| 96 | } | 114 | } |
| 97 | 115 | ||
| 116 | pub fn getUniqueBufferName(self: Editor, allocator: Allocator, base_name: []const u8) ![]u8 { | ||
| 117 | var unique_name = try allocator.dupe(u8, base_name); | ||
| 118 | var idx: usize = 1; | ||
| 119 | while (self.hasBuffer(unique_name)) : (idx += 1) { | ||
| 120 | allocator.free(unique_name); | ||
| 121 | unique_name = try std.fmt.allocPrint(allocator, "{s}<{}>", .{ base_name, idx }); | ||
| 122 | } | ||
| 123 | |||
| 124 | return unique_name; | ||
| 125 | } | ||
| 126 | |||
| 98 | pub fn hasBuffer(self: Editor, name: []const u8) bool { | 127 | pub fn hasBuffer(self: Editor, name: []const u8) bool { |
| 99 | return self.getBuffer(name) != null; | 128 | return self.getBuffer(name) != null; |
| 100 | } | 129 | } |
| @@ -107,7 +136,7 @@ pub fn killCurrentBuffer(self: *Editor) !bool { | |||
| 107 | } | 136 | } |
| 108 | } | 137 | } |
| 109 | 138 | ||
| 110 | const entry_to_kill = self.buffers.fetchRemove(self.buffer.name).?; | 139 | const entry_to_kill = self.buffers.fetchRemove(self.buffer.short_name).?; |
| 111 | self.allocator.free(entry_to_kill.key); | 140 | self.allocator.free(entry_to_kill.key); |
| 112 | entry_to_kill.value.deinit(); | 141 | entry_to_kill.value.deinit(); |
| 113 | 142 | ||
| @@ -120,26 +149,26 @@ pub fn killCurrentBuffer(self: *Editor) !bool { | |||
| 120 | return true; | 149 | return true; |
| 121 | } | 150 | } |
| 122 | 151 | ||
| 123 | pub fn open(self: *Editor, fname: []const u8) !void { | 152 | pub fn open(self: *Editor, name: []const u8) !void { |
| 124 | if (self.hasBuffer(fname)) { | 153 | const file_path = try files.resolvePath(self.allocator, name); |
| 125 | if (!try self.promptYN("A file with such name is already open. Open anyways?")) { | 154 | defer self.allocator.free(file_path); |
| 126 | return; | 155 | |
| 127 | } | 156 | if (self.getBufferByPath(file_path)) |buffer| { |
| 157 | self.buffer = buffer; | ||
| 158 | return; | ||
| 128 | } | 159 | } |
| 129 | 160 | ||
| 130 | self.buffer = try self.getOrPutBuffer(fname); | 161 | const file_name = std.fs.path.basename(file_path); |
| 131 | // TODO: If already was dirty, ask again | 162 | self.buffer = try self.putNewBuffer(file_name); |
| 132 | self.buffer.has_file = true; | 163 | self.buffer.file_path = try self.allocator.dupe(u8, file_path); |
| 133 | 164 | ||
| 134 | try self.buffer.selectSyntaxHighlighting(); | 165 | try self.buffer.selectSyntaxHighlighting(); |
| 135 | self.buffer.deleteAllRows(); | 166 | std.debug.assert(self.buffer.rows.items.len == 0); |
| 136 | 167 | ||
| 137 | const file = std.fs.cwd().openFile(fname, .{ .read = true }) catch |err| switch (err) { | 168 | const file = std.fs.openFileAbsolute(file_path, .{ .read = true }) catch |err| switch (err) { |
| 138 | error.FileNotFound => { | 169 | error.FileNotFound => { |
| 139 | try self.setStatusMessage("Creating a new file...", .{}); | 170 | try self.setStatusMessage("Will create a new file on save...", .{}); |
| 140 | self.buffer.dirty = true; | 171 | self.buffer.dirty = true; |
| 141 | const file = try std.fs.cwd().createFile(fname, .{ .read = true }); | ||
| 142 | file.close(); | ||
| 143 | return; | 172 | return; |
| 144 | }, | 173 | }, |
| 145 | else => return err, | 174 | else => return err, |
| @@ -149,6 +178,7 @@ pub fn open(self: *Editor, fname: []const u8) !void { | |||
| 149 | var buffered_reader = std.io.bufferedReader(file.reader()); | 178 | var buffered_reader = std.io.bufferedReader(file.reader()); |
| 150 | const reader = buffered_reader.reader(); | 179 | const reader = buffered_reader.reader(); |
| 151 | 180 | ||
| 181 | // TODO: Limiting lines to 4096 characters | ||
| 152 | while (try reader.readUntilDelimiterOrEofAlloc(self.allocator, '\n', 4096)) |line| { | 182 | while (try reader.readUntilDelimiterOrEofAlloc(self.allocator, '\n', 4096)) |line| { |
| 153 | defer self.allocator.free(line); | 183 | defer self.allocator.free(line); |
| 154 | 184 | ||
| @@ -168,7 +198,7 @@ pub fn openFile(self: *Editor) !void { | |||
| 168 | } | 198 | } |
| 169 | 199 | ||
| 170 | pub fn processKeypress(self: *Editor) !void { | 200 | pub fn processKeypress(self: *Editor) !void { |
| 171 | const key = try readKey(); | 201 | const key = try self.readKey(); |
| 172 | try self.current_state(self, self.buffer, key); | 202 | try self.current_state(self, self.buffer, key); |
| 173 | } | 203 | } |
| 174 | 204 | ||
| @@ -193,7 +223,7 @@ pub fn promptEx( | |||
| 193 | 223 | ||
| 194 | // TODO: Navigation | 224 | // TODO: Navigation |
| 195 | // TODO: Draw the cursor | 225 | // TODO: Draw the cursor |
| 196 | const key = try readKey(); | 226 | const key = try self.readKey(); |
| 197 | switch (key) { | 227 | switch (key) { |
| 198 | Key.delete, Key.backspace => _ = buf.popOrNull(), | 228 | Key.delete, Key.backspace => _ = buf.popOrNull(), |
| 199 | Key.ctrl('g') => { | 229 | Key.ctrl('g') => { |
| @@ -260,9 +290,21 @@ pub fn putBuffer(self: *Editor, buf_name: []const u8) !*Buffer { | |||
| 260 | prev_kv.value.deinit(); | 290 | prev_kv.value.deinit(); |
| 261 | } | 291 | } |
| 262 | 292 | ||
| 293 | // TODO: This feels inefficient | ||
| 263 | return self.buffers.getPtr(duped_name).?; | 294 | return self.buffers.getPtr(duped_name).?; |
| 264 | } | 295 | } |
| 265 | 296 | ||
| 297 | /// This always adds a new buffer, name might be modified to avoid clashes. | ||
| 298 | pub fn putNewBuffer(self: *Editor, name: []const u8) !*Buffer { | ||
| 299 | const unique_name = try self.getUniqueBufferName(self.allocator, name); | ||
| 300 | errdefer self.allocator.free(unique_name); | ||
| 301 | |||
| 302 | try self.buffers.putNoClobber(unique_name, try Buffer.init(self.allocator, unique_name)); | ||
| 303 | |||
| 304 | // TODO: This feels inefficient | ||
| 305 | return self.buffers.getPtr(unique_name).?; | ||
| 306 | } | ||
| 307 | |||
| 266 | pub fn refreshScreen(self: *Editor) !void { | 308 | pub fn refreshScreen(self: *Editor) !void { |
| 267 | self.buffer.scroll(self.screenrows, self.screencols); | 309 | self.buffer.scroll(self.screenrows, self.screencols); |
| 268 | 310 | ||
| @@ -392,37 +434,50 @@ fn parseUnsignedOptDefault(comptime T: type, buf_opt: ?[]const u8, radix: u8, de | |||
| 392 | } | 434 | } |
| 393 | } | 435 | } |
| 394 | 436 | ||
| 395 | fn readKey() !Key { | 437 | fn readByte(reader: std.fs.File) !?u8 { |
| 396 | const std_in = std.io.getStdIn(); | 438 | var buf = [_]u8{undefined}; |
| 397 | 439 | if (1 != try reader.read(&buf)) { | |
| 398 | var buf = [_]u8{undefined} ** 3; | 440 | return null; |
| 399 | // No we do not care about possible EOF on stdin, don't run the editor with | 441 | } else { |
| 400 | // redirected stdin | 442 | return buf[0]; |
| 401 | while (1 != try std_in.read(buf[0..1])) { | ||
| 402 | try std.os.sched_yield(); // :) | ||
| 403 | } | 443 | } |
| 444 | } | ||
| 404 | 445 | ||
| 405 | if (buf[0] != '\x1b') { | 446 | fn readByteBlocking(reader: std.fs.File) !u8 { |
| 406 | return @intToEnum(Key, buf[0]); | 447 | // No we do not care about possible EOF on stdin, don't run the editor with redirected stdin. |
| 448 | var char = try readByte(reader); | ||
| 449 | while (char == null) : (char = try readByte(reader)) { | ||
| 450 | std.os.sched_yield() catch {}; // :) | ||
| 407 | } | 451 | } |
| 408 | 452 | ||
| 409 | if (1 != try std_in.read(buf[0..1])) { | 453 | return char.?; |
| 410 | return Key.escape; | 454 | } |
| 455 | |||
| 456 | fn readKey(self: *Editor) !Key { | ||
| 457 | if (self.key_buffer.items.len > 0) { | ||
| 458 | return self.key_buffer.pop(); | ||
| 411 | } | 459 | } |
| 412 | 460 | ||
| 413 | if (buf[0] == '[') { | 461 | const std_in = std.io.getStdIn(); |
| 414 | if (1 != try std_in.read(buf[1..2])) { | 462 | |
| 415 | return Key.meta('['); | 463 | const char1 = try readByteBlocking(std_in); |
| 416 | } | 464 | if (char1 != '\x1b') { |
| 465 | return Key.char(char1); | ||
| 466 | } | ||
| 417 | 467 | ||
| 418 | if (buf[1] >= '0' and buf[1] <= '9') { | 468 | // TODO: This is a bad way of parsing. |
| 419 | if (1 != try std_in.read(buf[2..3])) { | 469 | const char2 = (try readByte(std_in)) orelse { return Key.escape; }; |
| 420 | // TODO: Multiple key return support? | 470 | if (char2 == '[') { |
| 471 | const char3 = (try readByte(std_in)) orelse { return Key.meta('['); }; | ||
| 472 | if (char3 >= '0' and char3 <= '9') { | ||
| 473 | const char4 = (try readByte(std_in)) orelse { | ||
| 474 | std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); | ||
| 475 | try self.key_buffer.append(Key.char(char3)); | ||
| 421 | return Key.meta('['); | 476 | return Key.meta('['); |
| 422 | } | 477 | }; |
| 423 | 478 | ||
| 424 | if (buf[2] == '~') { | 479 | if (char4 == '~') { |
| 425 | return switch (buf[1]) { | 480 | return switch (char3) { |
| 426 | '1' => Key.home, | 481 | '1' => Key.home, |
| 427 | '3' => Key.delete, | 482 | '3' => Key.delete, |
| 428 | '4' => Key.end, | 483 | '4' => Key.end, |
| @@ -430,37 +485,90 @@ fn readKey() !Key { | |||
| 430 | '6' => Key.page_down, | 485 | '6' => Key.page_down, |
| 431 | '7' => Key.home, | 486 | '7' => Key.home, |
| 432 | '8' => Key.end, | 487 | '8' => Key.end, |
| 433 | // TODO: Multiple key return support | 488 | else => { |
| 434 | else => Key.meta('['), | 489 | std.log.err("Unknown terminal sequence '^[[{c}~'", .{char3}); |
| 490 | try self.key_buffer.append(Key.char('~')); | ||
| 491 | try self.key_buffer.append(Key.char(char3)); | ||
| 492 | return Key.meta('['); | ||
| 493 | }, | ||
| 494 | }; | ||
| 495 | } else if (char4 == ';' and char3 == '1') { | ||
| 496 | const char5 = (try readByte(std_in)) orelse { | ||
| 497 | std.log.err("Unknown terminal sequence '^[[1;'", .{}); | ||
| 498 | try self.key_buffer.append(Key.char(';')); | ||
| 499 | try self.key_buffer.append(Key.char('1')); | ||
| 500 | return Key.meta('['); | ||
| 435 | }; | 501 | }; |
| 502 | |||
| 503 | if (char5 == '5') { | ||
| 504 | const char6 = (try readByte(std_in)) orelse { | ||
| 505 | std.log.err("Unknown terminal sequence '^[[1;5'", .{}); | ||
| 506 | try self.key_buffer.append(Key.char('5')); | ||
| 507 | try self.key_buffer.append(Key.char(';')); | ||
| 508 | try self.key_buffer.append(Key.char('1')); | ||
| 509 | return Key.meta('['); | ||
| 510 | }; | ||
| 511 | |||
| 512 | return switch (char6) { | ||
| 513 | 'A' => Key.ctrl_up, | ||
| 514 | 'B' => Key.ctrl_down, | ||
| 515 | 'C' => Key.ctrl_right, | ||
| 516 | 'D' => Key.ctrl_left, | ||
| 517 | 'F' => Key.ctrl_end, | ||
| 518 | 'H' => Key.ctrl_home, | ||
| 519 | else => { | ||
| 520 | std.log.err("Unknown terminal sequence '^[[1;5{c}'", .{char6}); | ||
| 521 | try self.key_buffer.append(Key.char(char6)); | ||
| 522 | try self.key_buffer.append(Key.char('5')); | ||
| 523 | try self.key_buffer.append(Key.char(';')); | ||
| 524 | try self.key_buffer.append(Key.char('1')); | ||
| 525 | return Key.meta('['); | ||
| 526 | }, | ||
| 527 | }; | ||
| 528 | } else { | ||
| 529 | std.log.err("Unknown terminal sequence '^[[1;{c}'", .{char5}); | ||
| 530 | try self.key_buffer.append(Key.char(char5)); | ||
| 531 | try self.key_buffer.append(Key.char(';')); | ||
| 532 | try self.key_buffer.append(Key.char('1')); | ||
| 533 | return Key.meta('['); | ||
| 534 | } | ||
| 436 | } else { | 535 | } else { |
| 437 | // TODO: Multiple key return support | 536 | std.log.err("Unknown terminal sequence '^[[{c}{c}'", .{char3, char4}); |
| 537 | try self.key_buffer.append(Key.char(char4)); | ||
| 538 | try self.key_buffer.append(Key.char(char3)); | ||
| 438 | return Key.meta('['); | 539 | return Key.meta('['); |
| 439 | } | 540 | } |
| 440 | } else { | 541 | } else { |
| 441 | return switch (buf[1]) { | 542 | return switch (char3) { |
| 442 | 'A' => Key.up, | 543 | 'A' => Key.up, |
| 443 | 'B' => Key.down, | 544 | 'B' => Key.down, |
| 444 | 'C' => Key.right, | 545 | 'C' => Key.right, |
| 445 | 'D' => Key.left, | 546 | 'D' => Key.left, |
| 446 | 'F' => Key.end, | 547 | 'F' => Key.end, |
| 447 | 'H' => Key.home, | 548 | 'H' => Key.home, |
| 448 | // TODO: Multiple key return support | 549 | else => { |
| 449 | else => Key.meta('['), | 550 | std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); |
| 551 | try self.key_buffer.append(Key.char(char3)); | ||
| 552 | return Key.meta('['); | ||
| 553 | }, | ||
| 450 | }; | 554 | }; |
| 451 | } | 555 | } |
| 452 | } else if (buf[0] == 'O') { | 556 | } else if (char2 == 'O') { |
| 453 | if (1 != try std_in.read(buf[1..2])) { | 557 | const char3 = (try readByte(std_in)) orelse { return Key.meta('O'); }; |
| 454 | return Key.meta('O'); | 558 | return switch (char3) { |
| 455 | } | ||
| 456 | |||
| 457 | return switch (buf[1]) { | ||
| 458 | 'F' => Key.end, | 559 | 'F' => Key.end, |
| 459 | 'H' => Key.home, | 560 | 'H' => Key.home, |
| 460 | // TODO: Multiple key return support | 561 | else => { |
| 461 | else => Key.meta('O'), | 562 | std.log.err("Unknown terminal sequence '^[O{c}'", .{char3}); |
| 563 | try self.key_buffer.append(Key.char(char3)); | ||
| 564 | return Key.meta('O'); | ||
| 565 | }, | ||
| 462 | }; | 566 | }; |
| 463 | } else { | 567 | } else { |
| 464 | return Key.meta(buf[0]); | 568 | return Key.meta(char2); |
| 465 | } | 569 | } |
| 466 | } | 570 | } |
| 571 | // C-<page up> = ^[[5;5~ | ||
| 572 | // C-<page down> = ^[[6;5 | ||
| 573 | |||
| 574 | // 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 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | |||
| 5 | pub fn resolvePath(allocator: Allocator, name: []const u8) ![]u8 { | ||
| 6 | return std.fs.cwd().realpathAlloc(allocator, name) catch |err| switch (err) { | ||
| 7 | error.FileNotFound => if (name.len == 0) { | ||
| 8 | return error.FileNotFound; | ||
| 9 | } else if (name[0] == std.fs.path.sep) { | ||
| 10 | // Already is an absolute path | ||
| 11 | return allocator.dupe(u8, name); | ||
| 12 | } else { | ||
| 13 | // TODO: if (name[0] == '~') | ||
| 14 | const cwd = try std.process.getCwdAlloc(allocator); | ||
| 15 | defer allocator.free(cwd); | ||
| 16 | |||
| 17 | return std.fmt.allocPrint(allocator, "{s}{s}{s}", .{cwd, std.fs.path.sep_str, name}); | ||
| 18 | }, | ||
| 19 | else => return err, | ||
| 20 | }; | ||
| 21 | } | ||
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) { | |||
| 14 | right, | 14 | right, |
| 15 | up, | 15 | up, |
| 16 | down, | 16 | down, |
| 17 | delete, | ||
| 18 | home, | 17 | home, |
| 19 | end, | 18 | end, |
| 19 | delete, | ||
| 20 | page_up, | 20 | page_up, |
| 21 | page_down, | 21 | page_down, |
| 22 | 22 | ||
| 23 | ctrl_left, | ||
| 24 | ctrl_right, | ||
| 25 | ctrl_up, | ||
| 26 | ctrl_down, | ||
| 27 | ctrl_home, | ||
| 28 | ctrl_end, | ||
| 29 | // ctrl_delete, | ||
| 30 | // ctrl_page_up, | ||
| 31 | // ctrl_page_down, | ||
| 32 | |||
| 23 | _, | 33 | _, |
| 24 | 34 | ||
| 25 | pub fn char(ch: u8) Key { | 35 | pub fn char(ch: u8) Key { |
| @@ -53,12 +63,22 @@ pub const Key = enum(u16) { | |||
| 53 | .right => std.fmt.formatBuf("<right>", options, writer), | 63 | .right => std.fmt.formatBuf("<right>", options, writer), |
| 54 | .up => std.fmt.formatBuf("<up>", options, writer), | 64 | .up => std.fmt.formatBuf("<up>", options, writer), |
| 55 | .down => std.fmt.formatBuf("<down>", options, writer), | 65 | .down => std.fmt.formatBuf("<down>", options, writer), |
| 56 | .delete => std.fmt.formatBuf("<delete>", options, writer), | ||
| 57 | .home => std.fmt.formatBuf("<home>", options, writer), | 66 | .home => std.fmt.formatBuf("<home>", options, writer), |
| 58 | .end => std.fmt.formatBuf("<end>", options, writer), | 67 | .end => std.fmt.formatBuf("<end>", options, writer), |
| 68 | .delete => std.fmt.formatBuf("<delete>", options, writer), | ||
| 59 | .page_up => std.fmt.formatBuf("<page-up>", options, writer), | 69 | .page_up => std.fmt.formatBuf("<page-up>", options, writer), |
| 60 | .page_down => std.fmt.formatBuf("<page-down>", options, writer), | 70 | .page_down => std.fmt.formatBuf("<page-down>", options, writer), |
| 61 | 71 | ||
| 72 | .ctrl_left => std.fmt.formatBuf("C-<left>", options, writer), | ||
| 73 | .ctrl_right => std.fmt.formatBuf("C-<right>", options, writer), | ||
| 74 | .ctrl_up => std.fmt.formatBuf("C-<up>", options, writer), | ||
| 75 | .ctrl_down => std.fmt.formatBuf("C-<down>", options, writer), | ||
| 76 | .ctrl_home => std.fmt.formatBuf("C-<home>", options, writer), | ||
| 77 | .ctrl_end => std.fmt.formatBuf("C-<end>", options, writer), | ||
| 78 | // ctrl_delete | ||
| 79 | // ctrl_page_up | ||
| 80 | // ctrl_page_down | ||
| 81 | |||
| 62 | _ => key.formatGeneric(options, writer), | 82 | _ => key.formatGeneric(options, writer), |
| 63 | }; | 83 | }; |
| 64 | } | 84 | } |
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{ | |||
| 15 | std.fs.File.OpenError || | 15 | std.fs.File.OpenError || |
| 16 | std.fs.File.ReadError || | 16 | std.fs.File.ReadError || |
| 17 | std.fs.File.WriteError || | 17 | std.fs.File.WriteError || |
| 18 | std.os.SchedYieldError; | 18 | std.os.GetCwdError || |
| 19 | std.os.RealPathError; | ||
| 19 | pub const KeyState = fn (*Editor, *Buffer, Key) Error!void; | 20 | pub const KeyState = fn (*Editor, *Buffer, Key) Error!void; |
| 20 | 21 | ||
| 21 | pub fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { | 22 | pub fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { |