diff options
Diffstat (limited to 'src/Buffer.zig')
| -rw-r--r-- | src/Buffer.zig | 518 |
1 files changed, 518 insertions, 0 deletions
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 @@ | |||
| 1 | const es_config = @import("es-config"); | ||
| 2 | const std = @import("std"); | ||
| 3 | |||
| 4 | const Allocator = std.mem.Allocator; | ||
| 5 | const ArrayList = std.ArrayList; | ||
| 6 | const Buffer = @This(); | ||
| 7 | const Config = @import("Config.zig"); | ||
| 8 | const File = std.fs.File; | ||
| 9 | const Editor = @import("Editor.zig"); | ||
| 10 | const Highlight = @import("highlight.zig").Highlight; | ||
| 11 | const Row = @import("Row.zig"); | ||
| 12 | const Syntax = @import("Syntax.zig"); | ||
| 13 | |||
| 14 | allocator: Allocator, | ||
| 15 | |||
| 16 | // TODO: Short name & file name split | ||
| 17 | name: []u8, | ||
| 18 | |||
| 19 | rows: ArrayList(Row), | ||
| 20 | |||
| 21 | cx: usize, | ||
| 22 | cy: usize, | ||
| 23 | rx: usize, | ||
| 24 | |||
| 25 | rowoff: usize, | ||
| 26 | coloff: usize, | ||
| 27 | |||
| 28 | dirty: bool, | ||
| 29 | has_file: bool, | ||
| 30 | |||
| 31 | config: Config, | ||
| 32 | syntax: ?Syntax, | ||
| 33 | |||
| 34 | pub fn init(allocator: Allocator, name: []const u8) !Buffer { | ||
| 35 | var name_owned = try allocator.dupe(u8, name); | ||
| 36 | errdefer allocator.free(name_owned); | ||
| 37 | |||
| 38 | // TODO: buffer-specific config support | ||
| 39 | var config = try Config.readConfig(allocator); | ||
| 40 | |||
| 41 | return Buffer { | ||
| 42 | .allocator = allocator, | ||
| 43 | |||
| 44 | .name = name_owned, | ||
| 45 | |||
| 46 | .rows = ArrayList(Row).init(allocator), | ||
| 47 | |||
| 48 | .cx = 0, | ||
| 49 | .cy = 0, | ||
| 50 | .rx = 0, | ||
| 51 | |||
| 52 | .rowoff = 0, | ||
| 53 | .coloff = 0, | ||
| 54 | |||
| 55 | .dirty = false, | ||
| 56 | .has_file = false, | ||
| 57 | |||
| 58 | .config = config, | ||
| 59 | .syntax = null, | ||
| 60 | }; | ||
| 61 | } | ||
| 62 | |||
| 63 | pub fn deinit(self: Buffer) void { | ||
| 64 | self.allocator.free(self.name); | ||
| 65 | |||
| 66 | for (self.rows.items) |row| { | ||
| 67 | row.deinit(); | ||
| 68 | } | ||
| 69 | self.rows.deinit(); | ||
| 70 | } | ||
| 71 | |||
| 72 | pub fn appendRow(self: *Buffer, data: []const u8) !void { | ||
| 73 | try self.insertRow(self.rows.items.len, data); | ||
| 74 | } | ||
| 75 | |||
| 76 | pub fn backwardChar(self: *Buffer) void { | ||
| 77 | if (self.cx == 0) { | ||
| 78 | if (self.cy == 0) { | ||
| 79 | return; | ||
| 80 | } else { | ||
| 81 | self.cy -= 1; | ||
| 82 | self.cx = self.rows.items[self.cy].data.items.len; | ||
| 83 | } | ||
| 84 | } else { | ||
| 85 | self.cx -= 1; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | |||
| 89 | pub fn backwardDeleteChar(self: *Buffer) !void { | ||
| 90 | self.backwardChar(); | ||
| 91 | return self.deleteChar(); | ||
| 92 | } | ||
| 93 | |||
| 94 | pub fn cleanWhiteSpace(self: *Buffer) !void { | ||
| 95 | for (self.rows.items) |*row| { | ||
| 96 | try row.cleanWhiteSpace(self); | ||
| 97 | } | ||
| 98 | |||
| 99 | if (self.cy == self.rows.items.len) { | ||
| 100 | return; | ||
| 101 | } | ||
| 102 | |||
| 103 | const row = self.rows.items[self.cy]; | ||
| 104 | if (self.cx > row.data.items.len) { | ||
| 105 | self.cx = row.data.items.len; | ||
| 106 | self.rx = row.cxToRx(self.config, self.cx); | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | pub fn deleteChar(self: *Buffer) !void { | ||
| 111 | if (self.cy == self.rows.items.len) { | ||
| 112 | return; | ||
| 113 | } | ||
| 114 | |||
| 115 | if (self.cx < self.rows.items[self.cy].data.items.len) { | ||
| 116 | self.dirty = true; | ||
| 117 | try self.rows.items[self.cy].deleteChar(self, self.cx); | ||
| 118 | } else { | ||
| 119 | if (self.cy == self.rows.items.len - 1) { | ||
| 120 | return; | ||
| 121 | } | ||
| 122 | |||
| 123 | self.dirty = true; | ||
| 124 | try self.rows.items[self.cy].appendString(self, self.rows.items[self.cy + 1].data.items); | ||
| 125 | self.deleteRow(self.cy + 1); | ||
| 126 | } | ||
| 127 | } | ||
| 128 | |||
| 129 | pub fn deleteAllRows(self: *Buffer) void { | ||
| 130 | if (self.rows.items.len == 0) { | ||
| 131 | return; | ||
| 132 | } | ||
| 133 | |||
| 134 | self.dirty = true; | ||
| 135 | while (self.rows.popOrNull()) |row| { | ||
| 136 | row.deinit(); | ||
| 137 | } | ||
| 138 | } | ||
| 139 | |||
| 140 | pub fn deleteRow(self: *Buffer, at: usize) void { | ||
| 141 | self.dirty = true; | ||
| 142 | |||
| 143 | self.rows.orderedRemove(at).deinit(); | ||
| 144 | var i = at; | ||
| 145 | while (i < self.rows.items.len) : (i += 1) { | ||
| 146 | self.rows.items[i].idx -= 1; | ||
| 147 | } | ||
| 148 | } | ||
| 149 | |||
| 150 | pub fn drawRows(self: Buffer, writer: anytype, screenrows: usize, screencols: usize) !void { | ||
| 151 | const line_num_digits = self.lineNumberDigits(); | ||
| 152 | var y: usize = 0; | ||
| 153 | while (y < screenrows) : (y += 1) { | ||
| 154 | const filerow = y + self.rowoff; | ||
| 155 | if (filerow < self.rows.items.len) { | ||
| 156 | const row = self.rows.items[filerow]; | ||
| 157 | |||
| 158 | try writer.writeAll(Highlight.line_no.asString()); | ||
| 159 | try printWithLeftPadding(self.allocator, writer, "{}", line_num_digits - 1, ' ', .{filerow + 1}); | ||
| 160 | try writer.writeByte(' '); | ||
| 161 | |||
| 162 | if (row.rdata.items.len >= self.coloff) { | ||
| 163 | try writer.writeAll("\x1b[m"); | ||
| 164 | var last_hl = Highlight.normal; | ||
| 165 | |||
| 166 | var x: usize = 0; | ||
| 167 | while (x < screencols - line_num_digits) : (x += 1) { | ||
| 168 | const rx = x + self.coloff; | ||
| 169 | if (rx >= row.rdata.items.len) { | ||
| 170 | break; | ||
| 171 | } | ||
| 172 | |||
| 173 | const char = row.rdata.items[rx]; | ||
| 174 | if (std.ascii.isCntrl(char)) { | ||
| 175 | const sym = [_]u8{if (char <= 26) char + '@' else '?'}; | ||
| 176 | try writer.print("\x1b[7m{s}\x1b[m", .{&sym}); | ||
| 177 | last_hl = Highlight.none; | ||
| 178 | } else { | ||
| 179 | const hl = if (rx > self.config.line_limit) Highlight.overlong_line else row.hldata.items[rx]; | ||
| 180 | if (hl != last_hl) { | ||
| 181 | try writer.writeAll(hl.asString()); | ||
| 182 | last_hl = hl; | ||
| 183 | } | ||
| 184 | |||
| 185 | try writer.writeByte(char); | ||
| 186 | } | ||
| 187 | } | ||
| 188 | } | ||
| 189 | } else if (self.rows.items.len == 0 and y == screenrows / 3) { | ||
| 190 | const welcome_data = try std.fmt.allocPrint(self.allocator, "ES -- version {}", .{ es_config.es_version }); | ||
| 191 | defer self.allocator.free(welcome_data); | ||
| 192 | var welcome = welcome_data; | ||
| 193 | if (welcome.len > screencols - 1) { | ||
| 194 | welcome = welcome[0..(screencols - 1)]; | ||
| 195 | } | ||
| 196 | |||
| 197 | const padding = (screencols - welcome.len - 1) / 2; | ||
| 198 | try writer.print("{s}~{s}", .{Highlight.line_no.asString(), Highlight.normal.asString()}); | ||
| 199 | try printWithLeftPadding(self.allocator, writer, "{s}", welcome.len + padding, ' ', .{welcome}); | ||
| 200 | } else { | ||
| 201 | try writer.print("{s}~", .{Highlight.line_no.asString()}); | ||
| 202 | } | ||
| 203 | |||
| 204 | try writer.writeAll("\x1b[K\r\n"); | ||
| 205 | } | ||
| 206 | } | ||
| 207 | |||
| 208 | pub fn drawStatusBar(self: Buffer, writer: anytype, screencols: usize) !void { | ||
| 209 | try writer.writeAll("\x1b[m\x1b[7m"); | ||
| 210 | |||
| 211 | var name = if (self.name.len > 20) | ||
| 212 | try std.fmt.allocPrint(self.allocator, "{s}...", .{self.name[0..17]}) | ||
| 213 | else | ||
| 214 | try self.allocator.dupe(u8, self.name); | ||
| 215 | defer self.allocator.free(name); | ||
| 216 | |||
| 217 | const modified = if (self.dirty) | ||
| 218 | @as([]const u8, " (modified)") | ||
| 219 | else | ||
| 220 | @as([]const u8, ""); | ||
| 221 | |||
| 222 | var lbuf = try std.fmt.allocPrint(self.allocator, "{s}{s}", .{name, modified}); | ||
| 223 | defer self.allocator.free(lbuf); | ||
| 224 | |||
| 225 | var rbuf = try std.fmt.allocPrint(self.allocator, " {s} | {}/{}", .{ | ||
| 226 | if (self.syntax) |syntax| syntax.name else "Fundamental", | ||
| 227 | self.cy + 1, | ||
| 228 | self.rows.items.len, | ||
| 229 | }); | ||
| 230 | defer self.allocator.free(rbuf); | ||
| 231 | |||
| 232 | var rlen = if (rbuf.len > screencols) screencols else rbuf.len; | ||
| 233 | var llen = if (lbuf.len > screencols - rlen) screencols - rlen else lbuf.len; | ||
| 234 | |||
| 235 | try writer.writeAll(lbuf[0..llen]); | ||
| 236 | try writer.writeByteNTimes(' ', screencols - llen - rlen); | ||
| 237 | try writer.writeAll(rbuf[0..rlen]); | ||
| 238 | try writer.writeAll("\x1b[m\r\n"); | ||
| 239 | } | ||
| 240 | |||
| 241 | pub fn forwardChar(self: *Buffer) void { | ||
| 242 | if (self.rows.items.len == self.cy) { | ||
| 243 | return; | ||
| 244 | } | ||
| 245 | |||
| 246 | if (self.cx == self.rows.items[self.cy].data.items.len) { | ||
| 247 | self.cx = 0; | ||
| 248 | self.cy += 1; | ||
| 249 | } else { | ||
| 250 | self.cx += 1; | ||
| 251 | } | ||
| 252 | } | ||
| 253 | |||
| 254 | pub fn insertChar(self: *Buffer, char: u8) !void { | ||
| 255 | if (self.cy == self.rows.items.len) { | ||
| 256 | try self.insertRow(self.rows.items.len, ""); | ||
| 257 | } | ||
| 258 | |||
| 259 | try self.rows.items[self.cy].insertChar(self, self.cx, char); | ||
| 260 | self.cx += 1; | ||
| 261 | |||
| 262 | self.dirty = true; | ||
| 263 | } | ||
| 264 | |||
| 265 | pub fn insertNewline(self: *Buffer) !void { | ||
| 266 | self.dirty = true; | ||
| 267 | |||
| 268 | if (self.cx == 0) { | ||
| 269 | try self.insertRow(self.cy, ""); | ||
| 270 | self.cy += 1; | ||
| 271 | return; | ||
| 272 | } | ||
| 273 | |||
| 274 | var row = &self.rows.items[self.cy]; | ||
| 275 | |||
| 276 | const indentation = try row.indentation(self.allocator); | ||
| 277 | defer self.allocator.free(indentation); | ||
| 278 | |||
| 279 | try self.insertRow(self.cy + 1, indentation); | ||
| 280 | row = &self.rows.items[self.cy]; | ||
| 281 | |||
| 282 | try self.rows.items[self.cy + 1].appendString(self, row.data.items[self.cx..]); | ||
| 283 | try row.data.resize(self.cx); | ||
| 284 | |||
| 285 | try row.update(self); | ||
| 286 | |||
| 287 | self.cx = indentation.len; | ||
| 288 | self.cy += 1; | ||
| 289 | } | ||
| 290 | |||
| 291 | pub fn insertRow(self: *Buffer, at: usize, data: []const u8) !void { | ||
| 292 | var row = try Row.init(self.allocator, at, data); | ||
| 293 | errdefer row.deinit(); | ||
| 294 | |||
| 295 | try self.rows.insert(at, row); | ||
| 296 | var i: usize = at + 1; | ||
| 297 | while (i < self.rows.items.len) : (i += 1) { | ||
| 298 | self.rows.items[i].idx += 1; | ||
| 299 | } | ||
| 300 | try self.rows.items[at].update(self); | ||
| 301 | |||
| 302 | self.dirty = true; | ||
| 303 | } | ||
| 304 | |||
| 305 | pub fn killLine(self: *Buffer) !void { | ||
| 306 | return self.deleteRow(self.cy); | ||
| 307 | } | ||
| 308 | |||
| 309 | pub fn lineNumberDigits(self: Buffer) usize { | ||
| 310 | if (self.rows.items.len == 0) { | ||
| 311 | return 2; | ||
| 312 | } | ||
| 313 | return 2 + std.math.log10(self.rows.items.len); | ||
| 314 | } | ||
| 315 | |||
| 316 | pub fn moveBeginningOfLine(self: *Buffer) void { | ||
| 317 | self.cx = 0; | ||
| 318 | } | ||
| 319 | |||
| 320 | pub fn moveEndOfLine(self: *Buffer) void { | ||
| 321 | if (self.rows.items.len == self.cy) { | ||
| 322 | self.cx = 0; | ||
| 323 | } else { | ||
| 324 | self.cx = self.rows.items[self.cy].data.items.len; | ||
| 325 | } | ||
| 326 | } | ||
| 327 | |||
| 328 | pub fn nextLine(self: *Buffer) void { | ||
| 329 | if (self.rows.items.len == self.cy) { | ||
| 330 | return; | ||
| 331 | } | ||
| 332 | |||
| 333 | self.cy += 1; | ||
| 334 | |||
| 335 | if (self.rows.items.len == self.cy) { | ||
| 336 | self.cx = 0; | ||
| 337 | } else { | ||
| 338 | self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx); | ||
| 339 | } | ||
| 340 | } | ||
| 341 | |||
| 342 | pub fn pageDown(self: *Buffer, screenrows: usize) void { | ||
| 343 | self.cy = std.math.clamp( | ||
| 344 | self.rowoff + screenrows - 1 - self.config.page_overlap, | ||
| 345 | 0, | ||
| 346 | self.rows.items.len, | ||
| 347 | ); | ||
| 348 | var i: usize = 0; | ||
| 349 | while (i < screenrows) : (i += 1) { | ||
| 350 | self.nextLine(); | ||
| 351 | } | ||
| 352 | } | ||
| 353 | |||
| 354 | pub fn pageUp(self: *Buffer, screenrows: usize) void { | ||
| 355 | self.cy = std.math.min(self.rows.items.len, self.rowoff + self.config.page_overlap); | ||
| 356 | var i: usize = 0; | ||
| 357 | while (i < screenrows) : (i += 1) { | ||
| 358 | self.previousLine(); | ||
| 359 | } | ||
| 360 | } | ||
| 361 | |||
| 362 | pub fn previousLine(self: *Buffer) void { | ||
| 363 | if (self.cy == 0) { | ||
| 364 | return; | ||
| 365 | } | ||
| 366 | |||
| 367 | self.cy -= 1; | ||
| 368 | self.cx = self.rows.items[self.cy].rxToCx(self.config, self.rx); | ||
| 369 | } | ||
| 370 | |||
| 371 | pub fn recenterTopBottom(self: *Buffer, screenrows: usize) void { | ||
| 372 | // TODO: Currently only recenters | ||
| 373 | if (self.cy >= screenrows / 2) { | ||
| 374 | self.rowoff = self.cy - screenrows / 2; | ||
| 375 | } else { | ||
| 376 | self.rowoff = 0; | ||
| 377 | } | ||
| 378 | } | ||
| 379 | |||
| 380 | pub fn save(self: *Buffer, editor: *Editor) !void { | ||
| 381 | if (!self.has_file) { | ||
| 382 | const fname = try editor.prompt("Save as"); | ||
| 383 | if (fname == null) { | ||
| 384 | return; | ||
| 385 | } | ||
| 386 | |||
| 387 | self.allocator.free(self.name); | ||
| 388 | self.name = fname.?; | ||
| 389 | self.has_file = true; | ||
| 390 | |||
| 391 | try self.selectSyntaxHighlighting(); | ||
| 392 | } | ||
| 393 | |||
| 394 | // TODO: Add a config value for this | ||
| 395 | try self.cleanWhiteSpace(); | ||
| 396 | |||
| 397 | const tmpname = try std.fmt.allocPrint(self.allocator, "{s}~{}", .{self.name, std.time.milliTimestamp()}); | ||
| 398 | defer self.allocator.free(tmpname); | ||
| 399 | |||
| 400 | const tmpfile = std.fs.cwd().createFile(tmpname, .{.truncate = true}) catch |err| { | ||
| 401 | try editor.setStatusMessage("Cannot open tempfile '{s}' for writing: {}", .{tmpname, err}); | ||
| 402 | return; | ||
| 403 | }; | ||
| 404 | defer tmpfile.close(); | ||
| 405 | |||
| 406 | const stat = statFile(self.name) catch |err| { | ||
| 407 | try editor.setStatusMessage("Couldn't stat file '{s}': {}", .{self.name, err}); | ||
| 408 | return; | ||
| 409 | }; | ||
| 410 | |||
| 411 | tmpfile.chmod(stat.mode) catch |err| { | ||
| 412 | try editor.setStatusMessage("Couldn't chmod tempfile '{s}': {}", .{tmpname, err}); | ||
| 413 | return; | ||
| 414 | }; | ||
| 415 | |||
| 416 | // TODO: chown | ||
| 417 | |||
| 418 | self.writeToFile(tmpfile) catch |err| { | ||
| 419 | try editor.setStatusMessage("Couldn't write to tempfile '{s}': {}", .{tmpname, err}); | ||
| 420 | return; | ||
| 421 | }; | ||
| 422 | |||
| 423 | std.fs.cwd().rename(tmpname, self.name) catch |err| { | ||
| 424 | try editor.setStatusMessage("Couldn't move '{s}' to '{s}': {}", .{tmpname, self.name, err}); | ||
| 425 | return; | ||
| 426 | }; | ||
| 427 | |||
| 428 | try editor.setStatusMessage("Saved to '{s}'", .{self.name}); | ||
| 429 | self.dirty = false; | ||
| 430 | } | ||
| 431 | |||
| 432 | pub fn scroll(self: *Buffer, screenrows: usize, screencols: usize) void { | ||
| 433 | self.rx = 0; | ||
| 434 | if (self.cy < self.rows.items.len) { | ||
| 435 | self.rx = self.rows.items[self.cy].cxToRx(self.config, self.cx); | ||
| 436 | } | ||
| 437 | |||
| 438 | if (self.cy < self.rowoff) { | ||
| 439 | self.rowoff = self.cy; | ||
| 440 | } else if (self.cy >= self.rowoff + screenrows) { | ||
| 441 | self.rowoff = self.cy + 1 - screenrows; | ||
| 442 | } | ||
| 443 | |||
| 444 | if (self.rx < self.coloff) { | ||
| 445 | self.coloff = self.rx; | ||
| 446 | } else if (self.rx + self.lineNumberDigits() >= self.coloff + screencols) { | ||
| 447 | self.coloff = self.lineNumberDigits() + self.rx + 1 - screencols; | ||
| 448 | } | ||
| 449 | } | ||
| 450 | |||
| 451 | pub fn selectSyntaxHighlighting(self: *Buffer) !void { | ||
| 452 | self.syntax = null; | ||
| 453 | |||
| 454 | const ext = if (std.mem.lastIndexOfScalar(u8, self.name, '.')) |idx| self.name[idx..] else null; | ||
| 455 | for (Syntax.database) |syntax| { | ||
| 456 | for (syntax.filematch) |filematch| { | ||
| 457 | const is_ext = filematch[0] == '.'; | ||
| 458 | if ((is_ext and ext != null and std.mem.eql(u8, ext.?, filematch)) | ||
| 459 | or (!is_ext and std.mem.eql(u8, self.name, filematch)) | ||
| 460 | ) { | ||
| 461 | self.syntax = syntax; | ||
| 462 | |||
| 463 | for (self.rows.items) |*row| { | ||
| 464 | try row.updateSyntax(self); | ||
| 465 | } | ||
| 466 | |||
| 467 | return; | ||
| 468 | } | ||
| 469 | } | ||
| 470 | } | ||
| 471 | } | ||
| 472 | |||
| 473 | fn printWithLeftPadding( | ||
| 474 | allocator: Allocator, | ||
| 475 | writer: anytype, | ||
| 476 | comptime fmt: []const u8, | ||
| 477 | width: usize, | ||
| 478 | padding: u8, | ||
| 479 | args: anytype, | ||
| 480 | ) !void { | ||
| 481 | var unpadded = try std.fmt.allocPrint(allocator, fmt, args); | ||
| 482 | defer allocator.free(unpadded); | ||
| 483 | |||
| 484 | std.debug.assert(unpadded.len <= width); | ||
| 485 | |||
| 486 | try writer.writeByteNTimes(padding, width - unpadded.len); | ||
| 487 | try writer.writeAll(unpadded); | ||
| 488 | } | ||
| 489 | |||
| 490 | fn statFile(name: []const u8) !File.Stat { | ||
| 491 | const file = std.fs.cwd().openFile(name, .{}) catch |err| switch (err) { | ||
| 492 | error.FileNotFound => return File.Stat{ | ||
| 493 | .inode = 0, | ||
| 494 | .size = 0, | ||
| 495 | .mode = 0o644, | ||
| 496 | .kind = .File, | ||
| 497 | .atime = 0, | ||
| 498 | .mtime = 0, | ||
| 499 | .ctime = 0, | ||
| 500 | }, | ||
| 501 | else => return err, | ||
| 502 | }; | ||
| 503 | defer file.close(); | ||
| 504 | |||
| 505 | return file.stat(); | ||
| 506 | } | ||
| 507 | |||
| 508 | fn writeToFile(self: Buffer, file: File) !void { | ||
| 509 | var buffered_writer = std.io.bufferedWriter(file.writer()); | ||
| 510 | |||
| 511 | const writer = buffered_writer.writer(); | ||
| 512 | for (self.rows.items) |row| { | ||
| 513 | try writer.writeAll(row.data.items); | ||
| 514 | try writer.writeByte('\n'); | ||
| 515 | } | ||
| 516 | |||
| 517 | try buffered_writer.flush(); | ||
| 518 | } | ||