diff options
Diffstat (limited to 'src/Editor.zig')
| -rw-r--r-- | src/Editor.zig | 461 |
1 files changed, 461 insertions, 0 deletions
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 @@ | |||
| 1 | const linux = std.os.linux; | ||
| 2 | const std = @import("std"); | ||
| 3 | |||
| 4 | const Allocator = std.mem.Allocator; | ||
| 5 | const ArrayList = std.ArrayList; | ||
| 6 | const Buffer = @import("Buffer.zig"); | ||
| 7 | const Editor = @This(); | ||
| 8 | const Key = @import("key.zig").Key; | ||
| 9 | const key_state = @import("key_state.zig"); | ||
| 10 | const KeyState = key_state.KeyState; | ||
| 11 | const STDIN_FILENO = std.os.STDIN_FILENO; | ||
| 12 | const StringBuilder = @import("StringBuilder.zig"); | ||
| 13 | const StringHashMap = std.StringHashMap; | ||
| 14 | |||
| 15 | allocator: Allocator, | ||
| 16 | |||
| 17 | buffers: StringHashMap(Buffer), | ||
| 18 | buffer: *Buffer, | ||
| 19 | |||
| 20 | screenrows: usize, | ||
| 21 | screencols: usize, | ||
| 22 | |||
| 23 | statusmsg: ?[]u8, | ||
| 24 | statusmsg_time: i64, | ||
| 25 | |||
| 26 | current_state: KeyState, | ||
| 27 | |||
| 28 | should_exit: bool, | ||
| 29 | |||
| 30 | pub fn init(allocator: Allocator) !Editor { | ||
| 31 | var self = Editor{ | ||
| 32 | .allocator = allocator, | ||
| 33 | |||
| 34 | .buffers = StringHashMap(Buffer).init(allocator), | ||
| 35 | .buffer = undefined, | ||
| 36 | |||
| 37 | .screenrows = undefined, | ||
| 38 | .screencols = undefined, | ||
| 39 | |||
| 40 | .statusmsg = null, | ||
| 41 | .statusmsg_time = 0, | ||
| 42 | |||
| 43 | .current_state = key_state.defaultState, | ||
| 44 | |||
| 45 | .should_exit = false, | ||
| 46 | }; | ||
| 47 | errdefer self.deinit(); | ||
| 48 | |||
| 49 | // Initializes .screenrows and .screencols | ||
| 50 | try self.refreshWindowSize(); | ||
| 51 | |||
| 52 | self.buffer = try self.putBuffer("*scratch*"); | ||
| 53 | |||
| 54 | return self; | ||
| 55 | } | ||
| 56 | |||
| 57 | pub fn deinit(self: *Editor) void { | ||
| 58 | var buf_iterator = self.buffers.iterator(); | ||
| 59 | while (buf_iterator.next()) |kv| { | ||
| 60 | self.allocator.free(kv.key_ptr.*); | ||
| 61 | kv.value_ptr.deinit(); | ||
| 62 | } | ||
| 63 | self.buffers.deinit(); | ||
| 64 | |||
| 65 | if (self.statusmsg) |statusmsg| { | ||
| 66 | self.allocator.free(statusmsg); | ||
| 67 | } | ||
| 68 | |||
| 69 | self.* = undefined; | ||
| 70 | } | ||
| 71 | |||
| 72 | pub fn clearStatusMessage(self: *Editor) void { | ||
| 73 | if (self.statusmsg) |statusmsg| { | ||
| 74 | self.statusmsg = null; | ||
| 75 | self.allocator.free(statusmsg); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | pub fn getBuffer(self: Editor, name: []const u8) ?*Buffer { | ||
| 80 | return self.buffers.getPtr(name); | ||
| 81 | } | ||
| 82 | |||
| 83 | pub fn getOrPutBuffer(self: *Editor, name: []const u8) !*Buffer { | ||
| 84 | const get_or_put_res = try self.buffers.getOrPut(name); | ||
| 85 | if (get_or_put_res.found_existing) { | ||
| 86 | return get_or_put_res.value_ptr; | ||
| 87 | } | ||
| 88 | |||
| 89 | get_or_put_res.key_ptr.* = try self.allocator.dupe(u8, name); | ||
| 90 | errdefer self.allocator.free(get_or_put_res.key_ptr.*); | ||
| 91 | |||
| 92 | get_or_put_res.value_ptr.* = try Buffer.init(self.allocator, name); | ||
| 93 | errdefer get_or_put_res.value_ptr.deinit(); | ||
| 94 | |||
| 95 | return get_or_put_res.value_ptr; | ||
| 96 | } | ||
| 97 | |||
| 98 | pub fn hasBuffer(self: Editor, name: []const u8) bool { | ||
| 99 | return self.getBuffer(name) != null; | ||
| 100 | } | ||
| 101 | |||
| 102 | /// Returns true if killed, false if didn't. | ||
| 103 | pub fn killCurrentBuffer(self: *Editor) !bool { | ||
| 104 | if (self.buffer.dirty) { | ||
| 105 | if (!try self.promptYN("Unsaved changes, kill anyways?")) { | ||
| 106 | return false; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | const entry_to_kill = self.buffers.fetchRemove(self.buffer.name).?; | ||
| 111 | self.allocator.free(entry_to_kill.key); | ||
| 112 | entry_to_kill.value.deinit(); | ||
| 113 | |||
| 114 | if (self.buffers.valueIterator().next()) |buffer| { | ||
| 115 | self.buffer = buffer; | ||
| 116 | } else { | ||
| 117 | self.buffer = try self.putBuffer("*scratch*"); | ||
| 118 | } | ||
| 119 | |||
| 120 | return true; | ||
| 121 | } | ||
| 122 | |||
| 123 | pub fn open(self: *Editor, fname: []const u8) !void { | ||
| 124 | if (self.hasBuffer(fname)) { | ||
| 125 | if (!try self.promptYN("A file with such name is already open. Open anyways?")) { | ||
| 126 | return; | ||
| 127 | } | ||
| 128 | } | ||
| 129 | |||
| 130 | self.buffer = try self.getOrPutBuffer(fname); | ||
| 131 | // TODO: If already was dirty, ask again | ||
| 132 | self.buffer.has_file = true; | ||
| 133 | |||
| 134 | try self.buffer.selectSyntaxHighlighting(); | ||
| 135 | self.buffer.deleteAllRows(); | ||
| 136 | |||
| 137 | const file = std.fs.cwd().openFile(fname, .{ .read = true }) catch |err| switch (err) { | ||
| 138 | error.FileNotFound => { | ||
| 139 | try self.setStatusMessage("Creating a new file...", .{}); | ||
| 140 | self.buffer.dirty = true; | ||
| 141 | const file = try std.fs.cwd().createFile(fname, .{ .read = true }); | ||
| 142 | file.close(); | ||
| 143 | return; | ||
| 144 | }, | ||
| 145 | else => return err, | ||
| 146 | }; | ||
| 147 | defer file.close(); | ||
| 148 | |||
| 149 | var buffered_reader = std.io.bufferedReader(file.reader()); | ||
| 150 | const reader = buffered_reader.reader(); | ||
| 151 | |||
| 152 | while (try reader.readUntilDelimiterOrEofAlloc(self.allocator, '\n', 4096)) |line| { | ||
| 153 | defer self.allocator.free(line); | ||
| 154 | |||
| 155 | const trimmed = std.mem.trim(u8, line, "\r\n"); | ||
| 156 | try self.buffer.appendRow(trimmed); | ||
| 157 | } | ||
| 158 | |||
| 159 | self.buffer.dirty = false; | ||
| 160 | } | ||
| 161 | |||
| 162 | pub fn openFile(self: *Editor) !void { | ||
| 163 | const fname_opt = try self.prompt("File name"); | ||
| 164 | if (fname_opt) |fname| { | ||
| 165 | defer self.allocator.free(fname); | ||
| 166 | return self.open(fname); | ||
| 167 | } | ||
| 168 | } | ||
| 169 | |||
| 170 | pub fn processKeypress(self: *Editor) !void { | ||
| 171 | const key = try readKey(); | ||
| 172 | try self.current_state(self, self.buffer, key); | ||
| 173 | } | ||
| 174 | |||
| 175 | pub fn prompt(self: *Editor, prompt_str: []const u8) !?[]u8 { | ||
| 176 | return self.promptEx(void, error{}, prompt_str, null, {}); | ||
| 177 | } | ||
| 178 | |||
| 179 | pub fn promptEx( | ||
| 180 | self: *Editor, | ||
| 181 | comptime CallbackData: type, | ||
| 182 | comptime CallbackError: type, | ||
| 183 | prompt_str: []const u8, | ||
| 184 | callback: ?PromptCallback(CallbackData, CallbackError), | ||
| 185 | cb_data: CallbackData, | ||
| 186 | ) !?[]u8 { | ||
| 187 | var buf = ArrayList(u8).init(self.allocator); | ||
| 188 | defer buf.deinit(); | ||
| 189 | |||
| 190 | while (true) { | ||
| 191 | try self.setStatusMessage("{s}: {s}", .{prompt_str, buf.items}); | ||
| 192 | try self.refreshScreen(); | ||
| 193 | |||
| 194 | // TODO: Navigation | ||
| 195 | // TODO: Draw the cursor | ||
| 196 | const key = try readKey(); | ||
| 197 | switch (key) { | ||
| 198 | Key.delete, Key.backspace => _ = buf.popOrNull(), | ||
| 199 | Key.ctrl('g') => { | ||
| 200 | try self.setStatusMessage("Cancelled", .{}); | ||
| 201 | if (callback) |cb| { | ||
| 202 | try cb(self, buf.items, key, cb_data); | ||
| 203 | } | ||
| 204 | |||
| 205 | return null; | ||
| 206 | }, | ||
| 207 | Key.return_ => { | ||
| 208 | self.clearStatusMessage(); | ||
| 209 | if (callback) |cb| { | ||
| 210 | try cb(self, buf.items, key, cb_data); | ||
| 211 | } | ||
| 212 | |||
| 213 | return buf.toOwnedSlice(); | ||
| 214 | }, | ||
| 215 | else => if (@enumToInt(key) < @enumToInt(Key.max_char)) { | ||
| 216 | const key_char = @intCast(u8, @enumToInt(key)); | ||
| 217 | if (std.ascii.isSpace(key_char) or std.ascii.isGraph(key_char)) { | ||
| 218 | try buf.append(key_char); | ||
| 219 | } | ||
| 220 | } // else ?? | ||
| 221 | } | ||
| 222 | |||
| 223 | if (callback) |cb| { | ||
| 224 | try cb(self, buf.items, key, cb_data); | ||
| 225 | } | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | pub fn PromptCallback(comptime Data: type, comptime Error: type) type { | ||
| 230 | return fn(*Editor, []const u8, Key, Data) Error!void; | ||
| 231 | } | ||
| 232 | |||
| 233 | pub fn promptYN(self: *Editor, prompt_str: []const u8) !bool { | ||
| 234 | const full_prompt = try std.fmt.allocPrint(self.allocator, "{s} (Y/N)", .{prompt_str}); | ||
| 235 | defer self.allocator.free(full_prompt); | ||
| 236 | |||
| 237 | var response = try self.prompt(full_prompt); | ||
| 238 | defer if (response) |str| self.allocator.free(str); | ||
| 239 | // TODO: This can be improved | ||
| 240 | while (response == null | ||
| 241 | or (response.?[0] != 'y' and response.?[0] != 'Y' and response.?[0] != 'n' and response.?[0] != 'N')) { | ||
| 242 | if (response) |str| self.allocator.free(str); | ||
| 243 | response = try self.prompt(full_prompt); | ||
| 244 | } | ||
| 245 | |||
| 246 | return response.?[0] == 'y' or response.?[0] == 'Y'; | ||
| 247 | } | ||
| 248 | |||
| 249 | pub fn putBuffer(self: *Editor, buf_name: []const u8) !*Buffer { | ||
| 250 | const duped_name = try self.allocator.dupe(u8, buf_name); | ||
| 251 | errdefer self.allocator.free(duped_name); | ||
| 252 | |||
| 253 | if (try self.buffers.fetchPut(duped_name, try Buffer.init(self.allocator, duped_name))) |prev_kv| { | ||
| 254 | self.allocator.free(prev_kv.key); | ||
| 255 | prev_kv.value.deinit(); | ||
| 256 | } | ||
| 257 | |||
| 258 | return self.buffers.getPtr(duped_name).?; | ||
| 259 | } | ||
| 260 | |||
| 261 | pub fn refreshScreen(self: *Editor) !void { | ||
| 262 | self.buffer.scroll(self.screenrows, self.screencols); | ||
| 263 | |||
| 264 | var sb = StringBuilder.init(self.allocator); | ||
| 265 | const writer = sb.writer(); | ||
| 266 | defer sb.deinit(); | ||
| 267 | |||
| 268 | try writer.writeAll("\x1b[?25l\x1b[H"); | ||
| 269 | |||
| 270 | try self.buffer.drawRows(writer, self.screenrows, self.screencols); | ||
| 271 | try self.buffer.drawStatusBar(writer, self.screencols); | ||
| 272 | try self.drawMessageBar(writer); | ||
| 273 | |||
| 274 | try writer.print("\x1b[{};{}H", .{ | ||
| 275 | self.buffer.cy - self.buffer.rowoff + 1, | ||
| 276 | self.buffer.rx - self.buffer.coloff + 1 + self.buffer.lineNumberDigits(), | ||
| 277 | }); | ||
| 278 | try writer.writeAll("\x1b[?25h"); | ||
| 279 | |||
| 280 | try std.io.getStdOut().writeAll(sb.seeSlice()); | ||
| 281 | } | ||
| 282 | |||
| 283 | pub fn refreshWindowSize(self: *Editor) !void { | ||
| 284 | try getWindowSize(&self.screenrows, &self.screencols); | ||
| 285 | self.screenrows -= 2; | ||
| 286 | } | ||
| 287 | |||
| 288 | pub fn saveBuffersExit(self: *Editor) !void { | ||
| 289 | while (self.buffers.count() > 1 or self.buffer.dirty) { | ||
| 290 | if (!try self.killCurrentBuffer()) { | ||
| 291 | return; | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | try std.io.getStdOut().writeAll("\x1b[2J\x1b[H"); | ||
| 296 | self.should_exit = true; | ||
| 297 | } | ||
| 298 | |||
| 299 | pub fn setStatusMessage(self: *Editor, comptime fmt: []const u8, args: anytype) !void { | ||
| 300 | // Get new resources | ||
| 301 | var new_msg = try std.fmt.allocPrint(self.allocator, fmt, args); | ||
| 302 | errdefer self.allocator.free(new_msg); | ||
| 303 | |||
| 304 | // Get rid of old resources (no errors) | ||
| 305 | if (self.statusmsg) |old_msg| { | ||
| 306 | self.statusmsg = null; | ||
| 307 | self.allocator.free(old_msg); | ||
| 308 | } | ||
| 309 | |||
| 310 | // Assign new resources (no errors) | ||
| 311 | self.statusmsg = new_msg; | ||
| 312 | self.statusmsg_time = std.time.milliTimestamp(); | ||
| 313 | } | ||
| 314 | |||
| 315 | pub fn switchBuffer(self: *Editor) !void { | ||
| 316 | // TODO: completion | ||
| 317 | const bufname_opt = try self.prompt("Switch to buffer"); | ||
| 318 | if (bufname_opt) |bufname| { | ||
| 319 | defer self.allocator.free(bufname); | ||
| 320 | |||
| 321 | if (self.getBuffer(bufname)) |buffer| { | ||
| 322 | self.buffer = buffer; | ||
| 323 | } else { | ||
| 324 | try self.setStatusMessage("There is no buffer named '{s}'!", .{bufname}); | ||
| 325 | } | ||
| 326 | } | ||
| 327 | } | ||
| 328 | |||
| 329 | fn drawMessageBar(self: Editor, writer: anytype) !void { | ||
| 330 | try writer.writeAll("\x1b[K"); | ||
| 331 | if (self.statusmsg == null) { | ||
| 332 | return; | ||
| 333 | } | ||
| 334 | |||
| 335 | if (self.statusmsg.?.len != 0 and std.time.milliTimestamp() - self.statusmsg_time < 5 * std.time.ms_per_s) { | ||
| 336 | try writer.writeAll(self.statusmsg.?[0..(std.math.min(self.statusmsg.?.len, self.screencols))]); | ||
| 337 | } | ||
| 338 | } | ||
| 339 | |||
| 340 | fn getCursorPosition(row: *usize, col: *usize) !void { | ||
| 341 | const std_out = std.io.getStdOut(); | ||
| 342 | try std_out.writeAll("\x1b[6n\r\n"); | ||
| 343 | |||
| 344 | const std_in = std.io.getStdIn().reader(); | ||
| 345 | var buf = [_]u8 { undefined } ** 32; | ||
| 346 | var response = std_in.readUntilDelimiter(&buf, 'R') catch |err| switch (err) { | ||
| 347 | error.EndOfStream => return error.MisformedTerminalResponse, | ||
| 348 | error.StreamTooLong => return error.MisformedTerminalResponse, | ||
| 349 | else => return @errSetCast(std.os.ReadError, err), | ||
| 350 | }; | ||
| 351 | |||
| 352 | if (response.len < 2 or response[0] != '\x1b' or response[1] != '[') { | ||
| 353 | return error.MisformedTerminalResponse; | ||
| 354 | } | ||
| 355 | |||
| 356 | response = response[2..]; | ||
| 357 | |||
| 358 | var split_it = std.mem.split(u8, response, ";"); | ||
| 359 | row.* = parseUnsignedOptDefault(usize, split_it.next(), 10, 1) catch return error.MisformedTerminalResponse; | ||
| 360 | col.* = parseUnsignedOptDefault(usize, split_it.next(), 10, 1) catch return error.MisformedTerminalResponse; | ||
| 361 | if (split_it.next()) |_| { | ||
| 362 | return error.MisformedTerminalResponse; | ||
| 363 | } | ||
| 364 | } | ||
| 365 | |||
| 366 | fn getWindowSize(rows: *usize, cols: *usize) !void { | ||
| 367 | var ws: linux.winsize = undefined; | ||
| 368 | const rc = linux.ioctl(STDIN_FILENO, linux.T.IOCGWINSZ, @ptrToInt(&ws)); | ||
| 369 | switch (linux.getErrno(rc)) { | ||
| 370 | .SUCCESS => { | ||
| 371 | cols.* = ws.ws_col; | ||
| 372 | rows.* = ws.ws_row; | ||
| 373 | }, | ||
| 374 | else => { | ||
| 375 | const std_out = std.io.getStdOut(); | ||
| 376 | try std_out.writeAll("\x1b[999C\x1b[999B"); | ||
| 377 | return getCursorPosition(rows, cols); | ||
| 378 | }, | ||
| 379 | } | ||
| 380 | } | ||
| 381 | |||
| 382 | fn parseUnsignedOptDefault(comptime T: type, buf_opt: ?[]const u8, radix: u8, default: T) !T { | ||
| 383 | if (buf_opt) |buf| { | ||
| 384 | return std.fmt.parseUnsigned(T, buf, radix); | ||
| 385 | } else { | ||
| 386 | return default; | ||
| 387 | } | ||
| 388 | } | ||
| 389 | |||
| 390 | fn readKey() !Key { | ||
| 391 | const std_in = std.io.getStdIn(); | ||
| 392 | |||
| 393 | var buf = [_]u8{undefined} ** 3; | ||
| 394 | // No we do not care about possible EOF on stdin, don't run the editor with | ||
| 395 | // redirected stdin | ||
| 396 | while (1 != try std_in.read(buf[0..1])) { | ||
| 397 | try std.os.sched_yield(); // :) | ||
| 398 | } | ||
| 399 | |||
| 400 | if (buf[0] != '\x1b') { | ||
| 401 | return @intToEnum(Key, buf[0]); | ||
| 402 | } | ||
| 403 | |||
| 404 | if (1 != try std_in.read(buf[0..1])) { | ||
| 405 | return Key.escape; | ||
| 406 | } | ||
| 407 | |||
| 408 | if (buf[0] == '[') { | ||
| 409 | if (1 != try std_in.read(buf[1..2])) { | ||
| 410 | return Key.meta('['); | ||
| 411 | } | ||
| 412 | |||
| 413 | if (buf[1] >= '0' and buf[1] <= '9') { | ||
| 414 | if (1 != try std_in.read(buf[2..3])) { | ||
| 415 | // TODO: Multiple key return support? | ||
| 416 | return Key.meta('['); | ||
| 417 | } | ||
| 418 | |||
| 419 | if (buf[2] == '~') { | ||
| 420 | return switch (buf[1]) { | ||
| 421 | '1' => Key.home, | ||
| 422 | '3' => Key.delete, | ||
| 423 | '4' => Key.end, | ||
| 424 | '5' => Key.page_up, | ||
| 425 | '6' => Key.page_down, | ||
| 426 | '7' => Key.home, | ||
| 427 | '8' => Key.end, | ||
| 428 | // TODO: Multiple key return support | ||
| 429 | else => Key.meta('['), | ||
| 430 | }; | ||
| 431 | } else { | ||
| 432 | // TODO: Multiple key return support | ||
| 433 | return Key.meta('['); | ||
| 434 | } | ||
| 435 | } else { | ||
| 436 | return switch (buf[1]) { | ||
| 437 | 'A' => Key.up, | ||
| 438 | 'B' => Key.down, | ||
| 439 | 'C' => Key.right, | ||
| 440 | 'D' => Key.left, | ||
| 441 | 'F' => Key.end, | ||
| 442 | 'H' => Key.home, | ||
| 443 | // TODO: Multiple key return support | ||
| 444 | else => Key.meta('['), | ||
| 445 | }; | ||
| 446 | } | ||
| 447 | } else if (buf[0] == 'O') { | ||
| 448 | if (1 != try std_in.read(buf[1..2])) { | ||
| 449 | return Key.meta('O'); | ||
| 450 | } | ||
| 451 | |||
| 452 | return switch (buf[1]) { | ||
| 453 | 'F' => Key.end, | ||
| 454 | 'H' => Key.home, | ||
| 455 | // TODO: Multiple key return support | ||
| 456 | else => Key.meta('O'), | ||
| 457 | }; | ||
| 458 | } else { | ||
| 459 | return Key.meta(buf[0]); | ||
| 460 | } | ||
| 461 | } | ||