TUI editor and editor backend written in Zig
1//! Managed editor object for a single file. **All properties are considered private after
2//! initialization. Modifying them will result in undefined behavior.** Use the helper methods
3//! instead of modifying properties directly.
4
5const std = @import("std");
6
7const Pos = @import("pos.zig");
8const IndexPos = @import("indexpos.zig").IndexPos;
9const Range = @import("Range.zig");
10const Selection = @import("Selection.zig");
11const Language = @import("Language.zig");
12const Zig = @import("languages/Zig.zig");
13
14const Allocator = std.mem.Allocator;
15
16const Editor = @This();
17
18/// Represents a position in the currently open file in the `Editor`. Directly corresponds to a
19/// `Pos`.
20pub const CoordinatePos = struct {
21 row: usize,
22 col: usize,
23};
24
25pub const TokenType = enum {
26 Text,
27 Whitespace,
28};
29
30pub const Token = struct {
31 pos: Pos,
32 type: TokenType,
33 text: []const u8,
34};
35
36/// The currently loaded file. **Modifying this will cause undefined behavior**. Use the helper
37/// methods to manipulate the currently open file.
38filename: std.ArrayListUnmanaged(u8),
39/// The text of the currently loaded file. **Modifying this will cause undefined behavior**.
40/// Use the helper methods to manipulate file text.
41text: std.ArrayListUnmanaged(u8),
42/// The start position of each line in the content buffer using a byte-position. **Modifying this
43/// will cause undefined behavior**. This will automatically be kept up to date by helper methods.
44line_indexes: std.ArrayListUnmanaged(IndexPos),
45/// This is the length of the longest line at any given moment. This will automatically be kept up
46/// to date by helper methods. **Modifying this may cause undefined behavior**.
47longest_line: usize,
48/// An array of tokens in the text. The text will be tokenized every time it changes. **Modifying
49/// this will cause undefined behavior**. The default tokenization has the whole text set to a
50/// simple `Text` type.
51tokens: std.ArrayListUnmanaged(Token),
52/// An array tracking all of the selections in the editor. **Modifying this will cause undefined
53/// behavior**. Use the methods on the editor to manipulate the selections instead.
54selections: std.ArrayListUnmanaged(Selection),
55/// Adds language support features when present.
56language: ?Language = null,
57
58pub const Command = union(enum) {
59 /// Adds a new cursor at the given position.
60 AddCursor: Pos,
61 /// Adds a new selection at the given position.
62 AddSelection: Range,
63 /// Deletes text inside all selections. Nothing will be deleted from empty selections.
64 DeleteInsideSelections,
65 /// Deletes the letter after the cursor. Unicode-aware.
66 DeleteLetterAfterCursors,
67 /// Deletes the letter before the cursor. Unicode-aware.
68 DeleteLetterBeforeCursors,
69};
70
71/// Initializes an Editor struct with an empty filename and empty content buffer.
72pub fn init(allocator: Allocator) !Editor {
73 var selections: std.ArrayListUnmanaged(Selection) = .empty;
74 try selections.append(
75 allocator,
76 .init,
77 );
78
79 var lines: std.ArrayListUnmanaged(IndexPos) = .empty;
80 try lines.append(allocator, .fromInt(0));
81
82 var tokens: std.ArrayListUnmanaged(Token) = .empty;
83 try tokens.append(allocator, .{ .pos = .{ .row = 0, .col = 0 }, .text = "", .type = .Text });
84
85 return .{
86 .filename = .empty,
87 .line_indexes = lines,
88 .longest_line = 0,
89 .selections = selections,
90 .text = .empty,
91 .tokens = tokens,
92 };
93}
94
95pub fn deinit(self: *Editor, allocator: Allocator) void {
96 self.filename.deinit(allocator);
97 self.line_indexes.deinit(allocator);
98 self.selections.deinit(allocator);
99 self.text.deinit(allocator);
100 self.tokens.deinit(allocator);
101}
102
103/// Opens the file provided and loads the contents of the file into the content buffer. `filename`
104/// must be a file path relative to the current working directory or an absolute path.
105/// TODO: handle errors in a way that this can return `void` or maybe some `result` type.
106pub fn openFile(self: *Editor, allocator: Allocator, filename: []const u8) !void {
107 // 1. Open a scratch buffer if no file name is provided.
108
109 if (filename.len == 0) {
110 self.filename.clearAndFree(allocator);
111 self.text.clearAndFree(allocator);
112 try self.updateLines(allocator);
113 try self.tokenize(allocator);
114 return;
115 }
116
117 // 2. Open the file for reading.
118
119 const file = try std.fs.cwd().createFile(filename, .{ .read = true, .truncate = false });
120 defer file.close();
121
122 self.text.clearAndFree(allocator);
123
124 if (try file.getEndPos() > 0) {
125 // 3. Get a reader to read the file.
126
127 var buffer: [1024]u8 = undefined;
128 var file_reader = file.reader(&buffer);
129 var reader = &file_reader.interface;
130
131 // 4. Read the file and store in state.
132
133 var writer: std.Io.Writer.Allocating = .fromArrayList(allocator, &self.text);
134 defer writer.deinit();
135
136 // Stream from file to the text array list.
137 _ = try reader.stream(&writer.writer, .unlimited);
138 try writer.writer.flush();
139 self.text = writer.toArrayList();
140 }
141
142 // 5. Only after the file has been successfully read do we update file name and other state.
143
144 self.filename.clearRetainingCapacity();
145 try self.filename.appendSlice(allocator, filename);
146
147 // 6. Update line start array.
148
149 try self.updateLines(allocator);
150
151 // 7. Tokenize the new text.
152
153 try self.tokenize(allocator);
154
155 // 8. Initialize a language, if we have an implementation for it.
156
157 if (std.mem.eql(u8, std.fs.path.extension(self.filename.items), ".zig")) {
158 self.language = Zig.init();
159 } else {
160 self.language = null;
161 }
162}
163
164/// Saves the text to the current location based on the `filename` field.
165pub fn saveFile(self: *Editor, gpa: std.mem.Allocator) !void {
166 if (self.language) |*l| format_text: {
167 const formatted = l.formatter.format(gpa, self.text.items) catch {
168 std.log.debug("failed to format buffer", .{});
169 break :format_text;
170 };
171 defer gpa.free(formatted);
172
173 // If, for whatever reason, the formatter doesn't return anything, we likely do not want to
174 // actually empty the text buffer, so we do nothing in that case.
175 if (std.mem.eql(u8, formatted, "")) break :format_text;
176
177 std.log.debug("formatted source:\n{s}", .{formatted});
178
179 self.text.clearRetainingCapacity();
180 try self.text.ensureTotalCapacity(gpa, formatted.len);
181 self.text.appendSliceAssumeCapacity(formatted);
182
183 try self.updateLines(gpa);
184 try self.tokenize(gpa);
185
186 // It's possible the formatter resulted in fewer lines, in which case we have to make sure
187 // the cursors are still valid, and not beyond the end of the file.
188 self.ensureSelectionsAreValid();
189 }
190
191 std.log.debug("saving file: {s}", .{self.filename.items});
192
193 const file = try std.fs.cwd().createFile(self.filename.items, .{ .read = true, .truncate = true });
194 defer file.close();
195
196 var buffer: [1024]u8 = undefined;
197 var writer = file.writer(&buffer);
198
199 try writer.interface.writeAll(self.text.items);
200 try writer.interface.flush();
201}
202
203pub fn copySelectionsContent(self: *const Editor) void {
204 _ = self;
205 // TODO: implement.
206}
207
208/// Moves cursor before anchor for all selections.
209pub fn moveCursorBeforeAnchorForAllSelections(self: *Editor) void {
210 for (self.selections.items) |*s| {
211 if (!s.cursor.comesBefore(s.anchor)) {
212 const old_anchor = s.anchor;
213 s.anchor = s.cursor;
214 s.cursor = old_anchor;
215 }
216 }
217}
218
219/// Moves cursor after anchor for all selections.
220pub fn moveCursorAfterAnchorForAllSelections(self: *Editor) void {
221 for (self.selections.items) |*s| {
222 if (!s.cursor.comesAfter(s.anchor)) {
223 const old_anchor = s.anchor;
224 s.anchor = s.cursor;
225 s.cursor = old_anchor;
226 }
227 }
228}
229
230/// Moves each selection up one line. Selections will be collapsed to the cursor before they're
231/// moved.
232pub fn moveSelectionsUp(self: *Editor) void {
233 for (self.selections.items) |*s| {
234 if (s.cursor.row > 0) s.cursor.row -= 1;
235 s.anchor = s.cursor;
236 }
237
238 // TODO: Merge selections.
239}
240
241/// Moves each selection down one line. Selections will be collapsed to the cursor before they're
242/// moved.
243pub fn moveSelectionsDown(self: *Editor) void {
244 for (self.selections.items) |*s| {
245 var cursor = s.cursor;
246 cursor.row +|= 1;
247
248 // Keep the cursor where it is when we try to move beyond the last line.
249 if (cursor.row >= self.lineCount()) {
250 continue;
251 }
252
253 s.cursor = cursor;
254 s.anchor = s.cursor;
255 }
256
257 // TODO: Merge selections.
258}
259
260/// Moves each selection left one character. Selections will be collapsed to the cursor before
261/// they're moved.
262pub fn moveSelectionsLeft(self: *Editor) void {
263 for (self.selections.items) |*s| {
264 const line = self.getLine(s.cursor.row);
265
266 // Reset virtual column location if it extends beyond the length of the line.
267 if (s.cursor.col >= line.len) s.cursor.col = line.len;
268
269 if (s.cursor.col == 0 and s.cursor.row > 0) {
270 s.cursor.row -= 1;
271 const new_line = self.getLine(s.cursor.row);
272 s.cursor.col = new_line.len -| 1;
273 } else {
274 s.cursor.col -|= 1;
275 }
276
277 s.anchor = s.cursor;
278 }
279
280 // TODO: Merge selections.
281}
282
283/// Moves each selection right one character. Selections will be collapsed to the cursor before
284/// they're moved.
285pub fn moveSelectionsRight(self: *Editor) void {
286 for (self.selections.items) |*s| {
287 const line = self.getLine(s.cursor.row);
288
289 s.cursor.col +|= 1;
290
291 if (s.cursor.col > line.len -| 1 and s.cursor.row < self.lineCount() -| 1) {
292 s.cursor.row +|= 1;
293 s.cursor.col = 0;
294 } else if (s.cursor.row == self.lineCount() -| 1) {
295 // We want to allow the cursor to appear as if there's a new line at the end of a file
296 // so it can be moved beyond the end, so to speak.
297 const max_col = if (std.mem.endsWith(u8, line, "\n")) line.len else line.len +| 1;
298 if (s.cursor.col > max_col)
299 s.cursor.col = max_col;
300 }
301
302 s.anchor = s.cursor;
303 }
304
305 // TODO: Merge selections.
306}
307
308/// Collapses each selection to its cursor and moves it to the end of the line. If the cursor is
309/// already at the end of the line nothing will happen.
310pub fn moveSelectionsToEndOfLine(self: *Editor) void {
311 for (self.selections.items) |*s| {
312 // 1. Move the cursor to the end of its current line.
313
314 const cursor_line = self.getLine(s.cursor.row);
315 s.cursor.col = if (std.mem.endsWith(u8, cursor_line, "\n")) cursor_line.len -| 1 else cursor_line.len;
316
317 // 2. Turn the selection into a cursor by moving it to the same position as the cursor.
318
319 s.anchor = s.cursor;
320 }
321
322 // 3. Remove any selections in the same location.
323
324 for (0..self.selections.items.len -| 1) |idx| {
325 const s = self.selections.items[idx];
326 for (self.selections.items[idx + 1 ..], idx + 1..) |other, other_idx| {
327 // Just in case 2 or more consecutive selections happen to be in the same position
328 // we make sure this is a while loop so all the duplicates are removed.
329 while (s.eql(other)) _ = self.selections.orderedRemove(other_idx);
330 }
331 }
332}
333
334/// Collapses each selection to its cursor and moves it to the start of the line. If the cursor is
335/// already at the start of the line nothing will happen.
336pub fn moveSelectionsToStartOfLine(self: *Editor) void {
337 // 1. Collapse selections and move them to the start of the line.
338
339 for (self.selections.items) |*s| {
340 s.cursor.col = 0;
341 s.anchor = s.cursor;
342 }
343
344 // 2. Remove any selections in the same location.
345
346 for (0..self.selections.items.len -| 1) |idx| {
347 const s = self.selections.items[idx];
348 for (self.selections.items[idx + 1 ..], idx + 1..) |other, other_idx| {
349 // Just in case 2 or more consecutive selections happen to be in the same position
350 // we make sure this is a while loop so all the duplicates are removed.
351 while (s.eql(other)) _ = self.selections.orderedRemove(other_idx);
352 }
353 }
354}
355
356pub fn extendSelectionsRight(self: *Editor) void {
357 for (self.selections.items) |*s| {
358 s.cursor.col = s.cursor.col +| 1;
359
360 const line = self.getLine(s.cursor.row);
361
362 if (s.isCursor()) s.anchor.col = @min(s.anchor.col -| 1, line.len);
363
364 if (s.cursor.row >= self.lineCount() and s.cursor.col > line.len) {
365 s.cursor.col = line.len;
366 } else if (s.cursor.col > line.len) {
367 s.cursor.row = s.cursor.row +| 1;
368 s.cursor.col = 0;
369 }
370 }
371}
372
373pub fn extendSelectionsLeft(self: *Editor) void {
374 for (self.selections.items) |*s| {
375 if (s.cursor.col == 0 and s.cursor.row == 0) continue;
376
377 if (s.isCursor()) {
378 const line = self.getLine(s.cursor.row);
379 s.anchor.col = @min(s.anchor.col + 1, line.len);
380 }
381
382 if (s.cursor.col == 0) {
383 s.cursor.row -= 1;
384 const line = self.getLine(s.cursor.row);
385 s.cursor.col = line.len;
386 } else {
387 s.cursor.col -= 1;
388 }
389 }
390}
391
392pub fn extendSelectionsDown(self: *Editor) void {
393 for (self.selections.items) |*s| {
394 if (s.cursor.row >= self.lineCount()) continue;
395 s.cursor.row += 1;
396 }
397}
398
399pub fn extendSelectionsUp(self: *Editor) void {
400 for (self.selections.items) |*s| {
401 if (s.cursor.row == 0) continue;
402 s.cursor.row -= 1;
403 }
404}
405
406/// Selects the word that comes after each selection's cursor. Behavior varies depending on cursor
407/// location for each selection:
408///
409/// 1. If the cursor is at the start of a word, the selection will start from that position and go
410/// to the end of the word.
411/// 2. If the cursor is inside a word, the selection will start from that position and go to the end
412/// of the word.
413/// 3. If the cursor is at the end of a word, the selection will start at the beginning of the next
414/// word and go to the end of that word.
415/// 4. If the cursor is in the whitespace immediately before a word, the selection will start at the
416/// beginning of the next word and go to the end of that word.
417/// 5. If the cursor is in long whitespace (> 1 space), and not at the position right before the
418/// following word, the selection will start at the current cursor's position, and go to the
419/// beginning of the following word.
420pub fn selectNextWord(self: *Editor) void {
421 _ = self;
422 // TODO: implement.
423}
424
425/// Selects the word that comes before each selection's cursor. Behavior varies depending on cursor
426/// location for each selection:
427///
428/// 1. If the cursor is at the start of a word, the selection starts at that position and goes to
429/// the start of the preceding word.
430/// 2. If the cursor is inside a word, the selection will start from that position and go to the
431/// beginning of that word.
432/// 3. If the cursor is in the whitespace following a word, the selection will start from that
433/// position and go to the beginning of the preceding word.
434pub fn selectPreviousWord(self: *Editor) void {
435 _ = self;
436 // TODO: implement.
437}
438
439/// Deletes everything in front of each cursor until the start of each cursor's line.
440/// If cursor is already at the start of the line, it should delete the newline in front of it.
441pub fn deleteToStartOfLine(self: *Editor, gpa: std.mem.Allocator) !void {
442 std.log.debug("deleting to start of line", .{});
443 if (self.text.items.len == 0) return;
444
445 var selections: std.ArrayList(*Selection) = .empty;
446 defer selections.deinit(gpa);
447
448 try selections.ensureTotalCapacity(gpa, self.selections.items.len);
449
450 for (self.selections.items) |*s| selections.appendAssumeCapacity(s);
451
452 // Selections aren't guaranteed to be in order, so we sort them and make sure we delete
453 // from the back of the text first. That way we don't have to update all the cursors after each
454 // deletion.
455 std.mem.sort(*Selection, selections.items, {}, Selection.lessThanPtr);
456 std.mem.reverse(*Selection, selections.items);
457
458 for (selections.items) |s| {
459 const row = s.cursor.row;
460 const line = self.getLine(row);
461 const col = @min(s.cursor.col, line.len);
462
463 const line_start = self.line_indexes.items[row].toInt();
464
465 if (col == 0) continue;
466
467 var indexes: std.ArrayList(usize) = .empty;
468 defer indexes.deinit(gpa);
469
470 try indexes.ensureTotalCapacity(gpa, col);
471
472 for (line_start..line_start + col) |idx| indexes.appendAssumeCapacity(idx);
473
474 self.text.orderedRemoveMany(indexes.items);
475
476 s.cursor.col = 0;
477 s.anchor = s.cursor;
478 }
479
480 try self.updateLines(gpa);
481 try self.tokenize(gpa);
482}
483
484/// Returns the primary selection.
485pub fn getPrimarySelection(self: *const Editor) Selection {
486 std.debug.assert(self.selections.items.len > 0);
487 return self.selections.items[0];
488}
489
490/// Returns the requested 0-indexed line. Asserts that the line is a valid line number. The editor
491/// owns the memory returned, caller must not change or free the returned text.
492/// Includes the new line character at the end of the line.
493pub fn getLine(self: *const Editor, line: usize) []const u8 {
494 std.debug.assert(line < self.line_indexes.items.len);
495
496 const start_idx = self.line_indexes.items[line].toInt();
497 const end_idx = if (line < self.line_indexes.items.len -| 1)
498 self.line_indexes.items[line + 1].toInt()
499 else
500 self.text.items.len;
501
502 std.debug.assert(start_idx <= end_idx);
503
504 return self.text.items[start_idx..end_idx];
505}
506
507/// Returns all the text in the current file. Caller owns the memory and must free.
508pub fn getAllTextOwned(self: *const Editor, allocator: Allocator) ![]const u8 {
509 return try allocator.dupe(u8, self.text.items);
510}
511
512pub fn deleteInsideSelections(self: *Editor, allocator: Allocator) !void {
513 if (self.text.items.len == 0) return;
514
515 var selections: std.ArrayListUnmanaged(*Selection) = .empty;
516 defer selections.deinit(allocator);
517 for (self.selections.items) |*s| {
518 try selections.append(allocator, s);
519 }
520
521 // The selections aren't guaranteed to be in order, so we sort them and make sure we delete
522 // from the back of the text first. That way we don't have to update all the cursors after each
523 // deletion.
524 std.mem.sort(*Selection, selections.items, {}, Selection.lessThanPtr);
525 std.mem.reverse(*Selection, selections.items);
526
527 var deleted_text: usize = 0;
528
529 for (selections.items) |s| {
530 const as_range = s.toRange();
531 const from = toIndexPos(self.text.items, as_range.before()).toInt();
532 const to = toIndexPos(self.text.items, as_range.after()).toInt();
533
534 if (s.isCursor()) {
535 const del_pos = if (from >= self.text.items.len) from -| 1 else from;
536 const deleted = self.text.orderedRemove(del_pos);
537
538 if (deleted == '\n') {
539 s.cursor.row -|= 1;
540 // FIXME: it's probably inefficient to update all the line indexes here? But if we
541 // don't do it we can't use getLine() on the next line because the line
542 // indexes will be out-of-date.
543 try self.updateLines(allocator);
544 s.cursor.col = self.getLine(s.cursor.row).len;
545 s.anchor = s.cursor;
546 } else if (del_pos > self.text.items.len) {
547 s.cursor = self.toPos(.fromInt(self.text.items.len));
548 s.anchor = s.cursor;
549 }
550
551 deleted_text += 1;
552 } else {
553 self.text.replaceRangeAssumeCapacity(from, to - from + 1, "");
554
555 s.cursor = self.toPos(.fromInt(from -| deleted_text));
556 s.anchor = s.cursor;
557
558 deleted_text += to - from;
559 }
560 }
561
562 // Need to update the line positions since they will have moved in most cases.
563 try self.updateLines(allocator);
564
565 // Need to re-tokenize.
566 try self.tokenize(allocator);
567
568 // Remove duplicate selections.
569 for (self.selections.items, 0..) |selection, i| {
570 const is_last_selection = i == self.selections.items.len - 1;
571 if (is_last_selection) break;
572
573 var j = i + 1;
574 while (j < self.selections.items.len) {
575 const other_selection = self.selections.items[j];
576 if (selection.eql(other_selection)) {
577 _ = self.selections.swapRemove(j);
578
579 // It's essential to `continue` here so we check `selection` against whichever
580 // selection has now moved to be at index `j`.
581 continue;
582 }
583
584 j += 1;
585 }
586 }
587}
588
589/// Deletes the character immediately before the cursor.
590/// FIXME: Make this unicode aware.
591pub fn deleteCharacterBeforeCursors(self: *Editor, allocator: Allocator) !void {
592 // 1. Find all the cursor positions.
593
594 var cursors: std.ArrayListUnmanaged(IndexPos) = .empty;
595 defer cursors.deinit(allocator);
596
597 for (self.selections.items) |selection| {
598 var shouldAppend = true;
599 const selection_cursor = toIndexPos(self.text.items, selection.cursor);
600
601 // We make sure we only add one of each cursor position to make sure selections where the
602 // cursors are touching don't cause 2 deletes.
603 for (cursors.items) |cursor| {
604 if (cursor.eql(selection_cursor)) {
605 shouldAppend = false;
606 break;
607 }
608 }
609
610 if (shouldAppend) try cursors.append(allocator, selection_cursor);
611 }
612
613 // 2. The selections aren't guaranteed to be in order, so we sort them and make sure we delete
614 // from the back of the text first. That way we don't have to update all the cursors after
615 // each deletion.
616
617 std.mem.sort(IndexPos, cursors.items, {}, IndexPos.lessThan);
618 std.mem.reverse(IndexPos, cursors.items);
619
620 // 3. Delete character before each cursor.
621
622 const old_text = try allocator.dupe(u8, self.text.items);
623 defer allocator.free(old_text);
624
625 for (cursors.items) |cursor| {
626 // We can't delete before the first character in the file, so that's a noop.
627 if (cursor.toInt() == 0) continue;
628
629 _ = self.text.orderedRemove(cursor.toInt() -| 1);
630 }
631
632 // 4. Update state.
633
634 // Need to update the line positions since they will have moved in most cases.
635 try self.updateLines(allocator);
636
637 // Need to re-tokenize.
638 try self.tokenize(allocator);
639
640 // 5. Update selections.
641
642 // Each selection moves a different amount. The first selection in the file moves back 1
643 // character, the 2nd 2 characters, the 3rd 3 characters, and so on.
644 // Since selections aren't guaranteed to be in order we need a way to update them in order. We
645 // do this by constructing a map such that each selection receives an int describing how much it
646 // should move.
647 //
648 // Example:
649 // Selections: { 10, 40, 30 }
650 // File order map: { 1, 3, 2 } These values represent the order within the file in
651 // which these selections appear.
652 // Calculation: { 10-1, 40-3, 30-2}
653 // Result: { 9, 37, 28 }
654
655 var orderMap: std.ArrayListUnmanaged(usize) = .empty;
656 defer orderMap.deinit(allocator);
657
658 // FIXME: n^2 complexity on this is terrible, but it was the easy way to implement this. Use an
659 // actual sorting algorithm to do this faster, jeez. Although tbf, this is unlikely to
660 // be a real bottleneck, so maybe this is good enough. Integer math be fast like that.
661 for (self.selections.items) |current| {
662 var movement: usize = 1;
663 for (self.selections.items) |other| {
664 if (current.strictEql(other)) continue;
665
666 // We move the cursor 1 additional character for every cursor that comes before it.
667 if (other.cursor.comesBefore(current.cursor)) movement += 1;
668 }
669
670 try orderMap.append(allocator, movement);
671 }
672
673 // Need to move the cursors. Since each selection will have resulted in a deleted character we
674 // need to move each cursor back equal to it's position in the file.
675 for (self.selections.items, orderMap.items) |*selection, movement| {
676 const cursor = toIndexPos(old_text, selection.cursor);
677 const anchor = toIndexPos(old_text, selection.anchor);
678
679 if (selection.isCursor()) {
680 // FIXME: Make this unicode-aware.
681 selection.cursor = self.toPos(.fromInt(cursor.toInt() -| movement));
682 selection.anchor = selection.cursor;
683 continue;
684 }
685
686 // Move both by `movement` if the cursor comes before the anchor in the selection.
687 // Otherwise move just the cursor.
688 if (selection.cursor.comesBefore(selection.anchor)) {
689 // FIXME: Make this unicode-aware.
690 selection.cursor = self.toPos(.fromInt(cursor.toInt() -| movement));
691 selection.anchor = self.toPos(.fromInt(anchor.toInt() -| movement));
692 } else {
693 // FIXME: Make this unicode-aware.
694 selection.cursor = self.toPos(.fromInt(cursor.toInt() -| movement));
695
696 const is_first_selection_in_file = movement == 1;
697
698 // Anything but the first selection has to move both the anchor and the cursor.
699 if (!is_first_selection_in_file) {
700 // Since the anchor comes before the cursor in this case we can't include the
701 // shift generated by removing the character before the cursor in this selection,
702 // so we subtract 1 from the movement.
703 // FIXME: Make this unicode aware.
704 const anchor_movement = movement -| 1;
705 selection.anchor = self.toPos(.fromInt(anchor.toInt() -| anchor_movement));
706 }
707
708 // Collapse the selection into a cursor if the cursor moved beyond the anchor.
709 if (selection.cursor.comesBefore(selection.anchor)) selection.anchor = selection.cursor;
710 }
711 }
712
713 // 6. Remove duplicate selections.
714
715 for (self.selections.items, 0..) |selection, i| {
716 const is_last_selection = i == self.selections.items.len - 1;
717 if (is_last_selection) break;
718
719 var j = i + 1;
720 while (j < self.selections.items.len) {
721 const other_selection = self.selections.items[j];
722 if (selection.eql(other_selection)) {
723 _ = self.selections.swapRemove(j);
724
725 // It's essential to `continue` here so we check `selection` against whichever
726 // selection has now moved to be at index `j`.
727 continue;
728 }
729
730 j += 1;
731 }
732 }
733}
734
735pub fn startNewLineBelow(self: *Editor, gpa: std.mem.Allocator) !void {
736 self.moveSelectionsToEndOfLine();
737 try self.insertTextAtCursors(gpa, "\n");
738}
739
740pub fn startNewLineAbove(self: *Editor, gpa: std.mem.Allocator) !void {
741 self.moveSelectionsUp();
742 self.moveSelectionsToEndOfLine();
743 try self.insertTextAtCursors(gpa, "\n");
744}
745
746pub fn lineCount(self: *const Editor) usize {
747 return self.line_indexes.items.len;
748}
749
750/// Inserts the provided text at the cursor location for each selection. Selectiosn will not be
751/// cleared. If the anchor comes before the cursor the selection will expand to include the newly
752/// inserted text. If the anchor comes after the cursor the text will be inserted before the
753/// selection, and the selection moved with the new content such that it will still select the same
754/// text.
755pub fn insertTextAtCursors(self: *Editor, allocator: Allocator, text: []const u8) !void {
756 for (self.selections.items) |*s| {
757 std.debug.assert(s.cursor.row <= self.lineCount());
758
759 // Since we're inserting we want to clamp the cursor to the line length.
760 const cursor_line = cursor_line: {
761 const line = self.getLine(s.cursor.row);
762
763 if (std.mem.endsWith(u8, line, "\n")) break :cursor_line line[0..line.len -| 1];
764 break :cursor_line line;
765 };
766 const new_col = @min(cursor_line.len, s.cursor.col);
767 if (s.isCursor()) {
768 s.cursor.col = new_col;
769 s.anchor = s.cursor;
770 } else {
771 s.cursor.col = new_col;
772 }
773
774 const cursor = toIndexPos(self.text.items, s.cursor);
775
776 // Insert text.
777 try self.text.insertSlice(allocator, cursor.toInt(), text);
778
779 // Check inserted text for new lines so we can handle them properly.
780 const num_new_lines = std.mem.count(u8, text, "\n");
781 const last_new_line = std.mem.lastIndexOfScalar(u8, text, '\n');
782
783 // Update other selection positions.
784 // NOTE: We've asserted that no selections overlap, so we stick to that assumption here.
785 for (self.selections.items) |*other| {
786 if (s.eql(other.*)) continue;
787
788 if (s.comesBefore(other.*)) {
789 other.cursor = .{
790 .row = other.cursor.row +| num_new_lines,
791 .col = if (last_new_line) |i| text[i +| 1..].len else other.cursor.col +| text.len,
792 };
793 other.anchor = .{
794 .row = other.anchor.row +| num_new_lines,
795 .col = if (last_new_line) |i| text[i +| 1..].len else other.anchor.col +| text.len,
796 };
797 }
798 }
799
800 // Update this selection's positions.
801 if (s.isCursor()) {
802 s.cursor = .{
803 .row = s.cursor.row +| num_new_lines,
804 .col = if (last_new_line) |i| text[i +| 1..].len else s.cursor.col +| text.len,
805 };
806 s.anchor = s.cursor;
807 } else if (s.cursor.comesBefore(s.anchor)) {
808 s.cursor = .{
809 .row = s.cursor.row +| num_new_lines,
810 .col = if (last_new_line) |i| text[i +| 1..].len else s.cursor.col +| text.len,
811 };
812 s.anchor = .{
813 .row = s.anchor.row +| num_new_lines,
814 .col = if (last_new_line) |i| text[i +| 1..].len else s.anchor.col +| text.len,
815 };
816 } else {
817 s.cursor = .{
818 .row = s.cursor.row +| num_new_lines,
819 .col = if (last_new_line) |i| text[i +| 1..].len else s.cursor.col +| text.len,
820 };
821 }
822 }
823
824 std.debug.assert(!self.hasOverlappingSelections());
825
826 try self.updateLines(allocator);
827}
828
829/// Appends the provided Selection to the current list of selections. If the new selection overlaps
830/// an existing selection they will be merged.
831pub fn appendSelection(self: *Editor, allocator: Allocator, new_selection: Selection) !void {
832 // 1. Append the selection.
833
834 try self.selections.append(allocator, new_selection);
835
836 // 2. Merge any overlapping selections.
837
838 var outer: usize = 0;
839 outer_loop: while (outer < self.selections.items.len) {
840 const before: Selection = self.selections.items[outer];
841
842 // Go through the remaining selections and merge any that overlap with the current
843 // selection.
844 var inner = outer +| 1;
845 while (inner < self.selections.items.len) : (inner += 1) {
846 const after = self.selections.items[inner];
847 if (before.hasOverlap(after)) {
848 self.selections.items[outer] = .merge(before, after);
849 _ = self.selections.swapRemove(inner);
850 continue :outer_loop;
851 }
852 }
853
854 outer += 1;
855 }
856
857 std.debug.assert(!self.hasOverlappingSelections());
858}
859
860/// Tokenizes the text.
861/// TODO: Have language extensions implement this and call those functions when relevant.
862fn tokenize(self: *Editor, allocator: Allocator) !void {
863 self.tokens.clearRetainingCapacity();
864 try self.tokens.append(allocator, .{
865 .pos = .{ .row = 0, .col = 0 },
866 .text = self.text.items,
867 .type = .Text,
868 });
869}
870
871/// Updates the indeces for the start of each line in the text.
872fn updateLines(self: *Editor, allocator: Allocator) !void {
873 self.line_indexes.clearRetainingCapacity();
874 try self.line_indexes.append(allocator, .fromInt(0));
875
876 // NOTE: We start counting from 1 because we consider the start of a line to be **after** a
877 // newline character, not before.
878 for (self.text.items, 1..) |char, i| {
879 if (char == '\n') try self.line_indexes.append(allocator, .fromInt(i));
880 }
881
882 self.longest_line = longest_line: {
883 if (self.line_indexes.items.len == 1) break :longest_line self.text.items.len;
884
885 var max: usize = 0;
886 var prev_line_idx: usize = 0;
887
888 for (1..self.line_indexes.items.len) |idx| {
889 defer prev_line_idx = idx;
890
891 const prev_idx = self.line_indexes.items[prev_line_idx].toInt();
892 const curr_idx = self.line_indexes.items[idx].toInt();
893
894 max = @max(max, curr_idx - prev_idx);
895 }
896
897 break :longest_line max;
898 };
899}
900
901fn ensureSelectionsAreValid(self: *Editor) void {
902 for (self.selections.items) |*s| {
903 if (s.cursor.row >= self.lineCount()) s.cursor.row = self.lineCount() -| 1;
904 if (s.anchor.row >= self.lineCount()) s.anchor.row = self.lineCount() -| 1;
905 }
906}
907
908/// Converts the provided `Pos` object to a `CoordinatePos`.
909pub fn toPos(self: *Editor, pos: IndexPos) Pos {
910 // 1. Assert that the provided position is valid.
911
912 // NOTE: The position after the last character in a file is a valid position which is why we
913 // must check for equality against the text length.
914 std.debug.assert(pos.toInt() <= self.text.items.len);
915
916 // 2. Find the row indicated by the byte-level position.
917
918 const row: usize = row: {
919 var row: usize = 0;
920 for (self.line_indexes.items, 0..) |lineStartIndex, i| {
921 // If we're past the provided byte-level position then we know the previous position was the
922 // correct row_index.
923 if (lineStartIndex.comesAfter(pos)) break :row row;
924 row = i;
925 }
926
927 // If haven't found the position in the loop, we can safely use the last line.
928 break :row self.line_indexes.items.len -| 1;
929 };
930
931 // 3. Use the byte-level position of the start of the row to calculate the column of the
932 // provided position.
933
934 const startOfRowIndex: IndexPos = self.line_indexes.items[row];
935
936 return .{ .row = row, .col = pos.toInt() -| startOfRowIndex.toInt() };
937}
938
939/// Converts the provided `CoordinatePos` object to a `Pos`.
940pub fn toIndexPos(text: []const u8, pos: Pos) IndexPos {
941 // 1. Assert that the provided position is valid.
942
943 const lines = std.mem.count(u8, text, "\n") + 1;
944 std.debug.assert(pos.row < lines);
945
946 if (pos.row == 0) {
947 const end_of_line = if (std.mem.indexOfScalar(u8, text, '\n')) |idx| idx + 1 else text.len;
948 return .fromInt(@min(pos.col, end_of_line));
949 }
950
951 var line: usize = 0;
952 var i: usize = 0;
953 while (i < text.len) : (i += 1) {
954 if (text[i] == '\n') {
955 line += 1;
956
957 if (line == pos.row) {
958 const start_of_line = i + 1;
959 const end_of_line = if (std.mem.indexOfScalar(u8, text[start_of_line..], '\n')) |idx|
960 idx + 1
961 else
962 text[i..].len;
963 return .fromInt(start_of_line + @min(pos.col, end_of_line));
964 }
965 }
966 }
967
968 return .fromInt(text.len);
969}
970
971/// Returns `true` if any of the current selections overlap, `false` otherwise.
972fn hasOverlappingSelections(self: *const Editor) bool {
973 for (0..self.selections.items.len -| 1) |i| {
974 const s = self.selections.items[i];
975
976 for (self.selections.items[i + 1 ..]) |other| {
977 if (s.hasOverlap(other)) return true;
978 }
979 }
980
981 return false;
982}
983
984const talloc = std.testing.allocator;
985
986test init {
987 var editor = try Editor.init(talloc);
988 defer editor.deinit(talloc);
989}
990
991test toPos {
992 var editor = try Editor.init(talloc);
993 defer editor.deinit(talloc);
994
995 try editor.text.appendSlice(talloc, "012\n456\n890\n");
996 // Updating the lines is required to properly calculate the byte-level cursor positions.
997 try editor.updateLines(talloc);
998
999 try std.testing.expectEqual(Pos{ .row = 0, .col = 0 }, editor.toPos(.fromInt(0)));
1000 try std.testing.expectEqual(Pos{ .row = 0, .col = 2 }, editor.toPos(.fromInt(2)));
1001 try std.testing.expectEqual(Pos{ .row = 0, .col = 3 }, editor.toPos(.fromInt(3)));
1002
1003 try std.testing.expectEqual(Pos{ .row = 1, .col = 0 }, editor.toPos(.fromInt(4)));
1004 try std.testing.expectEqual(Pos{ .row = 1, .col = 1 }, editor.toPos(.fromInt(5)));
1005 try std.testing.expectEqual(Pos{ .row = 1, .col = 3 }, editor.toPos(.fromInt(7)));
1006
1007 try std.testing.expectEqual(Pos{ .row = 2, .col = 0 }, editor.toPos(.fromInt(8)));
1008 try std.testing.expectEqual(Pos{ .row = 2, .col = 2 }, editor.toPos(.fromInt(10)));
1009 try std.testing.expectEqual(Pos{ .row = 2, .col = 3 }, editor.toPos(.fromInt(11)));
1010
1011 try std.testing.expectEqual(Pos{ .row = 3, .col = 0 }, editor.toPos(.fromInt(12)));
1012}
1013
1014test toIndexPos {
1015 const text = "012\n456\n890\n";
1016
1017 try std.testing.expectEqual(
1018 (IndexPos.fromInt(0)),
1019 Editor.toIndexPos(text, Pos{ .row = 0, .col = 0 }),
1020 );
1021 try std.testing.expectEqual(
1022 (IndexPos.fromInt(2)),
1023 Editor.toIndexPos(text, Pos{ .row = 0, .col = 2 }),
1024 );
1025 try std.testing.expectEqual(
1026 (IndexPos.fromInt(3)),
1027 Editor.toIndexPos(text, Pos{ .row = 0, .col = 3 }),
1028 );
1029
1030 try std.testing.expectEqual(
1031 (IndexPos.fromInt(4)),
1032 Editor.toIndexPos(text, Pos{ .row = 1, .col = 0 }),
1033 );
1034 try std.testing.expectEqual(
1035 (IndexPos.fromInt(5)),
1036 Editor.toIndexPos(text, Pos{ .row = 1, .col = 1 }),
1037 );
1038 try std.testing.expectEqual(
1039 (IndexPos.fromInt(7)),
1040 Editor.toIndexPos(text, Pos{ .row = 1, .col = 3 }),
1041 );
1042
1043 try std.testing.expectEqual(
1044 (IndexPos.fromInt(8)),
1045 Editor.toIndexPos(text, Pos{ .row = 2, .col = 0 }),
1046 );
1047 try std.testing.expectEqual(
1048 (IndexPos.fromInt(10)),
1049 Editor.toIndexPos(text, Pos{ .row = 2, .col = 2 }),
1050 );
1051 try std.testing.expectEqual(
1052 (IndexPos.fromInt(11)),
1053 Editor.toIndexPos(text, Pos{ .row = 2, .col = 3 }),
1054 );
1055
1056 try std.testing.expectEqual(
1057 (IndexPos.fromInt(12)),
1058 Editor.toIndexPos(text, Pos{ .row = 3, .col = 0 }),
1059 );
1060}
1061
1062test lineCount {
1063 var editor = try Editor.init(talloc);
1064 defer editor.deinit(talloc);
1065
1066 try std.testing.expectEqual(1, editor.lineCount());
1067
1068 try editor.text.appendSlice(talloc, "012");
1069 try editor.updateLines(talloc);
1070
1071 try std.testing.expectEqual(1, editor.lineCount());
1072
1073 try editor.text.appendSlice(talloc, "\n345\n");
1074 try editor.updateLines(talloc);
1075
1076 try std.testing.expectEqual(3, editor.lineCount());
1077
1078 try editor.text.appendSlice(talloc, "678");
1079 try editor.updateLines(talloc);
1080
1081 try std.testing.expectEqual(3, editor.lineCount());
1082
1083 try editor.text.appendSlice(talloc, "\n\n");
1084 try editor.updateLines(talloc);
1085
1086 try std.testing.expectEqual(5, editor.lineCount());
1087}
1088
1089test insertTextAtCursors {
1090 var editor: Editor = try .init(talloc);
1091 defer editor.deinit(talloc);
1092
1093 // 1. Insertion happens at initial cursor to begin with.
1094
1095 try editor.insertTextAtCursors(talloc, "hello");
1096 // Cursor is now at the end of the text.
1097 try std.testing.expectEqualStrings(
1098 "hello",
1099 editor.text.items,
1100 );
1101 try std.testing.expectEqualSlices(
1102 Selection,
1103 &.{.createCursor(.{ .row = 0, .col = 5 })},
1104 editor.selections.items,
1105 );
1106
1107 // 2. Adding a cursor at the start makes insertion happen at both cursors.
1108
1109 try editor.appendSelection(talloc, .init);
1110 try editor.insertTextAtCursors(talloc, ", world!");
1111 try std.testing.expectEqualStrings(
1112 ", world!hello, world!",
1113 editor.text.items,
1114 );
1115 try std.testing.expectEqualSlices(
1116 Selection,
1117 &.{
1118 .createCursor(.{ .row = 0, .col = 21 }),
1119 .createCursor(.{ .row = 0, .col = 8 }),
1120 },
1121 editor.selections.items,
1122 );
1123
1124 // 3. Adding a selection in the middle causes text to appear in the right places.
1125
1126 try editor.appendSelection(talloc, .{
1127 .anchor = .{ .row = 0, .col = 12 },
1128 .cursor = .{ .row = 0, .col = 15 },
1129 });
1130 try editor.appendSelection(talloc, .{
1131 .anchor = .{ .row = 0, .col = 19 },
1132 .cursor = .{ .row = 0, .col = 17 },
1133 });
1134 try editor.insertTextAtCursors(talloc, "abc");
1135 try std.testing.expectEqualStrings(
1136 ", world!abchello, abcwoabcrld!abc",
1137 editor.text.items,
1138 );
1139 try std.testing.expectEqualSlices(
1140 Selection,
1141 &.{
1142 .createCursor(.{ .row = 0, .col = 33 }),
1143 .createCursor(.{ .row = 0, .col = 11 }),
1144 .{ .anchor = .{ .row = 0, .col = 15 }, .cursor = .{ .row = 0, .col = 21 } },
1145 .{ .anchor = .{ .row = 0, .col = 28 }, .cursor = .{ .row = 0, .col = 26 } },
1146 },
1147 editor.selections.items,
1148 );
1149
1150 // Reset.
1151 editor.deinit(talloc);
1152 editor = try .init(talloc);
1153
1154 // 4. New lines handled correctly.
1155
1156 try editor.insertTextAtCursors(talloc, "\n");
1157 try std.testing.expectEqualStrings(
1158 "\n",
1159 editor.text.items,
1160 );
1161 try std.testing.expectEqualSlices(
1162 Selection,
1163 &.{.createCursor(.{ .row = 1, .col = 0 })},
1164 editor.selections.items,
1165 );
1166
1167 try editor.insertTextAtCursors(talloc, "\n");
1168 try std.testing.expectEqualStrings(
1169 "\n\n",
1170 editor.text.items,
1171 );
1172 try std.testing.expectEqualSlices(
1173 Selection,
1174 &.{.createCursor(.{ .row = 2, .col = 0 })},
1175 editor.selections.items,
1176 );
1177
1178 try editor.insertTextAtCursors(talloc, "a\n");
1179 try std.testing.expectEqualStrings(
1180 "\n\na\n",
1181 editor.text.items,
1182 );
1183 try std.testing.expectEqualSlices(
1184 Selection,
1185 &.{.createCursor(.{ .row = 3, .col = 0 })},
1186 editor.selections.items,
1187 );
1188
1189 try editor.insertTextAtCursors(talloc, "\n\n\n");
1190 try std.testing.expectEqualStrings(
1191 "\n\na\n\n\n\n",
1192 editor.text.items,
1193 );
1194 try std.testing.expectEqualSlices(
1195 Selection,
1196 &.{.createCursor(.{ .row = 6, .col = 0 })},
1197 editor.selections.items,
1198 );
1199
1200 // Reset.
1201 editor.deinit(talloc);
1202 editor = try .init(talloc);
1203
1204 // 5. Virtual columns clamped correctly to the end of the line.
1205
1206 try editor.text.appendSlice(talloc, "123\n456\n789\n");
1207 try editor.updateLines(talloc);
1208
1209 editor.selections.clearRetainingCapacity();
1210 try editor.appendSelection(talloc, .createCursor(.{ .row = 0, .col = 5 }));
1211
1212 try editor.insertTextAtCursors(talloc, "abc");
1213
1214 try std.testing.expectEqualStrings("123abc\n456\n789\n", editor.text.items);
1215 try std.testing.expectEqualSlices(
1216 Selection,
1217 &.{.createCursor(.{ .row = 0, .col = 6 })},
1218 editor.selections.items,
1219 );
1220
1221 editor.selections.clearRetainingCapacity();
1222 try editor.appendSelection(talloc, .createCursor(.{ .row = 3, .col = 5 }));
1223
1224 try editor.insertTextAtCursors(talloc, "abc");
1225
1226 try std.testing.expectEqualStrings("123abc\n456\n789\nabc", editor.text.items);
1227 try std.testing.expectEqualSlices(
1228 Selection,
1229 &.{.createCursor(.{ .row = 3, .col = 3 })},
1230 editor.selections.items,
1231 );
1232}
1233
1234test appendSelection {
1235 var editor = try Editor.init(talloc);
1236 defer editor.deinit(talloc);
1237
1238 editor.selections.clearRetainingCapacity();
1239
1240 // 1. Inserting a selection that would cause an existing selection to expand into a different,
1241 // pre-existing selection should merge all selections.
1242
1243 // 1 | 01^2|345^67|89
1244 // ^ ^ insertion
1245 // Result:
1246 //
1247 // 1 | 01^2345678|9
1248
1249 try editor.appendSelection(
1250 talloc,
1251 .{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 0, .col = 3 } },
1252 );
1253 try editor.appendSelection(
1254 talloc,
1255 .{ .anchor = .{ .row = 0, .col = 6 }, .cursor = .{ .row = 0, .col = 8 } },
1256 );
1257
1258 try std.testing.expect(!editor.hasOverlappingSelections());
1259
1260 try editor.appendSelection(talloc, .{
1261 .anchor = .{ .row = 0, .col = 3 },
1262 .cursor = .{ .row = 0, .col = 9 },
1263 });
1264
1265 try std.testing.expect(!editor.hasOverlappingSelections());
1266 try std.testing.expectEqualSlices(
1267 Selection,
1268 &.{.{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 0, .col = 9 } }},
1269 editor.selections.items,
1270 );
1271
1272 editor.selections.clearRetainingCapacity();
1273
1274 // 1 | 01^2|345^67|89
1275 // ^ ^ insertion
1276 // Result:
1277 //
1278 // 1 | 01^2|345^678|9
1279
1280 try editor.appendSelection(talloc, .{
1281 .anchor = .{ .row = 0, .col = 2 },
1282 .cursor = .{ .row = 0, .col = 3 },
1283 });
1284 try editor.appendSelection(talloc, .{
1285 .anchor = .{ .row = 0, .col = 6 },
1286 .cursor = .{ .row = 0, .col = 8 },
1287 });
1288
1289 try std.testing.expect(!editor.hasOverlappingSelections());
1290
1291 try editor.appendSelection(talloc, .{
1292 .anchor = .{ .row = 0, .col = 7 },
1293 .cursor = .{ .row = 0, .col = 9 },
1294 });
1295
1296 try std.testing.expect(!editor.hasOverlappingSelections());
1297 try std.testing.expectEqualSlices(
1298 Selection,
1299 &.{
1300 .{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 0, .col = 3 } },
1301 .{ .anchor = .{ .row = 0, .col = 6 }, .cursor = .{ .row = 0, .col = 9 } },
1302 },
1303 editor.selections.items,
1304 );
1305}
1306
1307fn testOnly_resetEditor(editor: *Editor, allocator: Allocator) !void {
1308 editor.text.clearRetainingCapacity();
1309 try editor.text.appendSlice(allocator, "012\n456\n890\n");
1310 try editor.updateLines(allocator);
1311 editor.selections.clearRetainingCapacity();
1312}
1313
1314test deleteCharacterBeforeCursors {
1315 var editor = try Editor.init(talloc);
1316 defer editor.deinit(talloc);
1317
1318 try testOnly_resetEditor(&editor, talloc);
1319
1320 // Legend for selections:
1321 // * A cursor (0-width selection): +
1322 // * Anchor of a selection: ^
1323 // * Cursor of a selection: |
1324
1325 // == Cursors == //
1326
1327 // 1. Deleting from the first position is a noop.
1328
1329 // Before:
1330 //
1331 // 1 | +012
1332 // 2 | 456
1333 // 3 | 890
1334 // 4 |
1335 //
1336 // After:
1337 //
1338 // 1 | +012
1339 // 2 | 456
1340 // 3 | 890
1341 // 4 |
1342
1343 try editor.selections.append(talloc, .init);
1344
1345 try editor.deleteCharacterBeforeCursors(talloc);
1346
1347 try std.testing.expectEqualStrings("012\n456\n890\n", editor.text.items);
1348 try std.testing.expectEqualSlices(
1349 Selection,
1350 &.{.init},
1351 editor.selections.items,
1352 );
1353
1354 try testOnly_resetEditor(&editor, talloc);
1355
1356 // 2. Deleting from the back deletes the last character.
1357
1358 // Before:
1359 //
1360 // 1 | 012
1361 // 2 | 456
1362 // 3 | 890
1363 // 4 | +
1364 //
1365 // After:
1366 //
1367 // 1 | 012
1368 // 2 | 456
1369 // 3 | 890+
1370
1371 try editor.selections.append(talloc, .createCursor(.{ .row = 3, .col = 0 }));
1372
1373 try editor.deleteCharacterBeforeCursors(talloc);
1374
1375 try std.testing.expectEqualStrings("012\n456\n890", editor.text.items);
1376 try std.testing.expect(3 == editor.lineCount());
1377 try std.testing.expectEqualSlices(
1378 Selection,
1379 &.{.createCursor(.{ .row = 2, .col = 3 })},
1380 editor.selections.items,
1381 );
1382
1383 try testOnly_resetEditor(&editor, talloc);
1384
1385 // 3. Deleting the first character only deletes the first character.
1386
1387 // Before:
1388 //
1389 // 1 | 0+12
1390 // 2 | 456
1391 // 3 | 890
1392 // 4 |
1393 //
1394 // After:
1395 //
1396 // 1 | +12
1397 // 2 | 456
1398 // 3 | 890
1399 // 4 |
1400
1401 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 1 }));
1402
1403 try editor.deleteCharacterBeforeCursors(talloc);
1404
1405 try std.testing.expectEqualStrings("12\n456\n890\n", editor.text.items);
1406 try std.testing.expectEqualSlices(
1407 Selection,
1408 &.{.init},
1409 editor.selections.items,
1410 );
1411
1412 try testOnly_resetEditor(&editor, talloc);
1413
1414 // 4. Deleting in multiple places.
1415
1416 // Before:
1417 //
1418 // 1 | 012+
1419 // 2 | 456
1420 // 3 | +890
1421 // 4 |
1422 //
1423 // After:
1424 //
1425 // 1 | 01+
1426 // 2 | 456+890
1427 // 3 |
1428
1429 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1430 try editor.selections.append(talloc, .createCursor(.{ .row = 2, .col = 0 }));
1431
1432 try editor.deleteCharacterBeforeCursors(talloc);
1433
1434 try std.testing.expectEqualStrings("01\n456890\n", editor.text.items);
1435 try std.testing.expectEqualSlices(
1436 Selection,
1437 &.{
1438 .createCursor(.{ .row = 0, .col = 2 }),
1439 .createCursor(.{ .row = 1, .col = 3 }),
1440 },
1441 editor.selections.items,
1442 );
1443
1444 try testOnly_resetEditor(&editor, talloc);
1445
1446 // 5. Cursors should merge when they reach the start of the file.
1447
1448 // Before:
1449 //
1450 // 1 | 0+1+2
1451 // 2 | 456
1452 // 3 | 890
1453 // 4 |
1454 //
1455 // After:
1456 //
1457 // 1 | +2
1458 // 2 | 456
1459 // 3 | 890
1460 // 4 |
1461
1462 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 1 }));
1463 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 2 }));
1464
1465 try editor.deleteCharacterBeforeCursors(talloc);
1466
1467 try std.testing.expectEqualStrings("2\n456\n890\n", editor.text.items);
1468 try std.testing.expectEqualSlices(
1469 Selection,
1470 &.{.init},
1471 editor.selections.items,
1472 );
1473
1474 try testOnly_resetEditor(&editor, talloc);
1475
1476 // Before:
1477 //
1478 // 1 | 0+12+
1479 // 2 | 456
1480 // 3 | 890
1481 // 4 |
1482 //
1483 // After:
1484 //
1485 // 1 | +1+
1486 // 2 | 456
1487 // 3 | 890
1488 // 4 |
1489
1490 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 1 }));
1491 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1492
1493 try editor.deleteCharacterBeforeCursors(talloc);
1494
1495 try std.testing.expectEqualStrings("1\n456\n890\n", editor.text.items);
1496 try std.testing.expectEqualSlices(
1497 Selection,
1498 &.{
1499 .init,
1500 .createCursor(.{ .row = 0, .col = 1 }),
1501 },
1502 editor.selections.items,
1503 );
1504
1505 // Before:
1506 //
1507 // 1 | +1+
1508 // 2 | 456
1509 // 3 | 890
1510 // 4 |
1511 //
1512 // After:
1513 //
1514 // 1 | +
1515 // 2 | 456
1516 // 3 | 890
1517 // 4 |
1518
1519 try editor.deleteCharacterBeforeCursors(talloc);
1520
1521 try std.testing.expectEqualStrings("\n456\n890\n", editor.text.items);
1522 try std.testing.expectEqualSlices(
1523 Selection,
1524 &.{.init},
1525 editor.selections.items,
1526 );
1527
1528 try testOnly_resetEditor(&editor, talloc);
1529
1530 // 6. Cursors should merge when they reach the same index after a deletion.
1531
1532 // Before:
1533 //
1534 // 1 | 01+2+
1535 // 2 | 45+6
1536 // 3 | 890
1537 // 4 |
1538 //
1539 // After:
1540 //
1541 // 1 | 0+
1542 // 2 | 4+6
1543 // 3 | 890
1544 // 4 |
1545
1546 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 2 }));
1547 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1548 try editor.selections.append(talloc, .createCursor(.{ .row = 1, .col = 2 }));
1549
1550 try editor.deleteCharacterBeforeCursors(talloc);
1551
1552 try std.testing.expectEqualStrings("0\n46\n890\n", editor.text.items);
1553 try std.testing.expectEqualSlices(
1554 Selection,
1555 &.{
1556 .createCursor(.{ .row = 0, .col = 1 }),
1557 .createCursor(.{ .row = 1, .col = 1 }),
1558 },
1559 editor.selections.items,
1560 );
1561
1562 try testOnly_resetEditor(&editor, talloc);
1563
1564 // Same test, but the selections appear in a different order.
1565
1566 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 2 }));
1567 try editor.selections.append(talloc, .createCursor(.{ .row = 1, .col = 2 }));
1568 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1569
1570 try editor.deleteCharacterBeforeCursors(talloc);
1571
1572 try std.testing.expectEqualStrings("0\n46\n890\n", editor.text.items);
1573 try std.testing.expectEqualSlices(
1574 Selection,
1575 &.{
1576 .createCursor(.{ .row = 0, .col = 1 }),
1577 .createCursor(.{ .row = 1, .col = 1 }),
1578 },
1579 editor.selections.items,
1580 );
1581
1582 try testOnly_resetEditor(&editor, talloc);
1583
1584 // Same test, but the selections appear in a different order.
1585
1586 try editor.selections.append(talloc, .createCursor(.{ .row = 1, .col = 2 }));
1587 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1588 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 2 }));
1589
1590 try editor.deleteCharacterBeforeCursors(talloc);
1591
1592 try std.testing.expectEqualStrings("0\n46\n890\n", editor.text.items);
1593 try std.testing.expectEqualSlices(
1594 Selection,
1595 &.{
1596 .createCursor(.{ .row = 1, .col = 1 }),
1597 .createCursor(.{ .row = 0, .col = 1 }),
1598 },
1599 editor.selections.items,
1600 );
1601
1602 try testOnly_resetEditor(&editor, talloc);
1603
1604 // Same test, but the selections appear in a different order.
1605
1606 try editor.selections.append(talloc, .createCursor(.{ .row = 1, .col = 2 }));
1607 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 2 }));
1608 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1609
1610 try editor.deleteCharacterBeforeCursors(talloc);
1611
1612 try std.testing.expectEqualStrings("0\n46\n890\n", editor.text.items);
1613 try std.testing.expectEqualSlices(
1614 Selection,
1615 &.{
1616 .createCursor(.{ .row = 1, .col = 1 }),
1617 .createCursor(.{ .row = 0, .col = 1 }),
1618 },
1619 editor.selections.items,
1620 );
1621
1622 try testOnly_resetEditor(&editor, talloc);
1623
1624 // Same test, but the selections appear in a different order.
1625
1626 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 2 }));
1627 try editor.selections.append(talloc, .createCursor(.{ .row = 0, .col = 3 }));
1628 try editor.selections.append(talloc, .createCursor(.{ .row = 1, .col = 2 }));
1629
1630 try editor.deleteCharacterBeforeCursors(talloc);
1631
1632 try std.testing.expectEqualStrings("0\n46\n890\n", editor.text.items);
1633 try std.testing.expectEqualSlices(
1634 Selection,
1635 &.{
1636 .createCursor(.{ .row = 0, .col = 1 }),
1637 .createCursor(.{ .row = 1, .col = 1 }),
1638 },
1639 editor.selections.items,
1640 );
1641
1642 try testOnly_resetEditor(&editor, talloc);
1643
1644 // == Selections == //
1645
1646 // 6. Deleting when the cursor comes after the anchor should shrink the selection.
1647
1648 // Before:
1649 //
1650 // 1 | 01^2
1651 // 2 | 456|
1652 // 3 | 890
1653 // 4 |
1654 //
1655 // After:
1656 //
1657 // 1 | 01^2
1658 // 2 | 45|
1659 // 3 | 890
1660 // 4 |
1661
1662 try editor.selections.append(talloc, .{
1663 .anchor = .{ .row = 0, .col = 2 },
1664 .cursor = .{ .row = 1, .col = 3 },
1665 });
1666
1667 try editor.deleteCharacterBeforeCursors(talloc);
1668
1669 try std.testing.expectEqualStrings("012\n45\n890\n", editor.text.items);
1670 try std.testing.expectEqualSlices(
1671 Selection,
1672 &.{
1673 .{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 1, .col = 2 } },
1674 },
1675 editor.selections.items,
1676 );
1677
1678 try testOnly_resetEditor(&editor, talloc);
1679
1680 // 7. Shrinking a selection to a cursor should make that selection a cursor.
1681
1682 // Before:
1683 //
1684 // 1 | 01^2|
1685 // 2 | 456
1686 // 3 | 890
1687 // 4 |
1688 //
1689 // After:
1690 //
1691 // 1 | 01+
1692 // 2 | 456
1693 // 3 | 890
1694 // 4 |
1695
1696 try editor.selections.append(talloc, .{
1697 .anchor = .{ .row = 0, .col = 2 },
1698 .cursor = .{ .row = 0, .col = 3 },
1699 });
1700
1701 try editor.deleteCharacterBeforeCursors(talloc);
1702
1703 try std.testing.expectEqualStrings("01\n456\n890\n", editor.text.items);
1704 try std.testing.expectEqual(1, editor.selections.items.len);
1705 try std.testing.expect(editor.selections.items[0].isCursor());
1706 try std.testing.expectEqualSlices(
1707 Selection,
1708 &.{
1709 .createCursor(.{ .row = 0, .col = 2 }),
1710 },
1711 editor.selections.items,
1712 );
1713
1714 try testOnly_resetEditor(&editor, talloc);
1715
1716 // 8. Selections are moved correctly after deletion even if they're out of order in the
1717 // selections array.
1718
1719 // Before:
1720 //
1721 // 1 | 01^2|
1722 // 2 | 4^56|
1723 // 3 | ^8|90
1724 // 4 |
1725 //
1726 // After:
1727 //
1728 // 1 | 01+
1729 // 2 | 4^5|
1730 // 3 | +90
1731 // 4 |
1732
1733 try editor.selections.append(talloc, .{
1734 .anchor = .{ .row = 0, .col = 2 },
1735 .cursor = .{ .row = 0, .col = 3 },
1736 });
1737 try editor.selections.append(talloc, .{
1738 .anchor = .{ .row = 1, .col = 1 },
1739 .cursor = .{ .row = 1, .col = 3 },
1740 });
1741 try editor.selections.append(talloc, .{
1742 .anchor = .{ .row = 2, .col = 0 },
1743 .cursor = .{ .row = 2, .col = 1 },
1744 });
1745
1746 try editor.deleteCharacterBeforeCursors(talloc);
1747
1748 try std.testing.expectEqualStrings("01\n45\n90\n", editor.text.items);
1749 try std.testing.expectEqualSlices(
1750 Selection,
1751 &.{
1752 .createCursor(.{ .row = 0, .col = 2 }),
1753 .{ .anchor = .{ .row = 1, .col = 1 }, .cursor = .{ .row = 1, .col = 2 } },
1754 .createCursor(.{ .row = 2, .col = 0 }),
1755 },
1756 editor.selections.items,
1757 );
1758
1759 try testOnly_resetEditor(&editor, talloc);
1760
1761 // Same test, selections in a different order.
1762
1763 try editor.selections.append(talloc, .{
1764 .anchor = .{ .row = 1, .col = 1 },
1765 .cursor = .{ .row = 1, .col = 3 },
1766 });
1767 try editor.selections.append(talloc, .{
1768 .anchor = .{ .row = 0, .col = 2 },
1769 .cursor = .{ .row = 0, .col = 3 },
1770 });
1771 try editor.selections.append(talloc, .{
1772 .anchor = .{ .row = 2, .col = 0 },
1773 .cursor = .{ .row = 2, .col = 1 },
1774 });
1775
1776 try editor.deleteCharacterBeforeCursors(talloc);
1777
1778 try std.testing.expectEqualStrings("01\n45\n90\n", editor.text.items);
1779 try std.testing.expectEqualSlices(
1780 Selection,
1781 &.{
1782 .{ .anchor = .{ .row = 1, .col = 1 }, .cursor = .{ .row = 1, .col = 2 } },
1783 .createCursor(.{ .row = 0, .col = 2 }),
1784 .createCursor(.{ .row = 2, .col = 0 }),
1785 },
1786 editor.selections.items,
1787 );
1788
1789 try testOnly_resetEditor(&editor, talloc);
1790
1791 // Same test, selections in a different order.
1792
1793 try editor.selections.append(talloc, .{
1794 .anchor = .{ .row = 2, .col = 0 },
1795 .cursor = .{ .row = 2, .col = 1 },
1796 });
1797 try editor.selections.append(talloc, .{
1798 .anchor = .{ .row = 0, .col = 2 },
1799 .cursor = .{ .row = 0, .col = 3 },
1800 });
1801 try editor.selections.append(talloc, .{
1802 .anchor = .{ .row = 1, .col = 1 },
1803 .cursor = .{ .row = 1, .col = 3 },
1804 });
1805
1806 try editor.deleteCharacterBeforeCursors(talloc);
1807
1808 try std.testing.expectEqualStrings("01\n45\n90\n", editor.text.items);
1809 try std.testing.expectEqualSlices(
1810 Selection,
1811 &.{
1812 .createCursor(.{ .row = 2, .col = 0 }),
1813 .createCursor(.{ .row = 0, .col = 2 }),
1814 .{ .anchor = .{ .row = 1, .col = 1 }, .cursor = .{ .row = 1, .col = 2 } },
1815 },
1816 editor.selections.items,
1817 );
1818
1819 try testOnly_resetEditor(&editor, talloc);
1820
1821 // 9. Deleting from side-by-side selections where the anchor from one touches the cursor from
1822 // the other.
1823
1824 // Before:
1825 //
1826 // 1 | 0^1|^2|
1827 // 2 | 456
1828 // 3 | 890
1829 // 4 |
1830 //
1831 // After:
1832 //
1833 // 1 | 0+
1834 // 2 | 456
1835 // 3 | 890
1836 // 4 |
1837
1838 try editor.selections.append(
1839 talloc,
1840 .{
1841 .anchor = .{ .row = 0, .col = 1 },
1842 .cursor = .{ .row = 0, .col = 2 },
1843 },
1844 );
1845 try editor.selections.append(
1846 talloc,
1847 .{
1848 .anchor = .{ .row = 0, .col = 2 },
1849 .cursor = .{ .row = 0, .col = 3 },
1850 },
1851 );
1852
1853 try editor.deleteCharacterBeforeCursors(talloc);
1854
1855 try std.testing.expectEqualStrings("0\n456\n890\n", editor.text.items);
1856 try std.testing.expectEqualSlices(
1857 Selection,
1858 &.{
1859 .createCursor(.{ .row = 0, .col = 1 }),
1860 },
1861 editor.selections.items,
1862 );
1863
1864 try testOnly_resetEditor(&editor, talloc);
1865
1866 // Before:
1867 //
1868 // 1 | ^01|^2
1869 // 2 | |456
1870 // 3 | 890
1871 // 4 |
1872 //
1873 // After:
1874 //
1875 // 1 | ^0|^2|456
1876 // 2 | 890
1877 // 3 |
1878
1879 try editor.selections.append(talloc, .{
1880 .anchor = .init,
1881 .cursor = .{ .row = 0, .col = 2 },
1882 });
1883 try editor.selections.append(talloc, .{
1884 .anchor = .{ .row = 0, .col = 2 },
1885 .cursor = .{ .row = 1, .col = 0 },
1886 });
1887
1888 try editor.deleteCharacterBeforeCursors(talloc);
1889
1890 try std.testing.expectEqualStrings("02456\n890\n", editor.text.items);
1891 try std.testing.expectEqualSlices(
1892 Selection,
1893 &.{
1894 .{ .anchor = .init, .cursor = .{ .row = 0, .col = 1 } },
1895 .{ .anchor = .{ .row = 0, .col = 1 }, .cursor = .{ .row = 0, .col = 2 } },
1896 },
1897 editor.selections.items,
1898 );
1899
1900 try testOnly_resetEditor(&editor, talloc);
1901
1902 // 10. Deleting from side-by-side selections where the anchors are touching.
1903
1904 // Before:
1905 //
1906 // 1 | 01|2^^
1907 // 2 | |456
1908 // 3 | 890
1909 // 4 |
1910 //
1911 // 1 | 0|2^+456
1912 // 2 | 890
1913 // 4 |
1914
1915 try editor.selections.append(talloc, .{
1916 .anchor = .{ .row = 0, .col = 3 },
1917 .cursor = .{ .row = 0, .col = 2 },
1918 });
1919 try editor.selections.append(talloc, .{
1920 .anchor = .{ .row = 0, .col = 3 },
1921 .cursor = .{ .row = 1, .col = 0 },
1922 });
1923
1924 try editor.deleteCharacterBeforeCursors(talloc);
1925
1926 try std.testing.expectEqualStrings("02456\n890\n", editor.text.items);
1927 try std.testing.expectEqualSlices(
1928 Selection,
1929 &.{
1930 .{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 0, .col = 1 } },
1931 .createCursor(.{ .row = 0, .col = 2 }),
1932 },
1933 editor.selections.items,
1934 );
1935
1936 try testOnly_resetEditor(&editor, talloc);
1937
1938 // Before:
1939 //
1940 // 1 | 01|2^^
1941 // 2 | 4|56
1942 // 3 | 890
1943 // 4 |
1944 //
1945 // After:
1946 //
1947 // 1 | 0|2^^
1948 // 2 | |56
1949 // 2 | 890
1950 // 4 |
1951
1952 try editor.selections.append(talloc, .{
1953 .anchor = .{ .row = 0, .col = 3 },
1954 .cursor = .{ .row = 0, .col = 2 },
1955 });
1956 try editor.selections.append(talloc, .{
1957 .anchor = .{ .row = 0, .col = 3 },
1958 .cursor = .{ .row = 1, .col = 1 },
1959 });
1960
1961 try editor.deleteCharacterBeforeCursors(talloc);
1962
1963 try std.testing.expectEqualStrings("02\n56\n890\n", editor.text.items);
1964 try std.testing.expectEqualSlices(
1965 Selection,
1966 &.{
1967 .{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 0, .col = 1 } },
1968 .{ .anchor = .{ .row = 0, .col = 2 }, .cursor = .{ .row = 1, .col = 0 } },
1969 },
1970 editor.selections.items,
1971 );
1972
1973 try testOnly_resetEditor(&editor, talloc);
1974
1975 // 11. Deleting from side-by-side selections where the cursors are touching; cursors are
1976 // considered as a single cursor.
1977
1978 // Before:
1979 //
1980 // 1 | 01^2||
1981 // 2 | ^456
1982 // 3 | 890
1983 // 4 |
1984 //
1985 // After:
1986 //
1987 // 1 | 01+|
1988 // 2 | ^456
1989 // 3 | 890
1990 // 4 |
1991 //
1992 // FIXME: I think the selections should be merged here because one of the selections has been
1993 // reduced to just the cursor.
1994
1995 try editor.selections.append(talloc, .{
1996 .anchor = .{ .row = 0, .col = 2 },
1997 .cursor = .{ .row = 0, .col = 3 },
1998 });
1999 try editor.selections.append(talloc, .{
2000 .anchor = .{ .row = 1, .col = 0 },
2001 .cursor = .{ .row = 0, .col = 3 },
2002 });
2003
2004 try editor.deleteCharacterBeforeCursors(talloc);
2005
2006 try std.testing.expectEqualStrings("01\n456\n890\n", editor.text.items);
2007 try std.testing.expectEqualSlices(
2008 Selection,
2009 &.{
2010 .createCursor(.{ .row = 0, .col = 2 }),
2011 .{ .anchor = .{ .row = 1, .col = 0 }, .cursor = .{ .row = 0, .col = 2 } },
2012 },
2013 editor.selections.items,
2014 );
2015
2016 try testOnly_resetEditor(&editor, talloc);
2017
2018 // 12. Selections collapse into cursor when they become equal after a deletion.
2019
2020 // Before:
2021 //
2022 // 1 | 0^1|^2|
2023 // 2 | 4^5|6
2024 // 3 | 890
2025 // 4 |
2026 //
2027 // After:
2028 //
2029 // 1 | 0+
2030 // 2 | 4+6
2031 // 3 | 890
2032 // 4 |
2033
2034 try editor.selections.append(talloc, .{
2035 .anchor = .{ .row = 0, .col = 1 },
2036 .cursor = .{ .row = 0, .col = 2 },
2037 });
2038 try editor.selections.append(talloc, .{
2039 .anchor = .{ .row = 0, .col = 2 },
2040 .cursor = .{ .row = 0, .col = 3 },
2041 });
2042 try editor.selections.append(talloc, .{
2043 .anchor = .{ .row = 1, .col = 1 },
2044 .cursor = .{ .row = 1, .col = 2 },
2045 });
2046
2047 try editor.deleteCharacterBeforeCursors(talloc);
2048
2049 try std.testing.expectEqualStrings("0\n46\n890\n", editor.text.items);
2050 try std.testing.expectEqualSlices(
2051 Selection,
2052 &.{
2053 .createCursor(.{ .row = 0, .col = 1 }),
2054 .createCursor(.{ .row = 1, .col = 1 }),
2055 },
2056 editor.selections.items,
2057 );
2058
2059 try testOnly_resetEditor(&editor, talloc);
2060
2061 // == Mix between cursors and selections == //
2062
2063 // Do this later if needed.
2064}
2065
2066test tokenize {
2067 var editor = try Editor.init(talloc);
2068 defer editor.deinit(talloc);
2069
2070 // 1. Empty text.
2071
2072 try editor.tokenize(talloc);
2073
2074 try std.testing.expectEqualSlices(
2075 Token,
2076 &.{
2077 .{ .pos = .init, .text = &.{}, .type = .Text },
2078 },
2079 editor.tokens.items,
2080 );
2081
2082 // 2. Some text.
2083
2084 try editor.text.appendSlice(talloc, "lorem ipsum\n");
2085 try editor.tokenize(talloc);
2086
2087 try std.testing.expectEqual(1, editor.tokens.items.len);
2088
2089 const token = editor.tokens.items[0];
2090
2091 try std.testing.expectEqual(Editor.TokenType.Text, token.type);
2092 try std.testing.expectEqual(Pos.init, token.pos);
2093 try std.testing.expectEqualStrings("lorem ipsum\n", token.text);
2094}
2095
2096test updateLines {
2097 var editor = try Editor.init(talloc);
2098 defer editor.deinit(talloc);
2099
2100 // 1. Empty text.
2101
2102 try editor.updateLines(talloc);
2103
2104 // We should always have at least one line.
2105 try std.testing.expectEqual(1, editor.line_indexes.items.len);
2106 try std.testing.expectEqualSlices(
2107 IndexPos,
2108 &.{.fromInt(0)},
2109 editor.line_indexes.items,
2110 );
2111
2112 // 2. One line.
2113
2114 try editor.text.appendSlice(talloc, "012");
2115 try editor.updateLines(talloc);
2116
2117 try std.testing.expectEqualSlices(
2118 IndexPos,
2119 &.{.fromInt(0)},
2120 editor.line_indexes.items,
2121 );
2122
2123 editor.text.clearRetainingCapacity();
2124
2125 // 3. One line, ends with a new line.
2126
2127 try editor.text.appendSlice(talloc, "012\n");
2128 try editor.updateLines(talloc);
2129
2130 try std.testing.expectEqualSlices(
2131 IndexPos,
2132 &.{ .fromInt(0), .fromInt(4) },
2133 editor.line_indexes.items,
2134 );
2135
2136 editor.text.clearRetainingCapacity();
2137
2138 // 4. Multiple lines.
2139
2140 try editor.text.appendSlice(talloc, "012\n456\n890");
2141 try editor.updateLines(talloc);
2142
2143 try std.testing.expectEqualSlices(
2144 IndexPos,
2145 &.{ .fromInt(0), .fromInt(4), .fromInt(8) },
2146 editor.line_indexes.items,
2147 );
2148
2149 editor.text.clearRetainingCapacity();
2150
2151 // 5. Multiple lines, ends with a new line.
2152
2153 try editor.text.appendSlice(talloc, "012\n456\n890\n");
2154 try editor.updateLines(talloc);
2155
2156 try std.testing.expectEqualSlices(
2157 IndexPos,
2158 &.{ .fromInt(0), .fromInt(4), .fromInt(8), .fromInt(12) },
2159 editor.line_indexes.items,
2160 );
2161
2162 editor.text.clearRetainingCapacity();
2163
2164 // 6. Multiple new lines in a row.
2165
2166 try editor.text.appendSlice(talloc, "012\n\n\n67\n\n0");
2167 try editor.updateLines(talloc);
2168
2169 try std.testing.expectEqualSlices(
2170 IndexPos,
2171 &.{ .fromInt(0), .fromInt(4), .fromInt(5), .fromInt(6), .fromInt(9), .fromInt(10) },
2172 editor.line_indexes.items,
2173 );
2174}
2175
2176test "getLine" {
2177 var editor = try Editor.init(talloc);
2178 defer editor.deinit(talloc);
2179
2180 try editor.text.appendSlice(talloc, "012\n345\n456\n\n");
2181 try editor.updateLines(talloc);
2182
2183 try std.testing.expectEqualStrings(
2184 "012\n",
2185 editor.getLine(0),
2186 );
2187 try std.testing.expectEqualStrings(
2188 "345\n",
2189 editor.getLine(1),
2190 );
2191 try std.testing.expectEqualStrings(
2192 "456\n",
2193 editor.getLine(2),
2194 );
2195 try std.testing.expectEqualStrings(
2196 "\n",
2197 editor.getLine(3),
2198 );
2199 try std.testing.expectEqualStrings(
2200 "",
2201 editor.getLine(4),
2202 );
2203}
2204
2205test "refAllDecls" {
2206 std.testing.refAllDecls(@This());
2207}