const es = @import("root"); const std = @import("std"); const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Buffer = @This(); const Config = es.Config; const File = std.fs.File; const Editor = es.Editor; const Highlight = es.Highlight; const Row = es.Row; const Syntax = es.Syntax; allocator: Allocator, file_path: ?[]u8, short_name: []u8, rows: ArrayList(Row), cx: usize, cy: usize, rx: usize, rowoff: usize, coloff: usize, edited: bool, config: Config, syntax: ?Syntax, pub fn init(allocator: Allocator, name: []const u8) !Buffer { const name_owned = try allocator.dupe(u8, name); errdefer allocator.free(name_owned); // TODO: buffer-specific config support const config = try Config.readConfig(allocator); return Buffer{ .allocator = allocator, .file_path = null, .short_name = name_owned, .rows = ArrayList(Row).init(allocator), .cx = 0, .cy = 0, .rx = 0, .rowoff = 0, .coloff = 0, .edited = false, .config = config, .syntax = null, }; } pub fn deinit(self: Buffer) void { if (self.file_path) |file_path| { self.allocator.free(file_path); } self.allocator.free(self.short_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 { if (self.cx == 0 and self.cy == 0) { return; } self.backwardChar(); return self.deleteChar(); } pub fn backwardDeleteWord(self: *Buffer) !void { const start = self.findBackwardWordStart(); if (start == self.cx) { return self.backwardDeleteChar(); } else { var row = &self.rows.items[self.cy]; row.data.replaceRangeAssumeCapacity(start, self.cx - start, &.{}); self.cx = start; return row.update(self); } } pub fn backwardParagraph(self: *Buffer) void { if (self.cy == 0) { return; } self.cy -= 1; while (self.cy > 0) : (self.cy -= 1) { // TODO: I'm lazy but also consider lines of only whitespace if (self.rows.items[self.cy].data.items.len == 0) { break; } } self.cx = 0; } pub fn backwardWord(self: *Buffer) void { const target = self.findBackwardWordStart(); if (target == self.cx) { self.backwardChar(); } else { self.cx = target; } } pub fn beginningOfBuffer(self: *Buffer) void { self.cx = 0; self.cy = 0; } 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.edited = true; try self.rows.items[self.cy].deleteChar(self, self.cx); } else { if (self.cy == self.rows.items.len - 1) { return; } self.edited = 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.edited = true; while (self.rows.popOrNull()) |row| { row.deinit(); } } pub fn deleteWord(self: *Buffer) !void { const end = self.findForwardWordEnd(); if (end == self.cx) { return self.deleteChar(); } else { var row = &self.rows.items[self.cy]; row.data.replaceRangeAssumeCapacity(self.cx, end - self.cx, &.{}); return row.update(self); } } pub fn deleteRow(self: *Buffer, at: usize) void { if (at == self.rows.items.len) { return; } self.edited = true; self.rows.orderedRemove(at).deinit(); var i = at; while (i < self.rows.items.len) : (i += 1) { self.rows.items[i].idx -= 1; } if (self.cy == self.rows.items.len) { self.cx = 0; } else { self.cx = @min(self.cx, self.rows.items[self.cy].data.items.len); } } 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.isControl(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.conf.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"); const 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.short_name); defer self.allocator.free(name); const modified = if (self.isDirty()) @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); const rlen = if (rbuf.len > screencols) screencols else rbuf.len; const 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 endOfBuffer(self: *Buffer) void { self.cx = 0; self.cy = self.rows.items.len; } pub fn findBackwardWordStart(self: Buffer) usize { if (self.cy == self.rows.items.len) { return 0; } const chars = self.rows.items[self.cy].data.items; var start = self.cx; // First we skip non-word while (start > 0) : (start -= 1) { if (std.ascii.isAlphanumeric(chars[start - 1])) { break; } } // Then we skip word while (start > 0) : (start -= 1) { if (!std.ascii.isAlphanumeric(chars[start - 1])) { break; } } return start; } pub fn findForwardWordEnd(self: Buffer) usize { if (self.cy == self.rows.items.len) { return 0; } const chars = self.rows.items[self.cy].data.items; var end = self.cx; // First we skip non-word while (end < chars.len) : (end += 1) { if (std.ascii.isAlphanumeric(chars[end])) { break; } } // Then we skip word while (end < chars.len) : (end += 1) { if (!std.ascii.isAlphanumeric(chars[end])) { break; } } return end; } 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 forwardParagraph(self: *Buffer) void { if (self.cy == self.rows.items.len) { return; } self.cy += 1; while (self.cy < self.rows.items.len) : (self.cy += 1) { // TODO: I'm lazy but also consider whitespace-only lines if (self.rows.items[self.cy].data.items.len == 0) { break; } } self.cx = 0; } pub fn forwardWord(self: *Buffer) void { const end = self.findForwardWordEnd(); if (end == self.cx) { self.forwardChar(); } else { self.cx = end; } } // TODO: Make use of the callback feature to go to the final line while typing in. pub fn goToLine(self: *Buffer, editor: *Editor) !void { if (try editor.prompt(self.allocator, "Goto line")) |line_str| { defer self.allocator.free(line_str); const line = std.fmt.parseUnsigned(usize, line_str, 0) catch |err| { try editor.setStatusMessage("Couldn't parse '{s}' as an integer: {}", .{ line_str, err }); return; }; self.cy = std.math.clamp(line, 1, self.rows.items.len + 1) - 1; if (self.cy == self.rows.items.len) { self.cx = 0; } else { self.cx = @min(self.cx, self.rows.items[self.cy].data.items.len); } } } pub fn indent(self: *Buffer) !void { if (self.config.hard_tabs) { return self.insertChar('\t'); } else { return self.insertNChars(' ', self.config.tab_stop - self.cx % self.config.tab_stop); } } 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.edited = true; } pub fn insertNChars(self: *Buffer, char: u8, count: usize) !void { var remaining = count; while (remaining > 0) : (remaining -= 1) { try self.insertChar(char); } } pub fn insertNewline(self: *Buffer) !void { self.edited = true; if (self.cx == 0) { try self.insertRow(self.cy, ""); self.cy += 1; return; } var row = &self.rows.items[self.cy]; const indentation_size = row.indentationSize(); const indentation = row.data.items[0..indentation_size]; 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.edited = true; } pub fn isDirty(self: Buffer) bool { if (self.short_name.len > 0) { if (self.short_name[0] == '*' and self.short_name[self.short_name.len-1] == '*') { return false; } } return self.edited; } pub fn killLine(self: *Buffer) !void { if (self.cy == self.rows.items.len) { return; } var row = &self.rows.items[self.cy]; if (self.cx == row.data.items.len) { return self.deleteChar(); } else { self.edited = true; try row.data.resize(self.cx); return row.update(self); } } 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, editor: Editor) void { const screenrows = editor.screenrows; 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, editor: Editor) void { const screenrows = editor.screenrows; self.cy = @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, editor: *Editor) !void { // TODO: Currently only recenters try editor.refreshWindowSize(); const screenrows = editor.screenrows; 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.file_path == null) { const fname = (try editor.prompt(self.allocator, "Save as")) orelse { return; }; defer self.allocator.free(fname); const file_path = try es.files.resolvePath(self.allocator, fname); errdefer self.allocator.free(file_path); 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); 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 file_path = self.file_path.?; if (std.fs.path.dirname(file_path)) |dirname| { var res_dir = std.fs.openDirAbsolute(dirname, .{}); if (res_dir) |*dir| { dir.close(); } else |_| { const prompt = try std.fmt.allocPrint( self.allocator, "Create parent directory '{s}'?", .{dirname}, ); defer self.allocator.free(prompt); if (try editor.promptYN(prompt)) { std.fs.cwd().makePath(dirname) catch |err| { try editor.setStatusMessage( "Cannot create parent directory '{s}': {}", .{ dirname, err }, ); }; } else { return; } } } const tmp_path = try std.fmt.allocPrint( self.allocator, "{s}~{}", .{ file_path, std.time.milliTimestamp() }, ); defer self.allocator.free(tmp_path); 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 tmp_file.close(); const mode = statFileMode(file_path) catch |err| { try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{ file_path, err }); return; }; tmp_file.chmod(mode) catch |err| { try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{ tmp_path, err }); return; }; // TODO: chown self.writeToFile(tmp_file) catch |err| { try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{ tmp_path, err }); return; }; 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}'", .{file_path}); self.edited = 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 name = if (self.file_path) |file_path| std.fs.path.basename(file_path) else self.short_name; self.syntax = Syntax.chooseSyntax(name); if (self.syntax == null) { if (std.mem.lastIndexOfScalar(u8, name, '.')) |idx| { self.syntax = Syntax.chooseSyntax(name[idx..]); } } } pub fn unindent(self: *Buffer) !void { if (self.cy == self.rows.items.len) { return; } // Find the end of starting whitespace const row = &self.rows.items[self.cy]; var indentation_size = row.indentationSize(); if (indentation_size == 0) { return; } const indentation_rsize = row.cxToRx(self.config, indentation_size); const desired_rsize = indentation_rsize - 1 - (indentation_rsize - 1) % self.config.tab_stop; const desired_size = row.rxToCx(self.config, desired_rsize); if (self.cx < indentation_size - desired_size) { self.cx = 0; } else { self.cx -= indentation_size - desired_size; } // TODO: ArrayList needs orderedRemoveNItems while (indentation_size > desired_size) : (indentation_size -= 1) { try row.deleteChar(self, desired_size); } } fn printWithLeftPadding( allocator: Allocator, writer: anytype, comptime fmt: []const u8, width: usize, padding: u8, args: anytype, ) !void { const 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 statFileMode(name: []const u8) !File.Mode { const file = std.fs.cwd().openFile(name, .{}) catch |err| switch (err) { error.FileNotFound => return 0o644, else => return err, }; defer file.close(); return (try file.stat()).mode; } 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(); }