diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Buffer.zig | 518 | ||||
| -rw-r--r-- | src/Config.zig | 78 | ||||
| -rw-r--r-- | src/Editor.zig | 461 | ||||
| -rw-r--r-- | src/RawMode.zig | 38 | ||||
| -rw-r--r-- | src/Row.zig | 273 | ||||
| -rw-r--r-- | src/StringBuilder.zig | 45 | ||||
| -rw-r--r-- | src/Syntax.zig | 28 | ||||
| -rw-r--r-- | src/Syntax/makefile.zig | 65 | ||||
| -rw-r--r-- | src/Syntax/zig.zig | 43 | ||||
| -rw-r--r-- | src/highlight.zig | 34 | ||||
| -rw-r--r-- | src/key.zig | 102 | ||||
| -rw-r--r-- | src/key_state.zig | 96 | ||||
| -rw-r--r-- | src/main.zig | 40 | ||||
| -rw-r--r-- | src/search.zig | 94 |
14 files changed, 1915 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 | } | ||
diff --git a/src/Config.zig b/src/Config.zig new file mode 100644 index 0000000..b8b45a5 --- /dev/null +++ b/src/Config.zig | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | // TODO: Change this to proper TOML in the future :) | ||
| 2 | |||
| 3 | const std = @import("std"); | ||
| 4 | |||
| 5 | const Allocator = std.mem.Allocator; | ||
| 6 | const BufMap = std.BufMap; | ||
| 7 | const Config = @This(); | ||
| 8 | |||
| 9 | const config_path = "arkta/es/es.ini"; | ||
| 10 | |||
| 11 | line_limit: usize = 100, | ||
| 12 | page_overlap: usize = 2, | ||
| 13 | tab_stop: usize = 8, | ||
| 14 | |||
| 15 | pub fn readConfig(allocator: Allocator) !Config { | ||
| 16 | var config = Config {}; | ||
| 17 | |||
| 18 | var env_map = try std.process.getEnvMap(allocator); | ||
| 19 | defer env_map.deinit(); | ||
| 20 | |||
| 21 | if (env_map.get("XDG_CONFIG_DIRS")) |base_dirs| { | ||
| 22 | var it = std.mem.split(u8, base_dirs, ":"); | ||
| 23 | while (it.next()) |base_dir| { | ||
| 24 | try readConfigInBaseDir(allocator, &config, base_dir); | ||
| 25 | } | ||
| 26 | } else { | ||
| 27 | try readConfigInBaseDir(allocator, &config, "/etc/xdg"); | ||
| 28 | } | ||
| 29 | |||
| 30 | if (env_map.get("XDG_CONFIG_HOME")) |base_dir| { | ||
| 31 | try readConfigInBaseDir(allocator, &config, base_dir); | ||
| 32 | } else { | ||
| 33 | // TODO: Maybe return an error instead of .? | ||
| 34 | const home = env_map.get("HOME").?; | ||
| 35 | const home_config = try std.fs.path.join(allocator, &.{home, ".config"}); | ||
| 36 | defer allocator.free(home_config); | ||
| 37 | |||
| 38 | try readConfigInBaseDir(allocator, &config, home_config); | ||
| 39 | } | ||
| 40 | |||
| 41 | return config; | ||
| 42 | } | ||
| 43 | |||
| 44 | fn readConfigInBaseDir(allocator: Allocator, config: *Config, base_dir: []const u8) !void { | ||
| 45 | const filename = try std.fs.path.join(allocator, &.{base_dir, config_path}); | ||
| 46 | defer allocator.free(filename); | ||
| 47 | |||
| 48 | const file = std.fs.openFileAbsolute(filename, .{ .read = true }) catch |err| switch (err) { | ||
| 49 | error.FileNotFound => return, | ||
| 50 | else => return err, | ||
| 51 | }; | ||
| 52 | defer file.close(); | ||
| 53 | |||
| 54 | const reader = file.reader(); | ||
| 55 | while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |line_buf| { | ||
| 56 | defer allocator.free(line_buf); | ||
| 57 | |||
| 58 | var line = std.mem.trim(u8, line_buf, &std.ascii.spaces); | ||
| 59 | if (line.len == 0 or line[0] == '#') { | ||
| 60 | continue; | ||
| 61 | } | ||
| 62 | |||
| 63 | const split = if (std.mem.indexOfScalar(u8, line, '=')) |idx| idx else { | ||
| 64 | return error.MalformedConfig; | ||
| 65 | }; | ||
| 66 | |||
| 67 | const key = std.mem.trimRight(u8, line[0..split], &std.ascii.spaces); | ||
| 68 | const value = std.mem.trimLeft(u8, line[(split + 1)..], &std.ascii.spaces); | ||
| 69 | if (std.mem.eql(u8, key, "line-limit")) { | ||
| 70 | config.line_limit = try std.fmt.parseUnsigned(usize, value, 0); | ||
| 71 | } else if (std.mem.eql(u8, key, "page-overlap")) { | ||
| 72 | config.page_overlap = try std.fmt.parseUnsigned(usize, value, 0); | ||
| 73 | } else if (std.mem.eql(u8, key, "line-limit")) { | ||
| 74 | config.line_limit = try std.fmt.parseUnsigned(usize, value, 0); | ||
| 75 | } | ||
| 76 | // TODO: else ?? | ||
| 77 | } | ||
| 78 | } | ||
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 | } | ||
diff --git a/src/RawMode.zig b/src/RawMode.zig new file mode 100644 index 0000000..ed71819 --- /dev/null +++ b/src/RawMode.zig | |||
| @@ -0,0 +1,38 @@ | |||
| 1 | const linux = std.os.linux; | ||
| 2 | const std = @import("std"); | ||
| 3 | |||
| 4 | const STDIN_FILENO = std.os.STDIN_FILENO; | ||
| 5 | const RawMode = @This(); | ||
| 6 | const tcflag_t = linux.tcflag_t; | ||
| 7 | const tcgetattr = std.os.tcgetattr; | ||
| 8 | const tcsetattr = std.os.tcsetattr; | ||
| 9 | const termios = std.os.termios; | ||
| 10 | |||
| 11 | orig: termios, | ||
| 12 | |||
| 13 | pub fn init() !RawMode { | ||
| 14 | const orig = try tcgetattr(STDIN_FILENO); | ||
| 15 | const self = RawMode{ .orig = orig }; | ||
| 16 | errdefer self.deinit(); | ||
| 17 | |||
| 18 | var raw = orig; | ||
| 19 | |||
| 20 | raw.iflag &= ~@as(tcflag_t, linux.BRKINT | linux.ICRNL | linux.INPCK | linux.ISTRIP | linux.IXON); | ||
| 21 | raw.lflag &= ~@as(tcflag_t, linux.ECHO | linux.ICANON | linux.IEXTEN | linux.ISIG); | ||
| 22 | raw.oflag &= ~@as(tcflag_t, linux.OPOST); | ||
| 23 | |||
| 24 | raw.cflag |= linux.CS8; | ||
| 25 | |||
| 26 | raw.cc[linux.V.MIN] = 0; | ||
| 27 | raw.cc[linux.V.TIME] = 1; | ||
| 28 | |||
| 29 | try tcsetattr(STDIN_FILENO, .FLUSH, raw); | ||
| 30 | |||
| 31 | return self; | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn deinit(self: RawMode) void { | ||
| 35 | tcsetattr(STDIN_FILENO, .FLUSH, self.orig) catch |err| { | ||
| 36 | std.log.err("Failed to reset termios: {}", .{err}); | ||
| 37 | }; | ||
| 38 | } | ||
diff --git a/src/Row.zig b/src/Row.zig new file mode 100644 index 0000000..633e338 --- /dev/null +++ b/src/Row.zig | |||
| @@ -0,0 +1,273 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | const ArrayList = std.ArrayList; | ||
| 5 | const Buffer = @import("Buffer.zig"); | ||
| 6 | const Config = @import("Config.zig"); | ||
| 7 | const Highlight = @import("highlight.zig").Highlight; | ||
| 8 | const Row = @This(); | ||
| 9 | const StringBuilder = @import("StringBuilder.zig"); | ||
| 10 | |||
| 11 | allocator: Allocator, | ||
| 12 | |||
| 13 | idx: usize, | ||
| 14 | |||
| 15 | data: ArrayList(u8), | ||
| 16 | rdata: ArrayList(u8), | ||
| 17 | hldata: ArrayList(Highlight), | ||
| 18 | |||
| 19 | ends_with_open_comment: bool, | ||
| 20 | |||
| 21 | pub fn init(allocator: Allocator, idx: usize, data: []const u8) !Row { | ||
| 22 | var self = Row{ | ||
| 23 | .allocator = allocator, | ||
| 24 | |||
| 25 | .idx = idx, | ||
| 26 | |||
| 27 | .data = ArrayList(u8).init(allocator), | ||
| 28 | .rdata = ArrayList(u8).init(allocator), | ||
| 29 | .hldata = ArrayList(Highlight).init(allocator), | ||
| 30 | |||
| 31 | .ends_with_open_comment = false, | ||
| 32 | }; | ||
| 33 | errdefer self.deinit(); | ||
| 34 | |||
| 35 | try self.data.appendSlice(data); | ||
| 36 | return self; | ||
| 37 | } | ||
| 38 | |||
| 39 | pub fn deinit(self: Row) void { | ||
| 40 | self.data.deinit(); | ||
| 41 | self.rdata.deinit(); | ||
| 42 | self.hldata.deinit(); | ||
| 43 | } | ||
| 44 | |||
| 45 | pub fn appendString(self: *Row, buf: *Buffer, str: []const u8) !void { | ||
| 46 | try self.data.appendSlice(str); | ||
| 47 | return self.update(buf); | ||
| 48 | } | ||
| 49 | |||
| 50 | pub fn cleanWhiteSpace(self: *Row, buf: *Buffer) !void { | ||
| 51 | const orig_len = self.data.items.len; | ||
| 52 | while (self.data.items.len > 0) { | ||
| 53 | if (std.ascii.isBlank(self.data.items[self.data.items.len - 1])) { | ||
| 54 | _ = self.data.pop(); | ||
| 55 | } else { | ||
| 56 | break; | ||
| 57 | } | ||
| 58 | } | ||
| 59 | |||
| 60 | if (orig_len != self.data.items.len) { | ||
| 61 | buf.dirty = true; | ||
| 62 | try self.update(buf); | ||
| 63 | } | ||
| 64 | } | ||
| 65 | |||
| 66 | pub fn cxToRx(self: Row, config: Config, cx: usize) usize { | ||
| 67 | var rx: usize = 0; | ||
| 68 | var i: usize = 0; | ||
| 69 | while (i < cx) : (i += 1) { | ||
| 70 | if (self.data.items[i] == '\t') { | ||
| 71 | rx += config.tab_stop - (rx % config.tab_stop); | ||
| 72 | } else { | ||
| 73 | rx += 1; | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | return rx; | ||
| 78 | } | ||
| 79 | |||
| 80 | pub fn deleteChar(self: *Row, buf: *Buffer, at: usize) !void { | ||
| 81 | _ = self.data.orderedRemove(at); | ||
| 82 | try self.update(buf); | ||
| 83 | } | ||
| 84 | |||
| 85 | pub fn indentation(self: Row, allocator: Allocator) ![]u8 { | ||
| 86 | var str = ArrayList(u8).init(allocator); | ||
| 87 | defer str.deinit(); | ||
| 88 | |||
| 89 | var idx: usize = 0; | ||
| 90 | while (idx < self.data.items.len) : (idx += 1) { | ||
| 91 | if (!std.ascii.isBlank(self.data.items[idx])) { | ||
| 92 | break; | ||
| 93 | } | ||
| 94 | } | ||
| 95 | |||
| 96 | try str.appendSlice(self.data.items[0..idx]); | ||
| 97 | return str.toOwnedSlice(); | ||
| 98 | } | ||
| 99 | |||
| 100 | pub fn insertChar(self: *Row, buf: *Buffer, at: usize, char: u8) !void { | ||
| 101 | try self.data.insert(at, char); | ||
| 102 | try self.update(buf); | ||
| 103 | } | ||
| 104 | |||
| 105 | pub fn rxToCx(self: Row, config: Config, rx: usize) usize { | ||
| 106 | if (rx == 0) { | ||
| 107 | return 0; | ||
| 108 | } | ||
| 109 | |||
| 110 | var cur_rx: usize = 0; | ||
| 111 | for (self.data.items) |char, cx| { | ||
| 112 | if (char == '\t') { | ||
| 113 | cur_rx += config.tab_stop - (cur_rx % config.tab_stop); | ||
| 114 | } else { | ||
| 115 | cur_rx += 1; | ||
| 116 | } | ||
| 117 | |||
| 118 | if (cur_rx >= rx) { | ||
| 119 | return cx + 1; | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | return self.data.items.len; | ||
| 124 | } | ||
| 125 | |||
| 126 | // TODO: I don't like that this is modifying both row and buffer (parent of row) | ||
| 127 | pub fn update(self: *Row, buf: *Buffer) !void { | ||
| 128 | self.rdata.clearRetainingCapacity(); | ||
| 129 | |||
| 130 | for (self.data.items) |char| { | ||
| 131 | if (char == '\t') { | ||
| 132 | const len = buf.config.tab_stop - self.rdata.items.len % buf.config.tab_stop; | ||
| 133 | try self.rdata.appendNTimes(' ', len); | ||
| 134 | } else { | ||
| 135 | try self.rdata.append(char); | ||
| 136 | } | ||
| 137 | } | ||
| 138 | |||
| 139 | try self.updateSyntax(buf); | ||
| 140 | } | ||
| 141 | |||
| 142 | const UpdateSyntaxError = std.mem.Allocator.Error; | ||
| 143 | pub fn updateSyntax(self: *Row, buf: *Buffer) UpdateSyntaxError!void { | ||
| 144 | try self.hldata.resize(self.rdata.items.len); | ||
| 145 | std.mem.set(Highlight, self.hldata.items, .normal); | ||
| 146 | if (buf.syntax == null) { | ||
| 147 | return; | ||
| 148 | } | ||
| 149 | |||
| 150 | const syntax = buf.syntax.?; | ||
| 151 | const kw1 = syntax.keywords1; | ||
| 152 | const kw2 = syntax.keywords2; | ||
| 153 | |||
| 154 | var after_sep = true; | ||
| 155 | var in_comment = if (self.idx > 0) buf.rows.items[self.idx - 1].ends_with_open_comment else false; | ||
| 156 | var curr_str_quote: ?u8 = null; | ||
| 157 | |||
| 158 | var i: usize = 0; | ||
| 159 | main_loop: while (i < self.rdata.items.len) { | ||
| 160 | const prev_hl = if (i > 0) self.hldata.items[i - 1] else .normal; | ||
| 161 | |||
| 162 | if (syntax.singleline_comment_start) |scs| { | ||
| 163 | std.debug.assert(scs.len != 0); | ||
| 164 | if (curr_str_quote == null and !in_comment and scs.len + i <= self.rdata.items.len) { | ||
| 165 | if (std.mem.eql(u8, scs, self.rdata.items[i .. (i + scs.len)])) { | ||
| 166 | std.mem.set(Highlight, self.hldata.items[i..], .comment); | ||
| 167 | break; | ||
| 168 | } | ||
| 169 | } | ||
| 170 | } | ||
| 171 | |||
| 172 | if (syntax.multiline_comment_start) |mcs| { | ||
| 173 | std.debug.assert(mcs.len != 0); | ||
| 174 | if (syntax.multiline_comment_end) |mce| { | ||
| 175 | std.debug.assert(mce.len != 0); | ||
| 176 | if (curr_str_quote == null) { | ||
| 177 | if (in_comment) { | ||
| 178 | self.hldata.items[i] = .comment_ml; | ||
| 179 | if (mce.len + i <= self.rdata.items.len | ||
| 180 | and std.mem.eql(u8, mce, self.rdata.items[i .. (i + mce.len)]) | ||
| 181 | ) { | ||
| 182 | std.mem.set(Highlight, self.hldata.items[i .. (i + mce.len)], .comment_ml); | ||
| 183 | i += mce.len; | ||
| 184 | in_comment = false; | ||
| 185 | after_sep = true; | ||
| 186 | } else { | ||
| 187 | i += 1; | ||
| 188 | continue; | ||
| 189 | } | ||
| 190 | } else if (mcs.len + i <= self.rdata.items.len | ||
| 191 | and std.mem.eql(u8, mcs, self.rdata.items[i .. (i + mcs.len)])) { | ||
| 192 | std.mem.set(Highlight, self.hldata.items[i .. (i + mcs.len)], .comment_ml); | ||
| 193 | i += mcs.len; | ||
| 194 | in_comment = true; | ||
| 195 | continue; | ||
| 196 | } | ||
| 197 | } | ||
| 198 | |||
| 199 | } | ||
| 200 | } | ||
| 201 | |||
| 202 | const c = self.rdata.items[i]; | ||
| 203 | |||
| 204 | if (syntax.flags.hl_strings) { | ||
| 205 | if (curr_str_quote) |quote| { | ||
| 206 | self.hldata.items[i] = .string; | ||
| 207 | i += 1; | ||
| 208 | // Pretty dumb way of detecting \" or \' but it works \shrug/ | ||
| 209 | if (c == '\\' and i < self.rdata.items.len) { | ||
| 210 | self.hldata.items[i] = .string; | ||
| 211 | i += 1; | ||
| 212 | } else if (c == quote) { | ||
| 213 | curr_str_quote = null; | ||
| 214 | after_sep = true; | ||
| 215 | } | ||
| 216 | |||
| 217 | continue; | ||
| 218 | } else { | ||
| 219 | // TODO: Move this to syntax struct | ||
| 220 | if (c == '"' or c == '\'') { | ||
| 221 | curr_str_quote = c; | ||
| 222 | self.hldata.items[i] = .string; | ||
| 223 | i += 1; | ||
| 224 | continue; | ||
| 225 | } | ||
| 226 | } | ||
| 227 | } | ||
| 228 | |||
| 229 | if (syntax.flags.hl_numbers) { | ||
| 230 | if ((std.ascii.isDigit(c) and (after_sep or prev_hl == .number)) | ||
| 231 | or (c == '.' and prev_hl == .number)) { | ||
| 232 | after_sep = false; | ||
| 233 | self.hldata.items[i] = .number; | ||
| 234 | i += 1; | ||
| 235 | continue; | ||
| 236 | } | ||
| 237 | } | ||
| 238 | |||
| 239 | if (after_sep) { | ||
| 240 | for (kw1) |kw| { | ||
| 241 | if (i + kw.len <= self.rdata.items.len | ||
| 242 | and std.mem.eql(u8, kw, self.rdata.items[i .. (i + kw.len)]) | ||
| 243 | and (i + kw.len == self.rdata.items.len or syntax.isSeparator(self.rdata.items[i + kw.len]))) { | ||
| 244 | std.mem.set(Highlight, self.hldata.items[i .. (i + kw.len)], .keyword1); | ||
| 245 | i += kw.len; | ||
| 246 | after_sep = false; | ||
| 247 | continue :main_loop; | ||
| 248 | } | ||
| 249 | } | ||
| 250 | |||
| 251 | for (kw2) |kw| { | ||
| 252 | if (i + kw.len <= self.rdata.items.len | ||
| 253 | and std.mem.eql(u8, kw, self.rdata.items[i .. (i + kw.len)]) | ||
| 254 | and (i + kw.len == self.rdata.items.len or syntax.isSeparator(self.rdata.items[i + kw.len]))) { | ||
| 255 | std.mem.set(Highlight, self.hldata.items[i .. (i + kw.len)], .keyword2); | ||
| 256 | i += kw.len; | ||
| 257 | after_sep = false; | ||
| 258 | continue :main_loop; | ||
| 259 | } | ||
| 260 | } | ||
| 261 | } | ||
| 262 | |||
| 263 | after_sep = syntax.isSeparator(c); | ||
| 264 | i += 1; | ||
| 265 | } | ||
| 266 | |||
| 267 | if (in_comment != self.ends_with_open_comment) { | ||
| 268 | self.ends_with_open_comment = in_comment; | ||
| 269 | if (self.idx + 1 < buf.rows.items.len) { | ||
| 270 | try buf.rows.items[self.idx + 1].updateSyntax(buf); | ||
| 271 | } | ||
| 272 | } | ||
| 273 | } | ||
diff --git a/src/StringBuilder.zig b/src/StringBuilder.zig new file mode 100644 index 0000000..632b1b3 --- /dev/null +++ b/src/StringBuilder.zig | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | const ArrayList = std.ArrayList; | ||
| 5 | const StringBuilder = @This(); | ||
| 6 | |||
| 7 | pub const Error = std.mem.Allocator.Error; | ||
| 8 | pub const Writer = std.io.Writer(*StringBuilder, Error, writeFn); | ||
| 9 | |||
| 10 | data: ArrayList(u8), | ||
| 11 | |||
| 12 | pub fn init(allocator: std.mem.Allocator) StringBuilder { | ||
| 13 | return .{ | ||
| 14 | .data = ArrayList(u8).init(allocator), | ||
| 15 | }; | ||
| 16 | } | ||
| 17 | |||
| 18 | pub fn deinit(self: StringBuilder) void { | ||
| 19 | self.data.deinit(); | ||
| 20 | } | ||
| 21 | |||
| 22 | pub fn append(self: *StringBuilder, char: u8) Error!void { | ||
| 23 | return self.data.append(char); | ||
| 24 | } | ||
| 25 | |||
| 26 | pub fn appendMany(self: *StringBuilder, chars: []const u8) Error!void { | ||
| 27 | return self.data.appendSlice(chars); | ||
| 28 | } | ||
| 29 | |||
| 30 | pub fn seeSlice(self: *StringBuilder) []const u8 { | ||
| 31 | return self.data.items; | ||
| 32 | } | ||
| 33 | |||
| 34 | pub fn toOwnedSlice(self: StringBuilder) []u8 { | ||
| 35 | return self.data.toOwnedSlice(); | ||
| 36 | } | ||
| 37 | |||
| 38 | pub fn writer(self: *StringBuilder) Writer { | ||
| 39 | return Writer { .context = self }; | ||
| 40 | } | ||
| 41 | |||
| 42 | fn writeFn(self: *StringBuilder, chars: []const u8) Error!usize { | ||
| 43 | try self.appendMany(chars); | ||
| 44 | return chars.len; | ||
| 45 | } | ||
diff --git a/src/Syntax.zig b/src/Syntax.zig new file mode 100644 index 0000000..ce3f432 --- /dev/null +++ b/src/Syntax.zig | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | pub const makefile = @import("Syntax/makefile.zig"); | ||
| 2 | pub const zig = @import("Syntax/zig.zig"); | ||
| 3 | |||
| 4 | const std = @import("std"); | ||
| 5 | |||
| 6 | const Syntax = @This(); | ||
| 7 | |||
| 8 | pub const database = [_]Syntax{makefile.syntax, zig.syntax}; | ||
| 9 | |||
| 10 | pub const Flags = struct { | ||
| 11 | hl_numbers: bool = false, | ||
| 12 | hl_strings: bool = false, | ||
| 13 | }; | ||
| 14 | |||
| 15 | name: []const u8, | ||
| 16 | // TODO: Make these into comptime StringSets, see std.ComptimeStringMap | ||
| 17 | filematch: []const []const u8, | ||
| 18 | keywords1: []const []const u8, | ||
| 19 | keywords2: []const []const u8, | ||
| 20 | singleline_comment_start: ?[]const u8, | ||
| 21 | multiline_comment_start: ?[]const u8, | ||
| 22 | multiline_comment_end: ?[]const u8, | ||
| 23 | separators: []const u8, | ||
| 24 | flags: Flags, | ||
| 25 | |||
| 26 | pub fn isSeparator(self: Syntax, char: u8) bool { | ||
| 27 | return std.ascii.isSpace(char) or std.mem.indexOfScalar(u8, self.separators, char) != null; | ||
| 28 | } | ||
diff --git a/src/Syntax/makefile.zig b/src/Syntax/makefile.zig new file mode 100644 index 0000000..4f73a2c --- /dev/null +++ b/src/Syntax/makefile.zig | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | const Syntax = @import("../Syntax.zig"); | ||
| 2 | |||
| 3 | pub const syntax = Syntax{ | ||
| 4 | .name = "Makefile", | ||
| 5 | .filematch = &[_][]const u8{ "GNUmakefile", "makefile", "Makefile", ".mk" }, | ||
| 6 | .keywords1 = &[_][]const u8{ | ||
| 7 | "$@", "$(@D)", "$(@F)", | ||
| 8 | "$%", "$(%D)", "$(%F)", | ||
| 9 | "$<", "$(<D)", "$(<F)", | ||
| 10 | "$?", "$(?D)", "$(?F)", | ||
| 11 | "$^", "$(^D)", "$(^F)", | ||
| 12 | "$+", "$(+D)", "$(+F)", | ||
| 13 | "$|", | ||
| 14 | "$*", "$(*D)", "$(*F)", | ||
| 15 | |||
| 16 | ".DEFAULT", ".DEFAULT_GOAL", ".DELETE_ON_ERROR", ".EXPORT_ALL_VARIABLES", ".EXTRA_PREREQS", ".FEATURES", | ||
| 17 | ".IGNORE", ".INCLUDE_DIRS", ".INTERMEDIATE", ".LIBPATTERNS", ".LOADED", ".LOW_RESOLUTION_TIME", ".NOTPARALLEL", | ||
| 18 | ".ONESHELL", ".PHONY", ".POSIX", ".PRECIOUS", ".RECIPEPREFIX", ".SECONDARY", ".SECONDEXPANSION", ".SHELLFLAGS", | ||
| 19 | ".SHELLSTATUS", ".SILENT", ".SUFFIXES", ".VARIABLES", | ||
| 20 | |||
| 21 | "abspath", "addprefix", "addsuffix", "and", "basename", "call", "define", "dir", "else", "endef", "endif", | ||
| 22 | "error", "eval", "export", "file", "filter", "filter-out", "findstring", "firstword", "flavor", "foreach", | ||
| 23 | "gmk-eval", "gmk-expand", "guile", "if", "ifdef", "ifeq", "ifndef", "ifneq", "include", "info", "join", | ||
| 24 | "lastword", "load", "notdir", "or", "origin", "override", "patsubst", "private", "realpath", "shell", "sort", | ||
| 25 | "strip", "subst", "suffix", "undefined", "unexport", "value", "vpath", "warning", "wildcard", "word", "wordlist", | ||
| 26 | "words", | ||
| 27 | |||
| 28 | "ar", "as", "awk", "bison", "c99", "cat", "cc", "chgrp", "chmod", "chown", "cmp", "co", "cp", "ctangle", "cweave", | ||
| 29 | "diff", "do", "done", "echo", "elif", "else", "egrep", "expr", "fc", "for", "f77", "false", "fi", "find", "flex", | ||
| 30 | "g++", "get", "grep", "gzip", "if", "in", "install", "install-info", "ld", "ldconfig", "lex", "lint", "ln", "ls", | ||
| 31 | "m2c", "make", "makeinfo", "mkdir", "mknod", "mv", "pc", "printf", "pwd", "ranlib", "rm", "rmdir", "sed", "sleep", | ||
| 32 | "sort", "tangle", "tar", "test", "tex", "texi2dvi", "then", "tr", "true", "touch", "weave", "yacc", | ||
| 33 | }, | ||
| 34 | .keywords2 = &[_][]const u8{ | ||
| 35 | "AR", "AS", "AWK", "BISON", "CAT", "CC", "CHGRP", "CHMOD", "CHOWN", "CMP", "CO", "CP", "CPP", "CTANGLE", "CWEAVE", | ||
| 36 | "CXX", "DIFF", "ECHO", "EGREP", "EXPR", "FALSE", "FC", "FIND", "FLEX", "GET", "GREP", "GZIP", "INSTALL", | ||
| 37 | "INSTALL_DATA", "INSTALL_INFO", "INSTALL_PROGRAM", "LD", "LDCONFIG", "LEX", "LINT", "LN", "LS", "M2C", "MAKE", | ||
| 38 | "MAKEINFO", "MKDIR", "MKNOD", "MV", "RM", "PC", "PRINTF", "PWD", "RANLIB", "RMDIR", "SED", "SLEEP", "SORT", | ||
| 39 | "TANGLE", "TAR", "TEST", "TEX", "TEXI2DVI", "TOUCH", "TR", "TRUE", "WEAVE", "YACC", | ||
| 40 | |||
| 41 | "ARFLAGS", "BISONFLAGS", "ASFLAGS", "CFLAGS", "COFLAGS", "CPPFLAGS", "CXXFLAGS", "FFLAGS", "GFLAGS", "LDFLAGS", | ||
| 42 | "LDLIBS", "LFLAGS", "LINTFLAGS", "MAKEFLAGS", "MFLAGS", "PFLAGS", "REALFLAGS", "YFLAGS", | ||
| 43 | |||
| 44 | "COMSPEC", "CURDIR", "DESTDIR", "GPATH", "LOADLIBES", "MAKECMDGOALS", "MAKEFILES", "MAKEFILE_LIST", "MAKELEVEL", | ||
| 45 | "MAKEOVERRIDES", "MAKESHELL", "MAKE_HOST", "MAKE_RESTARTS", "MAKE_TERMERR", "MAKE_TERMOUT", "MAKE_VERSION", | ||
| 46 | "OUTPUT_OPTION", "SHELL", "SUFFIXES", "VPATH", | ||
| 47 | |||
| 48 | "bindir", "datadir", "datarootdir", "docdir", "dvidir", "exec_prefix", "htmldir", "includedir", "infodir", | ||
| 49 | "libexecdir", "libdir", "lispdir", "localedir", "localstatedir", "mandir", "manext", "man1dir", "man1ext", | ||
| 50 | "man2dir", "man2ext", "man3dir", "man3ext", "man4dir", "man4ext", "man5dir", "man5ext", "man6dir", "man6ext", | ||
| 51 | "man7dir", "man7ext", "man8dir", "man8ext", "manndir", "mannext", "oldincludedir", "pdfdir", "psdir", "prefix", | ||
| 52 | "runstatedir", "sbindir", "srcdir", "sharedstatedir", "sysconfdir", | ||
| 53 | |||
| 54 | "all", "check", "clean", "dist", "distclean", "dvi", "html", "info", "install", "install-dvi", "install-html", | ||
| 55 | "install-pdf", "install-ps", "install-strip", "installcheck", "installdirs", "maintainer-clean", "mostlyclean", | ||
| 56 | "pdf", "ps", "uninstall", "TAGS", | ||
| 57 | |||
| 58 | "NORMAL_INSTALL", "NORMAL_UNINSTALL", "POST_INSTALL", "POST_UNINSTALL", "PRE_INSTALL", "PRE_UNINSTALL" | ||
| 59 | }, | ||
| 60 | .singleline_comment_start = "#", | ||
| 61 | .multiline_comment_start = null, | ||
| 62 | .multiline_comment_end = null, | ||
| 63 | .separators = "(){};:-@+", | ||
| 64 | .flags = .{}, | ||
| 65 | }; | ||
diff --git a/src/Syntax/zig.zig b/src/Syntax/zig.zig new file mode 100644 index 0000000..653fb1b --- /dev/null +++ b/src/Syntax/zig.zig | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Syntax = @import("../Syntax.zig"); | ||
| 4 | |||
| 5 | // TODO: Add support for the multiline string \\ | ||
| 6 | pub const syntax = Syntax{ | ||
| 7 | .name = "Zig", | ||
| 8 | .filematch = &[_][]const u8{ ".zig" }, | ||
| 9 | .keywords1 = &[_][]const u8{ | ||
| 10 | "align", "allowzero", "and", "anyframe", "anytype", "asm", "async", "await", "break", "catch", "comptime", | ||
| 11 | "const", "continue", "defer", "else", "enum", "errdefer", "error", "export", "extern", "false", "fn", "for", "if", | ||
| 12 | "inline", "noalias", "nosuspend", "null", "or", "orelse", "packed", "pub", "resume", "return", "linksection", | ||
| 13 | "struct", "suspend", "switch", "test", "threadlocal", "true", "try", "undefined", "union", "unreachable", | ||
| 14 | "usingnamespace", "var", "volatile", "while", | ||
| 15 | |||
| 16 | "opaque", | ||
| 17 | }, | ||
| 18 | .keywords2 = &[_][]const u8{ | ||
| 19 | // TODO: Generate all integer types with a comptime fn. | ||
| 20 | "i8", "u8", "i16", "u16", "i32", "u32", "i64", "u64", "i128", "u128", "isize", "usize", "c_short", "c_ushort", | ||
| 21 | "c_int", "c_uint", "c_long", "c_ulong", "c_longlong", "c_ulonglong", "c_longdouble", "c_void", "f16", "f32", | ||
| 22 | "f64", "f128", "bool", "void", "noreturn", "type", "anyerror", "comptime_int", "comptime_float", | ||
| 23 | |||
| 24 | "@addWithOverflow", "@alignCast", "@alignOf", "@as", "@asyncCall", "@atomicLoad", "@atomicRmw", "@atomicStore", | ||
| 25 | "@bitCast", "@bitOffsetOf", "@boolToInt", "@bitSizeOf", "@breakpoint", "@mulAdd", "@byteSwap", "@bitReverse", | ||
| 26 | "@offsetOf", "@call", "@cDefine", "@cImport", "@cInclude", "@clz", "@cmpxchgStrong", "@cmpxchgWeak", | ||
| 27 | "@compileError", "@compileLog", "@ctz", "@cUndef", "@divExact", "@divFloor", "@divTrunc", "@embedFile", | ||
| 28 | "@enumToIt", "@errorName", "@errorReturnTrace", "@errorToInt", "@errSetCast", "@export", "@extern", "@fence", | ||
| 29 | "@field", "@fieldParentPtr", "@floatCast", "@floatToInt", "@frame", "@Frame", "@frameAddress", "@frameSize", | ||
| 30 | "@hasDecl", "@hasField", "@import", "@intCast", "@intToEnum", "@intToError" , "@intToFloat", "@intToPtr", | ||
| 31 | "@maximum", "@memcpy", "@memset", "@minimum", "@wasmMemorySize", "@wasmMemoryGrow", "@mod", "@mulWithOverflow", | ||
| 32 | "@panic", "@popCount", "@prefetch", "@ptrCast", "@ptrToInt", "@rem", "@returnAddress", "@select", | ||
| 33 | "@setAlignStack", "@setCold", "@setEvalBranchQuota", "@setFloatMode", "@setRuntimeSafety", "@shlExact", | ||
| 34 | "@shlWithOverflow", "@shrExact", "@shuffle", "@sizeOf", "@splat", "@reduce", "@src", "@sqrt", "@sin", "@cos", | ||
| 35 | "@exp", "@exp2", "@log", "@log2", "@log10", "@fabs", "@floor", "@ceil", "@trunc", "@round", "@subWithOverflow", | ||
| 36 | "@tagName", "@This", "@truncate", "@Type", "@typeInfo", "@typeName", "@TypeOf", "@unionInit", | ||
| 37 | }, | ||
| 38 | .singleline_comment_start = "//", | ||
| 39 | .multiline_comment_start = null, | ||
| 40 | .multiline_comment_end = null, | ||
| 41 | .separators = "&*^:,.=!<{[(-%|+?>}]);/~", | ||
| 42 | .flags = .{ .hl_numbers = true, .hl_strings = true }, | ||
| 43 | }; | ||
diff --git a/src/highlight.zig b/src/highlight.zig new file mode 100644 index 0000000..b0f4cfd --- /dev/null +++ b/src/highlight.zig | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | pub const Highlight = enum { | ||
| 2 | none, | ||
| 3 | normal, | ||
| 4 | // Syntax highlighting | ||
| 5 | comment, | ||
| 6 | comment_ml, | ||
| 7 | keyword1, | ||
| 8 | keyword2, | ||
| 9 | number, | ||
| 10 | string, | ||
| 11 | // Other "faces" | ||
| 12 | line_no, | ||
| 13 | match, | ||
| 14 | overlong_line, | ||
| 15 | |||
| 16 | pub fn asString(hl: Highlight) []const u8 { | ||
| 17 | return switch (hl) { | ||
| 18 | .none => "\x1b[m", | ||
| 19 | |||
| 20 | .normal => "\x1b[39m", | ||
| 21 | |||
| 22 | .comment => "\x1b[36m", | ||
| 23 | .comment_ml => "\x1b[36m", | ||
| 24 | .keyword1 => "\x1b[33m", | ||
| 25 | .keyword2 => "\x1b[32m", | ||
| 26 | .number => "\x1b[31m", | ||
| 27 | .string => "\x1b[35m", | ||
| 28 | |||
| 29 | .line_no => "\x1b[0;90m", | ||
| 30 | .match => "\x1b[34m", | ||
| 31 | .overlong_line => "\x1b[91m", | ||
| 32 | }; | ||
| 33 | } | ||
| 34 | }; | ||
diff --git a/src/key.zig b/src/key.zig new file mode 100644 index 0000000..1a68ebb --- /dev/null +++ b/src/key.zig | |||
| @@ -0,0 +1,102 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | pub const Key = enum(u16) { | ||
| 4 | return_ = 0x0d, | ||
| 5 | escape = 0x1b, | ||
| 6 | space = 0x20, | ||
| 7 | backspace = 0x7f, | ||
| 8 | max_char = 0xff, | ||
| 9 | |||
| 10 | meta_nil = 0x100, | ||
| 11 | meta_max_char = 0x1ff, | ||
| 12 | |||
| 13 | left, | ||
| 14 | right, | ||
| 15 | up, | ||
| 16 | down, | ||
| 17 | delete, | ||
| 18 | home, | ||
| 19 | end, | ||
| 20 | page_up, | ||
| 21 | page_down, | ||
| 22 | |||
| 23 | _, | ||
| 24 | |||
| 25 | pub fn char(ch: u8) Key { | ||
| 26 | return @intToEnum(Key, ch); | ||
| 27 | } | ||
| 28 | |||
| 29 | pub fn ctrl(ch: u8) Key { | ||
| 30 | return @intToEnum(Key, ch & 0x1f); | ||
| 31 | } | ||
| 32 | |||
| 33 | pub fn format( | ||
| 34 | key: Key, | ||
| 35 | comptime fmt: []const u8, | ||
| 36 | options: std.fmt.FormatOptions, | ||
| 37 | writer: anytype, | ||
| 38 | ) @TypeOf(writer).Error!void { | ||
| 39 | comptime if (fmt.len != 0) { | ||
| 40 | @compileError("Key doesn't support {" ++ fmt ++ "} format"); | ||
| 41 | }; | ||
| 42 | |||
| 43 | return switch (key) { | ||
| 44 | .return_ => std.fmt.formatBuf("<return>", options, writer), | ||
| 45 | .escape => std.fmt.formatBuf("<escape>", options, writer), | ||
| 46 | .space => std.fmt.formatBuf("<space>", options, writer), | ||
| 47 | .backspace => std.fmt.formatBuf("<backspace>", options, writer), | ||
| 48 | .max_char => key.formatGeneric(options, writer), | ||
| 49 | |||
| 50 | .meta_nil, .meta_max_char => key.formatGeneric(options, writer), | ||
| 51 | |||
| 52 | .left => std.fmt.formatBuf("<left>", options, writer), | ||
| 53 | .right => std.fmt.formatBuf("<right>", options, writer), | ||
| 54 | .up => std.fmt.formatBuf("<up>", options, writer), | ||
| 55 | .down => std.fmt.formatBuf("<down>", options, writer), | ||
| 56 | .delete => std.fmt.formatBuf("<delete>", options, writer), | ||
| 57 | .home => std.fmt.formatBuf("<home>", options, writer), | ||
| 58 | .end => std.fmt.formatBuf("<end>", options, writer), | ||
| 59 | .page_up => std.fmt.formatBuf("<page-up>", options, writer), | ||
| 60 | .page_down => std.fmt.formatBuf("<page-down>", options, writer), | ||
| 61 | |||
| 62 | _ => key.formatGeneric(options, writer), | ||
| 63 | }; | ||
| 64 | } | ||
| 65 | |||
| 66 | pub fn meta(ch: u8) Key { | ||
| 67 | return @intToEnum(Key, ch + @enumToInt(Key.meta_nil)); | ||
| 68 | } | ||
| 69 | |||
| 70 | pub fn metaCtrl(ch: u8) Key { | ||
| 71 | return @intToEnum(Key, (ch & 0x1f) + @enumToInt(Key.meta_nil)); | ||
| 72 | } | ||
| 73 | |||
| 74 | fn formatGeneric( | ||
| 75 | key: Key, | ||
| 76 | options: std.fmt.FormatOptions, | ||
| 77 | writer: anytype, | ||
| 78 | ) @TypeOf(writer).Error!void { | ||
| 79 | const key_int = @enumToInt(key); | ||
| 80 | if (key_int < @enumToInt(Key.space)) { | ||
| 81 | const ch = std.ascii.toLower(@intCast(u8, key_int + 0x40)); | ||
| 82 | const buf = [_]u8{'C', '-', ch}; | ||
| 83 | return std.fmt.formatBuf(&buf, options, writer); | ||
| 84 | } else if (key_int < @enumToInt(Key.meta_nil)) { | ||
| 85 | const buf = [_]u8{@intCast(u8, key_int)}; | ||
| 86 | // This should be printed as C-? or <backspace>, it's dealt with in | ||
| 87 | // format() | ||
| 88 | std.debug.assert(buf[0] != '\x7F'); | ||
| 89 | return std.fmt.formatBuf(&buf, options, writer); | ||
| 90 | } else if (key_int <= @enumToInt(Key.meta_max_char)) { | ||
| 91 | try std.fmt.formatBuf("M-", options, writer); | ||
| 92 | return Key.format( | ||
| 93 | @intToEnum(Key, key_int - @enumToInt(Key.meta_nil)), | ||
| 94 | "", | ||
| 95 | options, | ||
| 96 | writer, | ||
| 97 | ); | ||
| 98 | } else { | ||
| 99 | unreachable; | ||
| 100 | } | ||
| 101 | } | ||
| 102 | }; | ||
diff --git a/src/key_state.zig b/src/key_state.zig new file mode 100644 index 0000000..b5b16ee --- /dev/null +++ b/src/key_state.zig | |||
| @@ -0,0 +1,96 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Buffer = @import("Buffer.zig"); | ||
| 4 | const Editor = @import("Editor.zig"); | ||
| 5 | const Key = @import("key.zig").Key; | ||
| 6 | const search = @import("search.zig").search; | ||
| 7 | |||
| 8 | pub const Error = error{ | ||
| 9 | MalformedConfig, | ||
| 10 | MisformedTerminalResponse, | ||
| 11 | StreamTooLong, | ||
| 12 | } || | ||
| 13 | std.mem.Allocator.Error || | ||
| 14 | std.fmt.ParseIntError || | ||
| 15 | std.fs.File.OpenError || | ||
| 16 | std.fs.File.ReadError || | ||
| 17 | std.fs.File.WriteError || | ||
| 18 | std.os.SchedYieldError; | ||
| 19 | pub const KeyState = fn(*Editor, *Buffer, Key) Error!void; | ||
| 20 | |||
| 21 | pub fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void { | ||
| 22 | _ = buf; | ||
| 23 | editor.current_state = defaultState; | ||
| 24 | editor.clearStatusMessage(); | ||
| 25 | |||
| 26 | switch (key) { | ||
| 27 | // ========== C-<*> ========== | ||
| 28 | Key.ctrl('b') => try editor.switchBuffer(), | ||
| 29 | Key.ctrl('c') => try editor.saveBuffersExit(), | ||
| 30 | Key.ctrl('f') => try editor.openFile(), | ||
| 31 | Key.ctrl('g') => {}, | ||
| 32 | Key.ctrl('s') => try buf.save(editor), | ||
| 33 | |||
| 34 | // ========== <*> ========== | ||
| 35 | Key.char('k') => _ = try editor.killCurrentBuffer(), | ||
| 36 | |||
| 37 | else => try editor.setStatusMessage("Unknown chord: C-x {}", .{key}), | ||
| 38 | } | ||
| 39 | } | ||
| 40 | |||
| 41 | pub fn defaultState(editor: *Editor, buf: *Buffer, key: Key) Error!void { | ||
| 42 | switch (key) { | ||
| 43 | // ========== M-C-<*> ========== | ||
| 44 | Key.metaCtrl('d'), Key.backspace => try buf.backwardDeleteChar(), | ||
| 45 | |||
| 46 | // ========== M-<*> ========== | ||
| 47 | Key.meta('v'), Key.page_up => buf.pageUp(editor.screenrows), | ||
| 48 | |||
| 49 | // ========== C-<*> ========== | ||
| 50 | Key.ctrl('a'), Key.home => buf.moveBeginningOfLine(), | ||
| 51 | Key.ctrl('b'), Key.left => buf.backwardChar(), | ||
| 52 | Key.ctrl('d'), Key.delete => try buf.deleteChar(), | ||
| 53 | Key.ctrl('e'), Key.end => buf.moveEndOfLine(), | ||
| 54 | Key.ctrl('f'), Key.right => buf.forwardChar(), | ||
| 55 | Key.ctrl('g') => editor.clearStatusMessage(), | ||
| 56 | |||
| 57 | // TODO: C-h help | ||
| 58 | |||
| 59 | // line feed | ||
| 60 | Key.ctrl('j') => try buf.insertNewline(), | ||
| 61 | Key.ctrl('k') => try buf.killLine(), | ||
| 62 | |||
| 63 | Key.ctrl('l') => { | ||
| 64 | try editor.refreshWindowSize(); | ||
| 65 | buf.recenterTopBottom(editor.screenrows); | ||
| 66 | }, | ||
| 67 | |||
| 68 | // carriage return | ||
| 69 | Key.ctrl('m') => try buf.insertNewline(), | ||
| 70 | Key.ctrl('n'), Key.down => buf.nextLine(), | ||
| 71 | Key.ctrl('p'), Key.up => buf.previousLine(), | ||
| 72 | Key.ctrl('s') => try search(editor, buf), | ||
| 73 | |||
| 74 | // TODO: C-q quotedInsert | ||
| 75 | |||
| 76 | Key.ctrl('v'), Key.page_down => buf.pageDown(editor.screenrows), | ||
| 77 | |||
| 78 | Key.ctrl('x') => { | ||
| 79 | editor.current_state = cxState; | ||
| 80 | try editor.setStatusMessage("C-x-", .{}); | ||
| 81 | }, | ||
| 82 | |||
| 83 | // ========== <*> ========== | ||
| 84 | else => { | ||
| 85 | if (@enumToInt(key) <= @enumToInt(Key.max_char)) { | ||
| 86 | const char = @intCast(u8, @enumToInt(key)); | ||
| 87 | if (std.ascii.isGraph(char) or std.ascii.isSpace(char)) { | ||
| 88 | try buf.insertChar(char); | ||
| 89 | return; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | try editor.setStatusMessage("Unknown key: {}", .{key}); | ||
| 94 | }, | ||
| 95 | } | ||
| 96 | } | ||
diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..9c81776 --- /dev/null +++ b/src/main.zig | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | const Editor = @import("Editor.zig"); | ||
| 5 | const GPA = std.heap.GeneralPurposeAllocator(.{}); | ||
| 6 | const RawMode = @import("RawMode.zig"); | ||
| 7 | |||
| 8 | pub fn main() !void { | ||
| 9 | var gpa = GPA{}; | ||
| 10 | defer _ = gpa.deinit(); | ||
| 11 | const allocator = gpa.allocator(); | ||
| 12 | |||
| 13 | const raw_mode = try RawMode.init(); | ||
| 14 | defer raw_mode.deinit(); | ||
| 15 | |||
| 16 | var editor = try Editor.init(allocator); | ||
| 17 | defer editor.deinit(); | ||
| 18 | |||
| 19 | try processCommandLineArgs(allocator, &editor); | ||
| 20 | |||
| 21 | try editor.setStatusMessage("C-x C-s = Save | C-x C-c = Quit | C-s = Search", .{}); | ||
| 22 | |||
| 23 | while (!editor.should_exit) { | ||
| 24 | try editor.refreshScreen(); | ||
| 25 | try editor.processKeypress(); | ||
| 26 | } | ||
| 27 | } | ||
| 28 | |||
| 29 | fn processCommandLineArgs(allocator: Allocator, editor: *Editor) !void { | ||
| 30 | const args = try std.process.argsAlloc(allocator); | ||
| 31 | defer std.process.argsFree(allocator, args); | ||
| 32 | if (args.len <= 1) { | ||
| 33 | // Do nothing | ||
| 34 | } else if (args.len == 2) { | ||
| 35 | try editor.open(args[1]); | ||
| 36 | } else { | ||
| 37 | std.log.err("What am I to do with {} arguments?", .{args.len}); | ||
| 38 | return error.CommandLineArgs; | ||
| 39 | } | ||
| 40 | } | ||
diff --git a/src/search.zig b/src/search.zig new file mode 100644 index 0000000..bc29813 --- /dev/null +++ b/src/search.zig | |||
| @@ -0,0 +1,94 @@ | |||
| 1 | const std = @import("std"); | ||
| 2 | |||
| 3 | const Allocator = std.mem.Allocator; | ||
| 4 | const ArrayList = std.ArrayList; | ||
| 5 | const Buffer = @import("Buffer.zig"); | ||
| 6 | const Editor = @import("Editor.zig"); | ||
| 7 | const Highlight = @import("highlight.zig").Highlight; | ||
| 8 | const Key = @import("key.zig").Key; | ||
| 9 | |||
| 10 | const CallbackData = struct { | ||
| 11 | allocator: Allocator, | ||
| 12 | last_match: usize, | ||
| 13 | saved_hl: ?struct { | ||
| 14 | line: usize, | ||
| 15 | data: []Highlight, | ||
| 16 | }, | ||
| 17 | buffer: *Buffer, | ||
| 18 | }; | ||
| 19 | |||
| 20 | const Error = std.mem.Allocator.Error; | ||
| 21 | |||
| 22 | pub fn search(editor: *Editor, buffer: *Buffer) !void { | ||
| 23 | const saved_cx = buffer.cx; | ||
| 24 | const saved_cy = buffer.cy; | ||
| 25 | const saved_coloff = buffer.coloff; | ||
| 26 | const saved_rowoff = buffer.rowoff; | ||
| 27 | |||
| 28 | var data = CallbackData{ | ||
| 29 | .allocator = editor.allocator, | ||
| 30 | .last_match = buffer.cy, | ||
| 31 | .saved_hl = null, | ||
| 32 | .buffer = buffer, | ||
| 33 | }; | ||
| 34 | |||
| 35 | defer if (data.saved_hl) |saved_hl| { | ||
| 36 | data.allocator.free(saved_hl.data); | ||
| 37 | }; | ||
| 38 | |||
| 39 | if (try editor.promptEx(*CallbackData, Error, "Search", searchCallback, &data)) |response| { | ||
| 40 | editor.allocator.free(response); | ||
| 41 | } else { | ||
| 42 | // Cancelled | ||
| 43 | buffer.cx = saved_cx; | ||
| 44 | buffer.cy = saved_cy; | ||
| 45 | buffer.coloff = saved_coloff; | ||
| 46 | buffer.rowoff = saved_rowoff; | ||
| 47 | } | ||
| 48 | } | ||
| 49 | |||
| 50 | fn searchCallback(editor: *Editor, query: []const u8, last_key: Key, data: *CallbackData) Error!void { | ||
| 51 | _ = editor; | ||
| 52 | |||
| 53 | if (data.saved_hl) |saved_hl| { | ||
| 54 | const row = &data.buffer.rows.items[saved_hl.line]; | ||
| 55 | row.hldata.shrinkRetainingCapacity(0); | ||
| 56 | row.hldata.appendSliceAssumeCapacity(saved_hl.data); | ||
| 57 | |||
| 58 | data.allocator.free(saved_hl.data); | ||
| 59 | data.saved_hl = null; | ||
| 60 | } | ||
| 61 | |||
| 62 | if (last_key == Key.return_ or last_key == Key.ctrl('g')) { | ||
| 63 | return; | ||
| 64 | } | ||
| 65 | |||
| 66 | if (last_key == Key.ctrl('s') and query.len != 0) { | ||
| 67 | data.last_match += 1; | ||
| 68 | } | ||
| 69 | |||
| 70 | var i: usize = 0; | ||
| 71 | while (i < data.buffer.rows.items.len) : (i += 1) { | ||
| 72 | const idx = (data.last_match + i) % data.buffer.rows.items.len; | ||
| 73 | |||
| 74 | const row = &data.buffer.rows.items[idx]; | ||
| 75 | if (std.mem.indexOf(u8, row.rdata.items, query)) |match| { | ||
| 76 | data.last_match = idx; | ||
| 77 | data.buffer.cy = idx; | ||
| 78 | data.buffer.cx = row.rxToCx(data.buffer.config, match); | ||
| 79 | // TODO: data.buffer.rowoff = data.buffer.rows.items.len; | ||
| 80 | |||
| 81 | data.saved_hl = .{ | ||
| 82 | .line = idx, | ||
| 83 | .data = try data.allocator.dupe(Highlight, row.hldata.items), | ||
| 84 | }; | ||
| 85 | |||
| 86 | var j: usize = 0; | ||
| 87 | while (j < query.len) : (j += 1) { | ||
| 88 | row.hldata.items[match + j] = .match; | ||
| 89 | } | ||
| 90 | |||
| 91 | return; | ||
| 92 | } | ||
| 93 | } | ||
| 94 | } | ||