summaryrefslogtreecommitdiff
path: root/src/Buffer.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Buffer.zig')
-rw-r--r--src/Buffer.zig518
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 @@
1const es_config = @import("es-config");
2const std = @import("std");
3
4const Allocator = std.mem.Allocator;
5const ArrayList = std.ArrayList;
6const Buffer = @This();
7const Config = @import("Config.zig");
8const File = std.fs.File;
9const Editor = @import("Editor.zig");
10const Highlight = @import("highlight.zig").Highlight;
11const Row = @import("Row.zig");
12const Syntax = @import("Syntax.zig");
13
14allocator: Allocator,
15
16// TODO: Short name & file name split
17name: []u8,
18
19rows: ArrayList(Row),
20
21cx: usize,
22cy: usize,
23rx: usize,
24
25rowoff: usize,
26coloff: usize,
27
28dirty: bool,
29has_file: bool,
30
31config: Config,
32syntax: ?Syntax,
33
34pub 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
63pub 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
72pub fn appendRow(self: *Buffer, data: []const u8) !void {
73 try self.insertRow(self.rows.items.len, data);
74}
75
76pub 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
89pub fn backwardDeleteChar(self: *Buffer) !void {
90 self.backwardChar();
91 return self.deleteChar();
92}
93
94pub 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
110pub 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
129pub 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
140pub 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
150pub 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
208pub 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
241pub 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
254pub 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
265pub 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
291pub 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
305pub fn killLine(self: *Buffer) !void {
306 return self.deleteRow(self.cy);
307}
308
309pub 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
316pub fn moveBeginningOfLine(self: *Buffer) void {
317 self.cx = 0;
318}
319
320pub 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
328pub 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
342pub 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
354pub 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
362pub 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
371pub 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
380pub 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
432pub 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
451pub 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
473fn 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
490fn 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
508fn 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}