TUI editor and editor backend written in Zig
at main 2207 lines 69 kB view raw
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}