github.com/aretext/aretext@v1.3.0/locate/character_test.go (about) 1 package locate 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/assert" 7 "github.com/stretchr/testify/require" 8 9 "github.com/aretext/aretext/text" 10 ) 11 12 func TestNextCharInLine(t *testing.T) { 13 testCases := []struct { 14 name string 15 inputString string 16 pos uint64 17 count uint64 18 includeEndOfLineOrFile bool 19 expectedPos uint64 20 }{ 21 { 22 name: "empty string", 23 inputString: "", 24 pos: 0, 25 count: 1, 26 expectedPos: 0, 27 }, 28 { 29 name: "first char, ASCII string", 30 inputString: "abcd", 31 pos: 0, 32 count: 1, 33 expectedPos: 1, 34 }, 35 { 36 name: "second char, ASCII string", 37 inputString: "abcd", 38 pos: 1, 39 count: 1, 40 expectedPos: 2, 41 }, 42 { 43 name: "last char, ASCII string", 44 inputString: "abcd", 45 pos: 3, 46 count: 1, 47 expectedPos: 3, 48 }, 49 { 50 name: "multi-char grapheme cluster", 51 inputString: "e\u0301xyz", 52 pos: 0, 53 count: 1, 54 expectedPos: 2, 55 }, 56 { 57 name: "up to end of line", 58 inputString: "ab\ncd", 59 pos: 1, 60 count: 1, 61 expectedPos: 1, 62 }, 63 { 64 name: "at end of line", 65 inputString: "ab\ncd", 66 pos: 2, 67 count: 1, 68 expectedPos: 2, 69 }, 70 { 71 name: "end of line with carriage return", 72 inputString: "ab\r\ncd", 73 pos: 1, 74 count: 1, 75 expectedPos: 1, 76 }, 77 { 78 name: "move cursor multiple chars within line", 79 inputString: "abcdefgh", 80 pos: 2, 81 count: 3, 82 expectedPos: 5, 83 }, 84 { 85 name: "move cursor multiple chars to end of line", 86 inputString: "abcd\nefgh", 87 pos: 1, 88 count: 2, 89 expectedPos: 3, 90 }, 91 { 92 name: "move cursor multiple chars past end of line", 93 inputString: "abcd\nefgh", 94 pos: 1, 95 count: 5, 96 expectedPos: 3, 97 }, 98 { 99 name: "move cursor multiple chars past end of string", 100 inputString: "abcd", 101 pos: 0, 102 count: 100, 103 expectedPos: 3, 104 }, 105 { 106 name: "include end of line", 107 inputString: "abcd\nefgh", 108 pos: 2, 109 count: 5, 110 includeEndOfLineOrFile: true, 111 expectedPos: 4, 112 }, 113 { 114 name: "include end of file", 115 inputString: "abcd", 116 pos: 2, 117 count: 5, 118 includeEndOfLineOrFile: true, 119 expectedPos: 4, 120 }, 121 { 122 name: "first character when including end of line or file", 123 inputString: "abcdefgh", 124 pos: 0, 125 count: 1, 126 includeEndOfLineOrFile: true, 127 expectedPos: 1, 128 }, 129 } 130 131 for _, tc := range testCases { 132 t.Run(tc.name, func(t *testing.T) { 133 textTree, err := text.NewTreeFromString(tc.inputString) 134 require.NoError(t, err) 135 actualPos := NextCharInLine(textTree, tc.count, tc.includeEndOfLineOrFile, tc.pos) 136 assert.Equal(t, tc.expectedPos, actualPos) 137 }) 138 } 139 } 140 141 func TestPrevCharInLine(t *testing.T) { 142 testCases := []struct { 143 name string 144 inputString string 145 pos uint64 146 count uint64 147 includeEndOfLineOrFile bool 148 expectedPos uint64 149 }{ 150 { 151 name: "empty", 152 inputString: "", 153 pos: 0, 154 count: 1, 155 expectedPos: 0, 156 }, 157 { 158 name: "last char, ASCII string", 159 inputString: "abcd", 160 pos: 3, 161 count: 1, 162 expectedPos: 2, 163 }, 164 { 165 name: "second-to-last char, ASCII string", 166 inputString: "abcd", 167 pos: 2, 168 count: 1, 169 expectedPos: 1, 170 }, 171 { 172 name: "first char, ASCII string", 173 inputString: "abcd", 174 pos: 0, 175 count: 1, 176 expectedPos: 0, 177 }, 178 { 179 name: "first char in line", 180 inputString: "ab\ncd", 181 pos: 3, 182 count: 1, 183 expectedPos: 3, 184 }, 185 { 186 name: "multi-char grapheme cluster", 187 inputString: "abe\u0301xyz", 188 pos: 4, 189 count: 1, 190 expectedPos: 2, 191 }, 192 { 193 name: "move cursor multiple chars within line", 194 inputString: "abcdefgh", 195 pos: 4, 196 count: 3, 197 expectedPos: 1, 198 }, 199 { 200 name: "move cursor multiple chars to beginning of line", 201 inputString: "ab\ncdefgh", 202 pos: 5, 203 count: 2, 204 expectedPos: 3, 205 }, 206 { 207 name: "move cursor multiple chars past beginning of line", 208 inputString: "ab\ncdefgh", 209 pos: 5, 210 count: 4, 211 expectedPos: 3, 212 }, 213 { 214 name: "include end of previous line", 215 inputString: "abcd\nefgh", 216 pos: 5, 217 count: 1, 218 includeEndOfLineOrFile: true, 219 expectedPos: 4, 220 }, 221 } 222 223 for _, tc := range testCases { 224 t.Run(tc.name, func(t *testing.T) { 225 textTree, err := text.NewTreeFromString(tc.inputString) 226 require.NoError(t, err) 227 actualPos := PrevCharInLine(textTree, tc.count, tc.includeEndOfLineOrFile, tc.pos) 228 assert.Equal(t, tc.expectedPos, actualPos) 229 }) 230 } 231 } 232 233 func TestPrevChar(t *testing.T) { 234 testCases := []struct { 235 name string 236 inputString string 237 pos uint64 238 count uint64 239 expectedPos uint64 240 }{ 241 { 242 name: "empty string", 243 inputString: "", 244 pos: 0, 245 count: 1, 246 expectedPos: 0, 247 }, 248 { 249 name: "back single char, same line", 250 inputString: "abc\ndef", 251 pos: 5, 252 count: 1, 253 expectedPos: 4, 254 }, 255 { 256 name: "back single char, prev line", 257 inputString: "abc\ndef", 258 pos: 3, 259 count: 1, 260 expectedPos: 2, 261 }, 262 { 263 name: "back multi-char grapheme cluster", 264 inputString: "e\u0301xyz", 265 pos: 2, 266 count: 1, 267 expectedPos: 0, 268 }, 269 { 270 name: "back multiple chars, within document", 271 inputString: "abc\ndef", 272 pos: 5, 273 count: 3, 274 expectedPos: 2, 275 }, 276 { 277 name: "back multiple chars, outside document", 278 inputString: "abc\ndef", 279 pos: 5, 280 count: 100, 281 expectedPos: 0, 282 }, 283 } 284 285 for _, tc := range testCases { 286 t.Run(tc.name, func(t *testing.T) { 287 textTree, err := text.NewTreeFromString(tc.inputString) 288 require.NoError(t, err) 289 actualPos := PrevChar(textTree, tc.count, tc.pos) 290 assert.Equal(t, tc.expectedPos, actualPos) 291 }) 292 } 293 } 294 295 func TestNextMatchingCharInLine(t *testing.T) { 296 testCases := []struct { 297 name string 298 inputString string 299 char rune 300 count uint64 301 includeChar bool 302 pos uint64 303 expectFound bool 304 expectedPos uint64 305 }{ 306 { 307 name: "empty string", 308 inputString: "", 309 char: 'x', 310 count: 1, 311 pos: 0, 312 expectFound: false, 313 expectedPos: 0, 314 }, 315 { 316 name: "not found on first line", 317 inputString: "abcxyz", 318 char: 'm', 319 count: 1, 320 pos: 1, 321 expectFound: false, 322 expectedPos: 0, 323 }, 324 { 325 name: "count zero finds nothing", 326 inputString: "abcxyz", 327 char: 'x', 328 count: 0, 329 pos: 1, 330 expectFound: false, 331 expectedPos: 0, 332 }, 333 { 334 name: "found on first line, include", 335 inputString: "abcxyz", 336 char: 'x', 337 count: 1, 338 includeChar: true, 339 pos: 1, 340 expectFound: true, 341 expectedPos: 3, 342 }, 343 { 344 name: "found on first line, exclude", 345 inputString: "abcxyz", 346 char: 'x', 347 count: 1, 348 includeChar: false, 349 pos: 1, 350 expectFound: true, 351 expectedPos: 2, 352 }, 353 { 354 name: "found on first line, count > 0", 355 inputString: "abcxyzxyz", 356 char: 'x', 357 count: 2, 358 includeChar: true, 359 pos: 1, 360 expectFound: true, 361 expectedPos: 6, 362 }, 363 { 364 name: "next match on subsequent line", 365 inputString: "abc\nxyz", 366 char: 'x', 367 count: 1, 368 includeChar: true, 369 pos: 1, 370 expectFound: false, 371 expectedPos: 0, 372 }, 373 { 374 name: "match at end of current line", 375 inputString: "abc\nabx\nyz", 376 char: 'x', 377 count: 1, 378 includeChar: true, 379 pos: 4, 380 expectFound: true, 381 expectedPos: 6, 382 }, 383 { 384 name: "no match character same as under cursor", 385 inputString: "ab", 386 char: 'a', 387 count: 1, 388 includeChar: false, 389 pos: 0, 390 expectFound: false, 391 expectedPos: 0, 392 }, 393 { 394 name: "match character same as under cursor", 395 inputString: "xaaaaaaaxbbbb", 396 char: 'x', 397 count: 1, 398 includeChar: false, 399 pos: 0, 400 expectFound: true, 401 expectedPos: 7, 402 }, 403 { 404 name: "match next character same as character under cursor", 405 inputString: "aab", 406 char: 'a', 407 count: 1, 408 includeChar: false, 409 pos: 0, 410 expectFound: true, 411 expectedPos: 0, 412 }, 413 } 414 415 for _, tc := range testCases { 416 t.Run(tc.name, func(t *testing.T) { 417 textTree, err := text.NewTreeFromString(tc.inputString) 418 require.NoError(t, err) 419 found, actualPos := NextMatchingCharInLine(textTree, tc.char, tc.count, tc.includeChar, tc.pos) 420 assert.Equal(t, tc.expectFound, found) 421 assert.Equal(t, tc.expectedPos, actualPos) 422 }) 423 } 424 } 425 426 func TestPrevMatchingCharInLine(t *testing.T) { 427 testCases := []struct { 428 name string 429 inputString string 430 char rune 431 count uint64 432 includeChar bool 433 pos uint64 434 expectFound bool 435 expectedPos uint64 436 }{ 437 { 438 name: "empty string", 439 inputString: "", 440 char: 'x', 441 count: 1, 442 pos: 0, 443 expectFound: false, 444 expectedPos: 0, 445 }, 446 { 447 name: "not found on first line", 448 inputString: "abcxyz", 449 char: 'm', 450 count: 1, 451 pos: 5, 452 expectFound: false, 453 expectedPos: 0, 454 }, 455 { 456 name: "count zero finds nothing", 457 inputString: "abcxyz", 458 char: 'x', 459 count: 0, 460 pos: 5, 461 expectFound: false, 462 expectedPos: 0, 463 }, 464 { 465 name: "found on first line, include", 466 inputString: "abcxyz", 467 char: 'x', 468 count: 1, 469 includeChar: true, 470 pos: 5, 471 expectFound: true, 472 expectedPos: 3, 473 }, 474 { 475 name: "found on first line, exclude", 476 inputString: "abcxyz", 477 char: 'x', 478 count: 1, 479 includeChar: false, 480 pos: 5, 481 expectFound: true, 482 expectedPos: 4, 483 }, 484 { 485 name: "found on first line, count > 0", 486 inputString: "abcxyzxyz", 487 char: 'x', 488 count: 2, 489 includeChar: true, 490 pos: 8, 491 expectFound: true, 492 expectedPos: 3, 493 }, 494 { 495 name: "next match on previous line", 496 inputString: "abcx\nyz", 497 char: 'x', 498 count: 1, 499 includeChar: true, 500 pos: 6, 501 expectFound: false, 502 expectedPos: 0, 503 }, 504 { 505 name: "match at start of current line", 506 inputString: "abc\nxab\nyz", 507 char: 'x', 508 count: 1, 509 includeChar: true, 510 pos: 6, 511 expectFound: true, 512 expectedPos: 4, 513 }, 514 } 515 516 for _, tc := range testCases { 517 t.Run(tc.name, func(t *testing.T) { 518 textTree, err := text.NewTreeFromString(tc.inputString) 519 require.NoError(t, err) 520 found, actualPos := PrevMatchingCharInLine(textTree, tc.char, tc.count, tc.includeChar, tc.pos) 521 assert.Equal(t, tc.expectFound, found) 522 assert.Equal(t, tc.expectedPos, actualPos) 523 }) 524 } 525 } 526 527 func TestPrevAutoIndent(t *testing.T) { 528 testCases := []struct { 529 name string 530 inputString string 531 autoIndentEnabled bool 532 pos uint64 533 expectedPos uint64 534 }{ 535 { 536 name: "empty string", 537 inputString: "", 538 pos: 0, 539 expectedPos: 0, 540 }, 541 { 542 name: "multiple tabs, autoindent disabled", 543 inputString: "\t\t", 544 pos: 2, 545 expectedPos: 2, 546 }, 547 { 548 name: "single space, autoindent enabled", 549 inputString: " ", 550 autoIndentEnabled: true, 551 pos: 1, 552 expectedPos: 0, 553 }, 554 { 555 name: "multiple spaces, autoindent enabled", 556 inputString: " ", 557 autoIndentEnabled: true, 558 pos: 8, 559 expectedPos: 4, 560 }, 561 { 562 name: "multiple tabs, autoindent enabled", 563 inputString: "\t\t", 564 autoIndentEnabled: true, 565 pos: 2, 566 expectedPos: 1, 567 }, 568 { 569 name: "mixed tabs and spaces, autoindent enabled", 570 inputString: " \t", 571 autoIndentEnabled: true, 572 pos: 2, 573 expectedPos: 0, 574 }, 575 { 576 name: "no tabs or spaces, autoindent enabled", 577 inputString: "ab", 578 autoIndentEnabled: true, 579 pos: 2, 580 expectedPos: 2, 581 }, 582 { 583 name: "start of line, autoindent enabled", 584 inputString: "ab\ncd", 585 autoIndentEnabled: true, 586 pos: 2, 587 expectedPos: 2, 588 }, 589 { 590 name: "end of document, autoindent enabled", 591 inputString: "ab\n\n", 592 autoIndentEnabled: true, 593 pos: 3, 594 expectedPos: 3, 595 }, 596 { 597 name: "spaces within line aligned, autoindent enabled", 598 inputString: "abcd ef", 599 autoIndentEnabled: true, 600 pos: 8, 601 expectedPos: 4, 602 }, 603 { 604 name: "spaces within line misaligned, autoindent enabled", 605 inputString: "ab cd", 606 autoIndentEnabled: true, 607 pos: 6, 608 expectedPos: 4, 609 }, 610 { 611 name: "tabs within line, autoindent enabled", 612 inputString: "ab\t\tcd", 613 autoIndentEnabled: true, 614 pos: 4, 615 expectedPos: 3, 616 }, 617 { 618 name: "spaces within line but not before cursor, autoindent enabled", 619 inputString: "ab cdef", 620 autoIndentEnabled: true, 621 pos: 7, 622 expectedPos: 7, 623 }, 624 { 625 name: "spaces at end of line less than tab size, autoindent enabled", 626 inputString: "abcdef ", 627 autoIndentEnabled: true, 628 pos: 8, 629 expectedPos: 6, 630 }, 631 } 632 633 for _, tc := range testCases { 634 t.Run(tc.name, func(t *testing.T) { 635 textTree, err := text.NewTreeFromString(tc.inputString) 636 require.NoError(t, err) 637 actualPos := PrevAutoIndent(textTree, tc.autoIndentEnabled, 4, tc.pos) 638 assert.Equal(t, tc.expectedPos, actualPos) 639 }) 640 } 641 } 642 643 func TestNextNonWhitespaceOrNewline(t *testing.T) { 644 testCases := []struct { 645 name string 646 inputString string 647 pos uint64 648 expectedPos uint64 649 }{ 650 { 651 name: "empty", 652 inputString: "", 653 pos: 0, 654 expectedPos: 0, 655 }, 656 { 657 name: "no movement", 658 inputString: " abcd ", 659 pos: 4, 660 expectedPos: 4, 661 }, 662 { 663 name: "movement", 664 inputString: " abcd ", 665 pos: 1, 666 expectedPos: 3, 667 }, 668 { 669 name: "stop before newline on empty line", 670 inputString: "abcd\n\n\nefgh", 671 pos: 5, 672 expectedPos: 5, 673 }, 674 { 675 name: "stop before newline at end of line", 676 inputString: "abcd\nefghi", 677 pos: 3, 678 expectedPos: 3, 679 }, 680 } 681 682 for _, tc := range testCases { 683 t.Run(tc.name, func(t *testing.T) { 684 textTree, err := text.NewTreeFromString(tc.inputString) 685 require.NoError(t, err) 686 actualPos := NextNonWhitespaceOrNewline(textTree, tc.pos) 687 assert.Equal(t, tc.expectedPos, actualPos) 688 }) 689 } 690 } 691 692 func TestNextNewline(t *testing.T) { 693 testCases := []struct { 694 name string 695 inputString string 696 pos uint64 697 expectedOk bool 698 expectedPos uint64 699 expectedLen uint64 700 }{ 701 { 702 name: "empty", 703 inputString: "", 704 pos: 0, 705 expectedOk: false, 706 }, 707 { 708 name: "last line", 709 inputString: "abcd", 710 pos: 2, 711 expectedOk: false, 712 }, 713 { 714 name: "before LF", 715 inputString: "abc\ndef", 716 pos: 1, 717 expectedOk: true, 718 expectedPos: 3, 719 expectedLen: 1, 720 }, 721 { 722 name: "on LF", 723 inputString: "abc\ndef", 724 pos: 3, 725 expectedOk: true, 726 expectedPos: 3, 727 expectedLen: 1, 728 }, 729 { 730 name: "before CR LF", 731 inputString: "abc\r\ndef", 732 pos: 1, 733 expectedOk: true, 734 expectedPos: 3, 735 expectedLen: 2, 736 }, 737 { 738 name: "on CR LF", 739 inputString: "abc\r\ndef", 740 pos: 3, 741 expectedOk: true, 742 expectedPos: 3, 743 expectedLen: 2, 744 }, 745 } 746 for _, tc := range testCases { 747 t.Run(tc.name, func(t *testing.T) { 748 textTree, err := text.NewTreeFromString(tc.inputString) 749 require.NoError(t, err) 750 actualPos, actualLen, actualOk := NextNewline(textTree, tc.pos) 751 assert.Equal(t, tc.expectedOk, actualOk) 752 assert.Equal(t, tc.expectedPos, actualPos) 753 assert.Equal(t, tc.expectedLen, actualLen) 754 }) 755 } 756 } 757 758 func TestNumGraphemeClustersInRange(t *testing.T) { 759 testCases := []struct { 760 name string 761 inputString string 762 startPos uint64 763 endPos uint64 764 expectedCount uint64 765 }{ 766 { 767 name: "empty text", 768 inputString: "", 769 startPos: 0, 770 endPos: 0, 771 expectedCount: 0, 772 }, 773 { 774 name: "empty range", 775 inputString: "abcdefgh", 776 startPos: 1, 777 endPos: 1, 778 expectedCount: 0, 779 }, 780 { 781 name: "single-rune grapheme clusters", 782 inputString: "abcdefgh", 783 startPos: 1, 784 endPos: 4, 785 expectedCount: 3, 786 }, 787 { 788 name: "multi-rune grapheme clusters", 789 inputString: "ᄀ̈각각̈͏", 790 startPos: 0, 791 endPos: 6, 792 expectedCount: 3, 793 }, 794 { 795 name: "past end of file", 796 inputString: "abcdefgh", 797 startPos: 3, 798 endPos: 100, 799 expectedCount: 5, 800 }, 801 } 802 803 for _, tc := range testCases { 804 t.Run(tc.name, func(t *testing.T) { 805 textTree, err := text.NewTreeFromString(tc.inputString) 806 require.NoError(t, err) 807 actualCount := NumGraphemeClustersInRange(textTree, tc.startPos, tc.endPos) 808 assert.Equal(t, tc.expectedCount, actualCount) 809 }) 810 } 811 }