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 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 518 insertions(+) create mode 100644 src/Buffer.zig (limited to 'src/Buffer.zig') 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(); +} -- cgit v1.2.3