summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Buffer.zig518
-rw-r--r--src/Config.zig78
-rw-r--r--src/Editor.zig461
-rw-r--r--src/RawMode.zig38
-rw-r--r--src/Row.zig273
-rw-r--r--src/StringBuilder.zig45
-rw-r--r--src/Syntax.zig28
-rw-r--r--src/Syntax/makefile.zig65
-rw-r--r--src/Syntax/zig.zig43
-rw-r--r--src/highlight.zig34
-rw-r--r--src/key.zig102
-rw-r--r--src/key_state.zig96
-rw-r--r--src/main.zig40
-rw-r--r--src/search.zig94
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 @@
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}
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
3const std = @import("std");
4
5const Allocator = std.mem.Allocator;
6const BufMap = std.BufMap;
7const Config = @This();
8
9const config_path = "arkta/es/es.ini";
10
11line_limit: usize = 100,
12page_overlap: usize = 2,
13tab_stop: usize = 8,
14
15pub 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
44fn 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 @@
1const linux = std.os.linux;
2const std = @import("std");
3
4const Allocator = std.mem.Allocator;
5const ArrayList = std.ArrayList;
6const Buffer = @import("Buffer.zig");
7const Editor = @This();
8const Key = @import("key.zig").Key;
9const key_state = @import("key_state.zig");
10const KeyState = key_state.KeyState;
11const STDIN_FILENO = std.os.STDIN_FILENO;
12const StringBuilder = @import("StringBuilder.zig");
13const StringHashMap = std.StringHashMap;
14
15allocator: Allocator,
16
17buffers: StringHashMap(Buffer),
18buffer: *Buffer,
19
20screenrows: usize,
21screencols: usize,
22
23statusmsg: ?[]u8,
24statusmsg_time: i64,
25
26current_state: KeyState,
27
28should_exit: bool,
29
30pub 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
57pub 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
72pub fn clearStatusMessage(self: *Editor) void {
73 if (self.statusmsg) |statusmsg| {
74 self.statusmsg = null;
75 self.allocator.free(statusmsg);
76 }
77}
78
79pub fn getBuffer(self: Editor, name: []const u8) ?*Buffer {
80 return self.buffers.getPtr(name);
81}
82
83pub 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
98pub 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.
103pub 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
123pub 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
162pub 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
170pub fn processKeypress(self: *Editor) !void {
171 const key = try readKey();
172 try self.current_state(self, self.buffer, key);
173}
174
175pub fn prompt(self: *Editor, prompt_str: []const u8) !?[]u8 {
176 return self.promptEx(void, error{}, prompt_str, null, {});
177}
178
179pub 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
229pub fn PromptCallback(comptime Data: type, comptime Error: type) type {
230 return fn(*Editor, []const u8, Key, Data) Error!void;
231}
232
233pub 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
249pub 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
261pub 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
283pub fn refreshWindowSize(self: *Editor) !void {
284 try getWindowSize(&self.screenrows, &self.screencols);
285 self.screenrows -= 2;
286}
287
288pub 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
299pub 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
315pub 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
329fn 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
340fn 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
366fn 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
382fn 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
390fn 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 @@
1const linux = std.os.linux;
2const std = @import("std");
3
4const STDIN_FILENO = std.os.STDIN_FILENO;
5const RawMode = @This();
6const tcflag_t = linux.tcflag_t;
7const tcgetattr = std.os.tcgetattr;
8const tcsetattr = std.os.tcsetattr;
9const termios = std.os.termios;
10
11orig: termios,
12
13pub 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
34pub 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 @@
1const std = @import("std");
2
3const Allocator = std.mem.Allocator;
4const ArrayList = std.ArrayList;
5const Buffer = @import("Buffer.zig");
6const Config = @import("Config.zig");
7const Highlight = @import("highlight.zig").Highlight;
8const Row = @This();
9const StringBuilder = @import("StringBuilder.zig");
10
11allocator: Allocator,
12
13idx: usize,
14
15data: ArrayList(u8),
16rdata: ArrayList(u8),
17hldata: ArrayList(Highlight),
18
19ends_with_open_comment: bool,
20
21pub 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
39pub fn deinit(self: Row) void {
40 self.data.deinit();
41 self.rdata.deinit();
42 self.hldata.deinit();
43}
44
45pub fn appendString(self: *Row, buf: *Buffer, str: []const u8) !void {
46 try self.data.appendSlice(str);
47 return self.update(buf);
48}
49
50pub 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
66pub 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
80pub fn deleteChar(self: *Row, buf: *Buffer, at: usize) !void {
81 _ = self.data.orderedRemove(at);
82 try self.update(buf);
83}
84
85pub 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
100pub 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
105pub 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)
127pub 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
142const UpdateSyntaxError = std.mem.Allocator.Error;
143pub 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 @@
1const std = @import("std");
2
3const Allocator = std.mem.Allocator;
4const ArrayList = std.ArrayList;
5const StringBuilder = @This();
6
7pub const Error = std.mem.Allocator.Error;
8pub const Writer = std.io.Writer(*StringBuilder, Error, writeFn);
9
10data: ArrayList(u8),
11
12pub fn init(allocator: std.mem.Allocator) StringBuilder {
13 return .{
14 .data = ArrayList(u8).init(allocator),
15 };
16}
17
18pub fn deinit(self: StringBuilder) void {
19 self.data.deinit();
20}
21
22pub fn append(self: *StringBuilder, char: u8) Error!void {
23 return self.data.append(char);
24}
25
26pub fn appendMany(self: *StringBuilder, chars: []const u8) Error!void {
27 return self.data.appendSlice(chars);
28}
29
30pub fn seeSlice(self: *StringBuilder) []const u8 {
31 return self.data.items;
32}
33
34pub fn toOwnedSlice(self: StringBuilder) []u8 {
35 return self.data.toOwnedSlice();
36}
37
38pub fn writer(self: *StringBuilder) Writer {
39 return Writer { .context = self };
40}
41
42fn 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 @@
1pub const makefile = @import("Syntax/makefile.zig");
2pub const zig = @import("Syntax/zig.zig");
3
4const std = @import("std");
5
6const Syntax = @This();
7
8pub const database = [_]Syntax{makefile.syntax, zig.syntax};
9
10pub const Flags = struct {
11 hl_numbers: bool = false,
12 hl_strings: bool = false,
13};
14
15name: []const u8,
16// TODO: Make these into comptime StringSets, see std.ComptimeStringMap
17filematch: []const []const u8,
18keywords1: []const []const u8,
19keywords2: []const []const u8,
20singleline_comment_start: ?[]const u8,
21multiline_comment_start: ?[]const u8,
22multiline_comment_end: ?[]const u8,
23separators: []const u8,
24flags: Flags,
25
26pub 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 @@
1const Syntax = @import("../Syntax.zig");
2
3pub 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 @@
1const std = @import("std");
2
3const Syntax = @import("../Syntax.zig");
4
5// TODO: Add support for the multiline string \\
6pub 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 @@
1pub 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 @@
1const std = @import("std");
2
3pub 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 @@
1const std = @import("std");
2
3const Buffer = @import("Buffer.zig");
4const Editor = @import("Editor.zig");
5const Key = @import("key.zig").Key;
6const search = @import("search.zig").search;
7
8pub 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;
19pub const KeyState = fn(*Editor, *Buffer, Key) Error!void;
20
21pub 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
41pub 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 @@
1const std = @import("std");
2
3const Allocator = std.mem.Allocator;
4const Editor = @import("Editor.zig");
5const GPA = std.heap.GeneralPurposeAllocator(.{});
6const RawMode = @import("RawMode.zig");
7
8pub 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
29fn 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 @@
1const std = @import("std");
2
3const Allocator = std.mem.Allocator;
4const ArrayList = std.ArrayList;
5const Buffer = @import("Buffer.zig");
6const Editor = @import("Editor.zig");
7const Highlight = @import("highlight.zig").Highlight;
8const Key = @import("key.zig").Key;
9
10const 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
20const Error = std.mem.Allocator.Error;
21
22pub 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
50fn 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}