github.com/aretext/aretext@v1.3.0/state/edit_test.go (about) 1 package state 2 3 import ( 4 "testing" 5 6 "github.com/stretchr/testify/assert" 7 "github.com/stretchr/testify/require" 8 9 "github.com/aretext/aretext/clipboard" 10 "github.com/aretext/aretext/locate" 11 "github.com/aretext/aretext/selection" 12 "github.com/aretext/aretext/text" 13 ) 14 15 func TestInsertRune(t *testing.T) { 16 testCases := []struct { 17 name string 18 inputString string 19 initialCursor cursorState 20 insertRune rune 21 expectedCursor cursorState 22 expectedText string 23 }{ 24 { 25 name: "insert into empty string", 26 inputString: "", 27 initialCursor: cursorState{position: 0}, 28 insertRune: 'x', 29 expectedCursor: cursorState{position: 1}, 30 expectedText: "x", 31 }, 32 { 33 name: "insert in middle of string", 34 inputString: "abcd", 35 initialCursor: cursorState{position: 1}, 36 insertRune: 'x', 37 expectedCursor: cursorState{position: 2}, 38 expectedText: "axbcd", 39 }, 40 { 41 name: "insert at end of string", 42 inputString: "abcd", 43 initialCursor: cursorState{position: 4}, 44 insertRune: 'x', 45 expectedCursor: cursorState{position: 5}, 46 expectedText: "abcdx", 47 }, 48 } 49 50 for _, tc := range testCases { 51 t.Run(tc.name, func(t *testing.T) { 52 textTree, err := text.NewTreeFromString(tc.inputString) 53 require.NoError(t, err) 54 state := NewEditorState(100, 100, nil, nil) 55 state.documentBuffer.textTree = textTree 56 state.documentBuffer.cursor = tc.initialCursor 57 InsertRune(state, tc.insertRune) 58 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 59 assert.Equal(t, tc.expectedText, textTree.String()) 60 }) 61 } 62 } 63 64 func TestInsertText(t *testing.T) { 65 testCases := []struct { 66 name string 67 inputString string 68 initialCursor cursorState 69 insertText string 70 expectedCursor cursorState 71 expectedText string 72 }{ 73 { 74 name: "empty", 75 initialCursor: cursorState{position: 0}, 76 inputString: "", 77 insertText: "", 78 expectedCursor: cursorState{position: 0}, 79 expectedText: "", 80 }, 81 { 82 name: "ascii", 83 initialCursor: cursorState{position: 1}, 84 inputString: "abc", 85 insertText: "xyz", 86 expectedCursor: cursorState{position: 4}, 87 expectedText: "axyzbc", 88 }, 89 { 90 name: "non-ascii unicode with multi-byte runes", 91 initialCursor: cursorState{position: 1}, 92 inputString: "abc", 93 insertText: "丂丄丅丆丏 ¢ह€한", 94 expectedCursor: cursorState{position: 11}, 95 expectedText: "a丂丄丅丆丏 ¢ह€한bc", 96 }, 97 } 98 99 for _, tc := range testCases { 100 t.Run(tc.name, func(t *testing.T) { 101 textTree, err := text.NewTreeFromString(tc.inputString) 102 require.NoError(t, err) 103 state := NewEditorState(100, 100, nil, nil) 104 state.documentBuffer.textTree = textTree 105 state.documentBuffer.cursor = tc.initialCursor 106 InsertText(state, tc.insertText) 107 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 108 assert.Equal(t, tc.expectedText, textTree.String()) 109 }) 110 } 111 } 112 113 func TestDeleteToPos(t *testing.T) { 114 testCases := []struct { 115 name string 116 inputString string 117 initialCursor cursorState 118 locator func(LocatorParams) uint64 119 expectedCursor cursorState 120 expectedText string 121 expectedClipboard clipboard.PageContent 122 }{ 123 { 124 name: "delete from empty string", 125 inputString: "", 126 initialCursor: cursorState{position: 0}, 127 locator: func(params LocatorParams) uint64 { 128 return locate.NextCharInLine(params.TextTree, 1, true, params.CursorPos) 129 }, 130 expectedCursor: cursorState{position: 0}, 131 expectedText: "", 132 }, 133 { 134 name: "delete next character at start of string", 135 inputString: "abcd", 136 initialCursor: cursorState{position: 0}, 137 locator: func(params LocatorParams) uint64 { 138 return locate.NextCharInLine(params.TextTree, 1, true, params.CursorPos) 139 }, 140 expectedCursor: cursorState{position: 0}, 141 expectedText: "bcd", 142 expectedClipboard: clipboard.PageContent{Text: "a"}, 143 }, 144 { 145 name: "delete from end of text", 146 inputString: "abcd", 147 initialCursor: cursorState{position: 3}, 148 locator: func(params LocatorParams) uint64 { 149 return locate.NextCharInLine(params.TextTree, 1, true, params.CursorPos) 150 }, 151 expectedCursor: cursorState{position: 3}, 152 expectedText: "abc", 153 expectedClipboard: clipboard.PageContent{Text: "d"}, 154 }, 155 { 156 name: "delete multiple characters", 157 inputString: "abcd", 158 initialCursor: cursorState{position: 1}, 159 locator: func(params LocatorParams) uint64 { 160 return locate.NextCharInLine(params.TextTree, 10, true, params.CursorPos) 161 }, 162 expectedCursor: cursorState{position: 1}, 163 expectedText: "a", 164 expectedClipboard: clipboard.PageContent{Text: "bcd"}, 165 }, 166 } 167 168 for _, tc := range testCases { 169 t.Run(tc.name, func(t *testing.T) { 170 textTree, err := text.NewTreeFromString(tc.inputString) 171 require.NoError(t, err) 172 state := NewEditorState(100, 100, nil, nil) 173 state.documentBuffer.textTree = textTree 174 state.documentBuffer.cursor = tc.initialCursor 175 DeleteToPos(state, tc.locator, clipboard.PageDefault) 176 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 177 assert.Equal(t, tc.expectedText, textTree.String()) 178 assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault)) 179 }) 180 } 181 } 182 183 func TestInsertNewline(t *testing.T) { 184 testCases := []struct { 185 name string 186 inputString string 187 autoIndent bool 188 cursorPos uint64 189 tabExpand bool 190 expectedCursorPos uint64 191 expectedText string 192 }{ 193 { 194 name: "empty document, autoindent disabled", 195 inputString: "", 196 cursorPos: 0, 197 expectedCursorPos: 1, 198 expectedText: "\n", 199 }, 200 { 201 name: "single line, autoindent disabled, no indentation", 202 inputString: "abcd", 203 cursorPos: 2, 204 expectedCursorPos: 3, 205 expectedText: "ab\ncd", 206 }, 207 { 208 name: "single line, autoindent disabled, with indentation", 209 inputString: "\tabcd", 210 cursorPos: 3, 211 expectedCursorPos: 4, 212 expectedText: "\tab\ncd", 213 }, 214 { 215 name: "single line, autoindent enabled, no indentation", 216 inputString: "abcd", 217 autoIndent: true, 218 cursorPos: 2, 219 expectedCursorPos: 3, 220 expectedText: "ab\ncd", 221 }, 222 { 223 name: "single line, autoindent enabled, tab indentation", 224 inputString: "\tabcd", 225 autoIndent: true, 226 cursorPos: 3, 227 expectedCursorPos: 5, 228 expectedText: "\tab\n\tcd", 229 }, 230 { 231 name: "single line, autoindent enabled, space indentation", 232 inputString: " abcd", 233 autoIndent: true, 234 cursorPos: 6, 235 expectedCursorPos: 8, 236 expectedText: " ab\n\tcd", 237 }, 238 { 239 name: "single line, autoindent enabled, mixed tabs and spaces aligned indentation", 240 inputString: " \tabcd", 241 autoIndent: true, 242 cursorPos: 4, 243 expectedCursorPos: 6, 244 expectedText: " \tab\n\tcd", 245 }, 246 { 247 name: "single line, autoindent enabled, mixed tabs and spaces misaligned indentation", 248 inputString: "\t abcd", 249 autoIndent: true, 250 cursorPos: 4, 251 expectedCursorPos: 7, 252 expectedText: "\t ab\n\t cd", 253 }, 254 { 255 name: "expand tab inserts spaces", 256 inputString: " abcd", 257 autoIndent: true, 258 tabExpand: true, 259 cursorPos: 8, 260 expectedCursorPos: 13, 261 expectedText: " abcd\n ", 262 }, 263 { 264 name: "dedent if extra whitespace at end of current line", 265 inputString: " abcd xyz", 266 autoIndent: true, 267 tabExpand: true, 268 cursorPos: 8, 269 expectedCursorPos: 13, 270 expectedText: " abcd\n xyz", 271 }, 272 } 273 274 for _, tc := range testCases { 275 t.Run(tc.name, func(t *testing.T) { 276 textTree, err := text.NewTreeFromString(tc.inputString) 277 require.NoError(t, err) 278 state := NewEditorState(100, 100, nil, nil) 279 state.documentBuffer.textTree = textTree 280 state.documentBuffer.cursor = cursorState{position: tc.cursorPos} 281 state.documentBuffer.autoIndent = tc.autoIndent 282 state.documentBuffer.tabSize = 4 283 state.documentBuffer.tabExpand = tc.tabExpand 284 InsertNewline(state) 285 assert.Equal(t, cursorState{position: tc.expectedCursorPos}, state.documentBuffer.cursor) 286 assert.Equal(t, tc.expectedText, textTree.String()) 287 }) 288 } 289 } 290 291 func TestClearAutoIndentWhitespaceLine(t *testing.T) { 292 testCases := []struct { 293 name string 294 inputString string 295 cursorPos uint64 296 targetLinePos uint64 297 expectedText string 298 expectedCursorPos uint64 299 }{ 300 { 301 name: "empty", 302 inputString: "", 303 cursorPos: 0, 304 targetLinePos: 0, 305 expectedText: "", 306 expectedCursorPos: 0, 307 }, 308 { 309 name: "line with non-whitespace chars", 310 inputString: " abc", 311 cursorPos: 0, 312 targetLinePos: 0, 313 expectedText: " abc", 314 expectedCursorPos: 0, 315 }, 316 { 317 name: "line with only spaces", 318 inputString: " ", 319 cursorPos: 1, 320 targetLinePos: 0, 321 expectedText: "", 322 expectedCursorPos: 0, 323 }, 324 { 325 name: "line with only tabs", 326 inputString: "\t\t", 327 cursorPos: 1, 328 targetLinePos: 0, 329 expectedText: "", 330 expectedCursorPos: 0, 331 }, 332 { 333 name: "cursor after target line", 334 inputString: " ab\n \n cd", 335 cursorPos: 17, 336 targetLinePos: 7, 337 expectedText: " ab\n\n cd", 338 expectedCursorPos: 13, 339 }, 340 { 341 name: "cursor before target line", 342 inputString: " ab\n \n cd", 343 cursorPos: 5, 344 targetLinePos: 7, 345 expectedText: " ab\n\n cd", 346 expectedCursorPos: 5, 347 }, 348 { 349 name: "cursor on target line", 350 inputString: " ab\n \n cd", 351 cursorPos: 9, 352 targetLinePos: 7, 353 expectedText: " ab\n\n cd", 354 expectedCursorPos: 7, 355 }, 356 } 357 358 for _, tc := range testCases { 359 t.Run(tc.name, func(t *testing.T) { 360 textTree, err := text.NewTreeFromString(tc.inputString) 361 require.NoError(t, err) 362 state := NewEditorState(100, 100, nil, nil) 363 state.documentBuffer.textTree = textTree 364 state.documentBuffer.cursor = cursorState{position: tc.cursorPos} 365 state.documentBuffer.autoIndent = true 366 ClearAutoIndentWhitespaceLine(state, func(p LocatorParams) uint64 { 367 return locate.StartOfLineAtPos(p.TextTree, tc.targetLinePos) 368 }) 369 assert.Equal(t, cursorState{position: tc.expectedCursorPos}, state.documentBuffer.cursor) 370 assert.Equal(t, tc.expectedText, textTree.String()) 371 }) 372 } 373 } 374 375 func TestInsertTab(t *testing.T) { 376 testCases := []struct { 377 name string 378 inputString string 379 initialCursor cursorState 380 expectedText string 381 expectedCursor cursorState 382 tabExpand bool 383 }{ 384 { 385 name: "insert tab, no expand", 386 inputString: "abcd", 387 initialCursor: cursorState{position: 2}, 388 expectedText: "ab\tcd", 389 expectedCursor: cursorState{position: 3}, 390 }, 391 { 392 name: "insert tab, expand full width", 393 tabExpand: true, 394 inputString: "abcd", 395 initialCursor: cursorState{position: 0}, 396 expectedText: " abcd", 397 expectedCursor: cursorState{position: 4}, 398 }, 399 { 400 name: "insert tab, partial width", 401 tabExpand: true, 402 inputString: "abcd", 403 initialCursor: cursorState{position: 2}, 404 expectedText: "ab cd", 405 expectedCursor: cursorState{position: 4}, 406 }, 407 { 408 name: "insert tab, expand with mixed tabs/spaces", 409 tabExpand: true, 410 inputString: "\t\tab", 411 initialCursor: cursorState{position: 2}, 412 expectedText: "\t\t ab", 413 expectedCursor: cursorState{position: 6}, 414 }, 415 } 416 417 for _, tc := range testCases { 418 t.Run(tc.name, func(t *testing.T) { 419 textTree, err := text.NewTreeFromString(tc.inputString) 420 require.NoError(t, err) 421 state := NewEditorState(100, 100, nil, nil) 422 state.documentBuffer.textTree = textTree 423 state.documentBuffer.cursor = tc.initialCursor 424 state.documentBuffer.tabSize = 4 425 state.documentBuffer.tabExpand = tc.tabExpand 426 InsertTab(state) 427 assert.Equal(t, tc.expectedText, textTree.String()) 428 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 429 }) 430 } 431 } 432 433 func TestDeleteLines(t *testing.T) { 434 testCases := []struct { 435 name string 436 inputString string 437 initialCursor cursorState 438 targetLineLocator func(LocatorParams) uint64 439 abortIfTargetIsCurrentLine bool 440 replaceWithEmptyLine bool 441 expectedCursor cursorState 442 expectedText string 443 expectedClipboard clipboard.PageContent 444 }{ 445 { 446 name: "empty", 447 inputString: "", 448 initialCursor: cursorState{position: 0}, 449 targetLineLocator: func(params LocatorParams) uint64 { 450 return locate.StartOfLineBelow(params.TextTree, 1, params.CursorPos) 451 }, 452 expectedCursor: cursorState{position: 0}, 453 expectedText: "", 454 }, 455 { 456 name: "delete single line", 457 inputString: "abcd", 458 initialCursor: cursorState{position: 2}, 459 targetLineLocator: func(params LocatorParams) uint64 { 460 return params.CursorPos 461 }, 462 expectedCursor: cursorState{position: 0}, 463 expectedText: "", 464 expectedClipboard: clipboard.PageContent{ 465 Text: "abcd", 466 Linewise: true, 467 }, 468 }, 469 { 470 name: "delete single line, abort if same line", 471 inputString: "abcd", 472 initialCursor: cursorState{position: 2}, 473 targetLineLocator: func(params LocatorParams) uint64 { 474 return params.CursorPos 475 }, 476 abortIfTargetIsCurrentLine: true, 477 expectedCursor: cursorState{position: 2}, 478 expectedText: "abcd", 479 }, 480 { 481 name: "delete single line, first line", 482 inputString: "abcd\nefgh\nijk", 483 initialCursor: cursorState{position: 2}, 484 targetLineLocator: func(params LocatorParams) uint64 { 485 return params.CursorPos 486 }, 487 expectedCursor: cursorState{position: 0}, 488 expectedText: "efgh\nijk", 489 expectedClipboard: clipboard.PageContent{ 490 Text: "abcd", 491 Linewise: true, 492 }, 493 }, 494 { 495 name: "delete single line, interior line", 496 inputString: "abcd\nefgh\nijk", 497 initialCursor: cursorState{position: 6}, 498 targetLineLocator: func(params LocatorParams) uint64 { 499 return params.CursorPos 500 }, 501 expectedCursor: cursorState{position: 5}, 502 expectedText: "abcd\nijk", 503 expectedClipboard: clipboard.PageContent{ 504 Text: "efgh", 505 Linewise: true, 506 }, 507 }, 508 { 509 name: "delete single line, last line", 510 inputString: "abcd\nefgh\nijk", 511 initialCursor: cursorState{position: 12}, 512 targetLineLocator: func(params LocatorParams) uint64 { 513 return params.CursorPos 514 }, 515 expectedCursor: cursorState{position: 5}, 516 expectedText: "abcd\nefgh", 517 expectedClipboard: clipboard.PageContent{ 518 Text: "ijk", 519 Linewise: true, 520 }, 521 }, 522 { 523 name: "delete empty line", 524 inputString: "abcd\n\nefgh", 525 initialCursor: cursorState{position: 5}, 526 targetLineLocator: func(params LocatorParams) uint64 { 527 return params.CursorPos 528 }, 529 expectedCursor: cursorState{position: 5}, 530 expectedText: "abcd\nefgh", 531 expectedClipboard: clipboard.PageContent{ 532 Text: "", 533 Linewise: true, 534 }, 535 }, 536 { 537 name: "delete multiple lines down", 538 inputString: "abcd\nefgh\nijk\nlmnop", 539 initialCursor: cursorState{position: 0}, 540 targetLineLocator: func(params LocatorParams) uint64 { 541 return locate.StartOfLineBelow(params.TextTree, 2, params.CursorPos) 542 }, 543 expectedCursor: cursorState{position: 0}, 544 expectedText: "lmnop", 545 expectedClipboard: clipboard.PageContent{ 546 Text: "abcd\nefgh\nijk", 547 Linewise: true, 548 }, 549 }, 550 { 551 name: "delete multiple lines up", 552 inputString: "abcd\nefgh\nijk\nlmnop", 553 initialCursor: cursorState{position: 16}, 554 targetLineLocator: func(params LocatorParams) uint64 { 555 return locate.StartOfLineAbove(params.TextTree, 2, params.CursorPos) 556 }, 557 expectedCursor: cursorState{position: 0}, 558 expectedText: "abcd", 559 expectedClipboard: clipboard.PageContent{ 560 Text: "efgh\nijk\nlmnop", 561 Linewise: true, 562 }, 563 }, 564 { 565 name: "replace with empty line, empty document", 566 inputString: "", 567 initialCursor: cursorState{position: 0}, 568 targetLineLocator: func(params LocatorParams) uint64 { 569 return locate.StartOfLineBelow(params.TextTree, 1, params.CursorPos) 570 }, 571 replaceWithEmptyLine: true, 572 expectedCursor: cursorState{position: 0}, 573 expectedText: "", 574 }, 575 { 576 name: "replace with empty line, on first line", 577 inputString: "abc\nefgh", 578 initialCursor: cursorState{position: 0}, 579 targetLineLocator: func(params LocatorParams) uint64 { 580 return params.CursorPos 581 }, 582 replaceWithEmptyLine: true, 583 expectedCursor: cursorState{position: 0}, 584 expectedText: "\nefgh", 585 expectedClipboard: clipboard.PageContent{ 586 Text: "abc", 587 Linewise: true, 588 }, 589 }, 590 { 591 name: "replace with empty line, on middle line", 592 inputString: "abc\nefg\nhij", 593 initialCursor: cursorState{position: 5}, 594 targetLineLocator: func(params LocatorParams) uint64 { 595 return params.CursorPos 596 }, 597 replaceWithEmptyLine: true, 598 expectedCursor: cursorState{position: 4}, 599 expectedText: "abc\n\nhij", 600 expectedClipboard: clipboard.PageContent{ 601 Text: "efg", 602 Linewise: true, 603 }, 604 }, 605 { 606 name: "replace with empty line, on empty line", 607 inputString: "abc\n\n\nhij", 608 initialCursor: cursorState{position: 4}, 609 targetLineLocator: func(params LocatorParams) uint64 { 610 return params.CursorPos 611 }, 612 replaceWithEmptyLine: true, 613 expectedCursor: cursorState{position: 4}, 614 expectedText: "abc\n\n\nhij", 615 expectedClipboard: clipboard.PageContent{ 616 Text: "", 617 Linewise: true, 618 }, 619 }, 620 { 621 name: "replace with empty line, on last line", 622 inputString: "abc\nefg\nhij", 623 initialCursor: cursorState{position: 8}, 624 targetLineLocator: func(params LocatorParams) uint64 { 625 return params.CursorPos 626 }, 627 replaceWithEmptyLine: true, 628 expectedCursor: cursorState{position: 8}, 629 expectedText: "abc\nefg\n", 630 expectedClipboard: clipboard.PageContent{ 631 Text: "hij", 632 Linewise: true, 633 }, 634 }, 635 { 636 name: "replace with empty line, multiple lines selected", 637 inputString: "abc\nefg\nhij\nlmnop", 638 initialCursor: cursorState{position: 5}, 639 targetLineLocator: func(params LocatorParams) uint64 { return 9 }, 640 replaceWithEmptyLine: true, 641 expectedCursor: cursorState{position: 4}, 642 expectedText: "abc\n\nlmnop", 643 expectedClipboard: clipboard.PageContent{ 644 Text: "efg\nhij", 645 Linewise: true, 646 }, 647 }, 648 } 649 650 for _, tc := range testCases { 651 t.Run(tc.name, func(t *testing.T) { 652 textTree, err := text.NewTreeFromString(tc.inputString) 653 require.NoError(t, err) 654 state := NewEditorState(100, 100, nil, nil) 655 state.documentBuffer.textTree = textTree 656 state.documentBuffer.cursor = tc.initialCursor 657 DeleteLines(state, tc.targetLineLocator, tc.abortIfTargetIsCurrentLine, tc.replaceWithEmptyLine, clipboard.PageDefault) 658 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 659 assert.Equal(t, tc.expectedText, textTree.String()) 660 assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault)) 661 }) 662 } 663 } 664 665 func TestReplaceChar(t *testing.T) { 666 testCases := []struct { 667 name string 668 inputString string 669 initialCursor cursorState 670 newChar rune 671 autoIndent bool 672 tabExpand bool 673 expectedCursor cursorState 674 expectedText string 675 }{ 676 { 677 name: "empty", 678 inputString: "", 679 newChar: 'a', 680 initialCursor: cursorState{position: 0}, 681 expectedCursor: cursorState{position: 0}, 682 expectedText: "", 683 }, 684 { 685 name: "replace char", 686 inputString: "abcd", 687 newChar: 'x', 688 initialCursor: cursorState{position: 1}, 689 expectedCursor: cursorState{position: 1}, 690 expectedText: "axcd", 691 }, 692 { 693 name: "empty line", 694 inputString: "ab\n\ncd", 695 newChar: 'x', 696 initialCursor: cursorState{position: 3}, 697 expectedCursor: cursorState{position: 3}, 698 expectedText: "ab\n\ncd", 699 }, 700 { 701 name: "insert newline", 702 inputString: "abcd", 703 newChar: '\n', 704 initialCursor: cursorState{position: 2}, 705 expectedCursor: cursorState{position: 3}, 706 expectedText: "ab\nd", 707 }, 708 { 709 name: "insert newline with autoindent", 710 inputString: "\tabcd", 711 newChar: '\n', 712 initialCursor: cursorState{position: 2}, 713 autoIndent: true, 714 expectedCursor: cursorState{position: 4}, 715 expectedText: "\ta\n\tcd", 716 }, 717 { 718 name: "insert tab, no expand", 719 inputString: "abcd", 720 newChar: '\t', 721 initialCursor: cursorState{position: 2}, 722 expectedCursor: cursorState{position: 2}, 723 expectedText: "ab\td", 724 }, 725 { 726 name: "insert tab, expand", 727 inputString: "abcd", 728 newChar: '\t', 729 initialCursor: cursorState{position: 2}, 730 tabExpand: true, 731 expectedCursor: cursorState{position: 3}, 732 expectedText: "ab d", 733 }, 734 } 735 736 for _, tc := range testCases { 737 t.Run(tc.name, func(t *testing.T) { 738 textTree, err := text.NewTreeFromString(tc.inputString) 739 require.NoError(t, err) 740 state := NewEditorState(100, 100, nil, nil) 741 state.documentBuffer.textTree = textTree 742 state.documentBuffer.cursor = tc.initialCursor 743 state.documentBuffer.autoIndent = tc.autoIndent 744 state.documentBuffer.tabExpand = tc.tabExpand 745 state.documentBuffer.tabSize = 4 746 ReplaceChar(state, tc.newChar) 747 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 748 assert.Equal(t, tc.expectedText, textTree.String()) 749 }) 750 } 751 } 752 753 func TestToggleCaseAtCursor(t *testing.T) { 754 testCases := []struct { 755 name string 756 inputString string 757 initialCursor cursorState 758 expectedCursor cursorState 759 expectedText string 760 }{ 761 { 762 name: "empty", 763 inputString: "", 764 initialCursor: cursorState{position: 0}, 765 expectedCursor: cursorState{position: 0}, 766 expectedText: "", 767 }, 768 { 769 name: "toggle lowercase to uppercase", 770 inputString: "abcd", 771 initialCursor: cursorState{position: 1}, 772 expectedCursor: cursorState{position: 2}, 773 expectedText: "aBcd", 774 }, 775 { 776 name: "toggle uppercase to lowercase", 777 inputString: "ABCD", 778 initialCursor: cursorState{position: 1}, 779 expectedCursor: cursorState{position: 2}, 780 expectedText: "AbCD", 781 }, 782 { 783 name: "toggle number", 784 inputString: "1234", 785 initialCursor: cursorState{position: 1}, 786 expectedCursor: cursorState{position: 2}, 787 expectedText: "1234", 788 }, 789 { 790 name: "empty line", 791 inputString: "ab\n\ncd", 792 initialCursor: cursorState{position: 3}, 793 expectedCursor: cursorState{position: 3}, 794 expectedText: "ab\n\ncd", 795 }, 796 { 797 name: "toggle at end of line", 798 inputString: "abcd\nefgh", 799 initialCursor: cursorState{position: 3}, 800 expectedCursor: cursorState{position: 3}, 801 expectedText: "abcD\nefgh", 802 }, 803 { 804 name: "toggle at end of document", 805 inputString: "abcd", 806 initialCursor: cursorState{position: 3}, 807 expectedCursor: cursorState{position: 3}, 808 expectedText: "abcD", 809 }, 810 } 811 812 for _, tc := range testCases { 813 t.Run(tc.name, func(t *testing.T) { 814 textTree, err := text.NewTreeFromString(tc.inputString) 815 require.NoError(t, err) 816 state := NewEditorState(100, 100, nil, nil) 817 state.documentBuffer.textTree = textTree 818 state.documentBuffer.cursor = tc.initialCursor 819 ToggleCaseAtCursor(state) 820 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 821 assert.Equal(t, tc.expectedText, textTree.String()) 822 }) 823 } 824 } 825 826 func TestToggleCaseInSelection(t *testing.T) { 827 testCases := []struct { 828 name string 829 inputString string 830 selectionMode selection.Mode 831 selectionStartPos uint64 832 selectionEndPos uint64 833 expectedCursor cursorState 834 expectedText string 835 }{ 836 { 837 name: "empty", 838 inputString: "", 839 selectionMode: selection.ModeChar, 840 selectionStartPos: 0, 841 selectionEndPos: 0, 842 expectedCursor: cursorState{position: 0}, 843 expectedText: "", 844 }, 845 { 846 name: "select single character", 847 inputString: "abcdefgh", 848 selectionMode: selection.ModeChar, 849 selectionStartPos: 2, 850 selectionEndPos: 3, 851 expectedCursor: cursorState{position: 2}, 852 expectedText: "abCdefgh", 853 }, 854 { 855 name: "select multiple characters", 856 inputString: "abcdefgh", 857 selectionMode: selection.ModeLine, 858 selectionStartPos: 2, 859 selectionEndPos: 6, 860 expectedCursor: cursorState{position: 2}, 861 expectedText: "abCDEFgh", 862 }, 863 } 864 865 for _, tc := range testCases { 866 t.Run(tc.name, func(t *testing.T) { 867 textTree, err := text.NewTreeFromString(tc.inputString) 868 require.NoError(t, err) 869 state := NewEditorState(100, 100, nil, nil) 870 state.documentBuffer.textTree = textTree 871 state.documentBuffer.cursor = cursorState{position: tc.selectionStartPos} 872 selectionEndLoc := func(p LocatorParams) uint64 { return tc.selectionEndPos } 873 ToggleCaseInSelection(state, selectionEndLoc) 874 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 875 assert.Equal(t, tc.expectedText, textTree.String()) 876 }) 877 } 878 } 879 880 func TestIndentLines(t *testing.T) { 881 testCases := []struct { 882 name string 883 inputString string 884 cursorPos uint64 885 targetLinePos uint64 886 count uint64 887 tabExpand bool 888 expectedCursor cursorState 889 expectedText string 890 }{ 891 { 892 name: "empty", 893 inputString: "", 894 cursorPos: 0, 895 targetLinePos: 0, 896 count: 1, 897 expectedCursor: cursorState{position: 0}, 898 expectedText: "", 899 }, 900 { 901 name: "empty line", 902 inputString: "abc\n\ndef", 903 cursorPos: 4, 904 targetLinePos: 4, 905 count: 1, 906 expectedCursor: cursorState{position: 4}, 907 expectedText: "abc\n\ndef", 908 }, 909 { 910 name: "empty line with carriage return", 911 inputString: "abc\r\n\r\ndef", 912 cursorPos: 5, 913 targetLinePos: 5, 914 count: 1, 915 expectedCursor: cursorState{position: 5}, 916 expectedText: "abc\r\n\r\ndef", 917 }, 918 { 919 name: "line with single character", 920 inputString: "a", 921 cursorPos: 0, 922 targetLinePos: 0, 923 count: 1, 924 expectedCursor: cursorState{position: 1}, 925 expectedText: "\ta", 926 }, 927 { 928 name: "line with single character, tab expand", 929 tabExpand: true, 930 inputString: "a", 931 cursorPos: 0, 932 targetLinePos: 0, 933 count: 1, 934 expectedCursor: cursorState{position: 4}, 935 expectedText: " a", 936 }, 937 { 938 name: "first line, cursor at start", 939 inputString: "abc\ndef\nghi", 940 cursorPos: 0, 941 targetLinePos: 0, 942 count: 1, 943 expectedCursor: cursorState{position: 1}, 944 expectedText: "\tabc\ndef\nghi", 945 }, 946 { 947 name: "first line, cursor past start", 948 inputString: "abc\ndef\nghi", 949 cursorPos: 1, 950 targetLinePos: 1, 951 count: 1, 952 expectedCursor: cursorState{position: 1}, 953 expectedText: "\tabc\ndef\nghi", 954 }, 955 { 956 name: "second line, cursor at start", 957 inputString: "abc\ndef\nghi", 958 cursorPos: 4, 959 targetLinePos: 4, 960 count: 1, 961 expectedCursor: cursorState{position: 5}, 962 expectedText: "abc\n\tdef\nghi", 963 }, 964 { 965 name: "second line, cursor past start", 966 inputString: "abc\ndef\nghi", 967 cursorPos: 6, 968 targetLinePos: 6, 969 count: 1, 970 expectedCursor: cursorState{position: 5}, 971 expectedText: "abc\n\tdef\nghi", 972 }, 973 { 974 name: "last line, cursor at end", 975 inputString: "abc\ndef\nghi", 976 cursorPos: 11, 977 targetLinePos: 11, 978 count: 1, 979 expectedCursor: cursorState{position: 9}, 980 expectedText: "abc\ndef\n\tghi", 981 }, 982 { 983 name: "tab expand, aligned", 984 inputString: "abc\ndef\nghi", 985 cursorPos: 6, 986 targetLinePos: 6, 987 count: 1, 988 tabExpand: true, 989 expectedCursor: cursorState{position: 8}, 990 expectedText: "abc\n def\nghi", 991 }, 992 { 993 name: "tab expand, line with whitespace at start", 994 inputString: "abc\n def\nghi", 995 cursorPos: 7, 996 targetLinePos: 7, 997 count: 1, 998 tabExpand: true, 999 expectedCursor: cursorState{position: 10}, 1000 expectedText: "abc\n def\nghi", 1001 }, 1002 { 1003 name: "multiple lines", 1004 inputString: "ab\ncd\nef\ngh", 1005 cursorPos: 4, 1006 targetLinePos: 7, 1007 count: 1, 1008 expectedCursor: cursorState{position: 4}, 1009 expectedText: "ab\n\tcd\n\tef\ngh", 1010 }, 1011 { 1012 name: "repeat count times", 1013 inputString: "ab\ncd\nef\ngh", 1014 cursorPos: 4, 1015 targetLinePos: 7, 1016 count: 3, 1017 expectedCursor: cursorState{position: 6}, 1018 expectedText: "ab\n\t\t\tcd\n\t\t\tef\ngh", 1019 }, 1020 { 1021 name: "tab expand, repeat count times", 1022 inputString: "abc\n def\nghi", 1023 cursorPos: 7, 1024 targetLinePos: 7, 1025 count: 3, 1026 tabExpand: true, 1027 expectedCursor: cursorState{position: 18}, 1028 expectedText: "abc\n def\nghi", 1029 }, 1030 } 1031 1032 for _, tc := range testCases { 1033 t.Run(tc.name, func(t *testing.T) { 1034 textTree, err := text.NewTreeFromString(tc.inputString) 1035 require.NoError(t, err) 1036 state := NewEditorState(100, 100, nil, nil) 1037 state.documentBuffer.textTree = textTree 1038 state.documentBuffer.cursor = cursorState{position: tc.cursorPos} 1039 state.documentBuffer.tabExpand = tc.tabExpand 1040 targetLineLoc := func(p LocatorParams) uint64 { return tc.targetLinePos } 1041 IndentLines(state, targetLineLoc, tc.count) 1042 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1043 assert.Equal(t, tc.expectedText, textTree.String()) 1044 }) 1045 } 1046 } 1047 1048 func TestOutdentLines(t *testing.T) { 1049 testCases := []struct { 1050 name string 1051 inputString string 1052 cursorPos uint64 1053 targetLinePos uint64 1054 count uint64 1055 tabSize uint64 1056 expectedCursor cursorState 1057 expectedText string 1058 }{ 1059 { 1060 name: "empty", 1061 inputString: "", 1062 cursorPos: 0, 1063 targetLinePos: 0, 1064 count: 1, 1065 tabSize: 4, 1066 expectedCursor: cursorState{position: 0}, 1067 expectedText: "", 1068 }, 1069 { 1070 name: "outdent first line starting with a single tab, on tab", 1071 inputString: "\tabc", 1072 cursorPos: 0, 1073 targetLinePos: 0, 1074 count: 1, 1075 tabSize: 4, 1076 expectedCursor: cursorState{position: 0}, 1077 expectedText: "abc", 1078 }, 1079 { 1080 name: "outdent first line starting with a single tab, on start of text", 1081 inputString: "\tabc", 1082 cursorPos: 1, 1083 targetLinePos: 1, 1084 count: 1, 1085 tabSize: 4, 1086 expectedCursor: cursorState{position: 0}, 1087 expectedText: "abc", 1088 }, 1089 { 1090 name: "outdent first line starting with a single tab, on end of text", 1091 inputString: "\tabc", 1092 cursorPos: 3, 1093 targetLinePos: 3, 1094 count: 1, 1095 tabSize: 4, 1096 expectedCursor: cursorState{position: 0}, 1097 expectedText: "abc", 1098 }, 1099 { 1100 name: "outdent first line starting with multiple tabs", 1101 inputString: "\t\t\tabc", 1102 cursorPos: 4, 1103 targetLinePos: 4, 1104 count: 1, 1105 tabSize: 4, 1106 expectedCursor: cursorState{position: 2}, 1107 expectedText: "\t\tabc", 1108 }, 1109 { 1110 name: "outdent first line starting with spaces less than tabsize", 1111 inputString: " abc", 1112 cursorPos: 2, 1113 targetLinePos: 2, 1114 count: 1, 1115 tabSize: 4, 1116 expectedCursor: cursorState{position: 0}, 1117 expectedText: "abc", 1118 }, 1119 { 1120 name: "outdent first line starting with spaces equal to tabsize", 1121 inputString: " abc", 1122 cursorPos: 2, 1123 targetLinePos: 2, 1124 count: 1, 1125 tabSize: 4, 1126 expectedCursor: cursorState{position: 0}, 1127 expectedText: "abc", 1128 }, 1129 { 1130 name: "outdent first line starting with spaces greater than tabsize", 1131 inputString: " abc", 1132 cursorPos: 2, 1133 targetLinePos: 2, 1134 count: 1, 1135 tabSize: 2, 1136 expectedCursor: cursorState{position: 2}, 1137 expectedText: " abc", 1138 }, 1139 { 1140 name: "outdent empty line", 1141 inputString: "abc\n\ndef", 1142 cursorPos: 5, 1143 targetLinePos: 5, 1144 count: 1, 1145 tabSize: 4, 1146 expectedCursor: cursorState{position: 5}, 1147 expectedText: "abc\n\ndef", 1148 }, 1149 { 1150 name: "outdent line with only space", 1151 inputString: "abc\n \ndef", 1152 cursorPos: 5, 1153 targetLinePos: 5, 1154 count: 1, 1155 tabSize: 4, 1156 expectedCursor: cursorState{position: 6}, 1157 expectedText: "abc\n \ndef", 1158 }, 1159 { 1160 name: "outdent middle line", 1161 inputString: "abc\n\t\tdef\nghi", 1162 cursorPos: 7, 1163 targetLinePos: 7, 1164 count: 1, 1165 tabSize: 4, 1166 expectedCursor: cursorState{position: 5}, 1167 expectedText: "abc\n\tdef\nghi", 1168 }, 1169 { 1170 name: "outdent mix of tabs and spaces", 1171 inputString: " \t abc", 1172 cursorPos: 5, 1173 targetLinePos: 5, 1174 count: 1, 1175 tabSize: 4, 1176 expectedCursor: cursorState{position: 1}, 1177 expectedText: " abc", 1178 }, 1179 { 1180 name: "multiple lines", 1181 inputString: "ab\n\tcd\n\t\tef\ngh", 1182 cursorPos: 5, 1183 targetLinePos: 8, 1184 count: 1, 1185 tabSize: 4, 1186 expectedCursor: cursorState{position: 3}, 1187 expectedText: "ab\ncd\n\tef\ngh", 1188 }, 1189 { 1190 name: "repeat count times", 1191 inputString: "ab\n\t\t\tcd\n\t\t\t\tef\ngh", 1192 cursorPos: 5, 1193 targetLinePos: 14, 1194 count: 3, 1195 tabSize: 4, 1196 expectedCursor: cursorState{position: 3}, 1197 expectedText: "ab\ncd\n\tef\ngh", 1198 }, 1199 } 1200 1201 for _, tc := range testCases { 1202 t.Run(tc.name, func(t *testing.T) { 1203 textTree, err := text.NewTreeFromString(tc.inputString) 1204 require.NoError(t, err) 1205 state := NewEditorState(100, 100, nil, nil) 1206 state.documentBuffer.textTree = textTree 1207 state.documentBuffer.cursor = cursorState{position: tc.cursorPos} 1208 state.documentBuffer.tabSize = tc.tabSize 1209 targetLineLoc := func(p LocatorParams) uint64 { return tc.targetLinePos } 1210 OutdentLines(state, targetLineLoc, tc.count) 1211 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1212 assert.Equal(t, tc.expectedText, textTree.String()) 1213 }) 1214 } 1215 } 1216 1217 func TestBeginNewLineAbove(t *testing.T) { 1218 testCases := []struct { 1219 name string 1220 inputString string 1221 cursorPos uint64 1222 autoIndent bool 1223 expectedCursor cursorState 1224 expectedText string 1225 }{ 1226 { 1227 name: "empty, no autoindent", 1228 inputString: "", 1229 cursorPos: 0, 1230 autoIndent: false, 1231 expectedCursor: cursorState{position: 0}, 1232 expectedText: "\n", 1233 }, 1234 { 1235 name: "empty, autoindent", 1236 inputString: "", 1237 cursorPos: 0, 1238 autoIndent: true, 1239 expectedCursor: cursorState{position: 0}, 1240 expectedText: "\n", 1241 }, 1242 { 1243 name: "multiple lines, no indentation, no autoindent", 1244 inputString: "abc\ndef\nhij", 1245 cursorPos: 5, 1246 autoIndent: false, 1247 expectedCursor: cursorState{position: 4}, 1248 expectedText: "abc\n\ndef\nhij", 1249 }, 1250 { 1251 name: "multiple lines, indentation, no autoindent", 1252 inputString: "abc\n\t\tdef\nhij", 1253 cursorPos: 5, 1254 autoIndent: false, 1255 expectedCursor: cursorState{position: 4}, 1256 expectedText: "abc\n\n\t\tdef\nhij", 1257 }, 1258 { 1259 name: "multiple lines, no indentation, autoindent", 1260 inputString: "abc\ndef\nhij", 1261 cursorPos: 5, 1262 autoIndent: true, 1263 expectedCursor: cursorState{position: 4}, 1264 expectedText: "abc\n\ndef\nhij", 1265 }, 1266 { 1267 name: "multiple lines, indentation, autoindent", 1268 inputString: "abc\n\t\tdef\nhij", 1269 cursorPos: 5, 1270 autoIndent: true, 1271 expectedCursor: cursorState{position: 6}, 1272 expectedText: "abc\n\t\t\n\t\tdef\nhij", 1273 }, 1274 } 1275 1276 for _, tc := range testCases { 1277 t.Run(tc.name, func(t *testing.T) { 1278 textTree, err := text.NewTreeFromString(tc.inputString) 1279 require.NoError(t, err) 1280 state := NewEditorState(100, 100, nil, nil) 1281 state.documentBuffer.textTree = textTree 1282 state.documentBuffer.cursor = cursorState{position: tc.cursorPos} 1283 state.documentBuffer.autoIndent = tc.autoIndent 1284 BeginNewLineAbove(state) 1285 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1286 assert.Equal(t, tc.expectedText, textTree.String()) 1287 }) 1288 } 1289 } 1290 1291 func TestJoinLines(t *testing.T) { 1292 testCases := []struct { 1293 name string 1294 inputString string 1295 initialCursor cursorState 1296 expectedText string 1297 expectedCursor cursorState 1298 }{ 1299 { 1300 name: "empty", 1301 inputString: "", 1302 initialCursor: cursorState{position: 0}, 1303 expectedText: "", 1304 expectedCursor: cursorState{position: 0}, 1305 }, 1306 { 1307 name: "two lines, no indentation, cursor at start", 1308 inputString: "abc\ndef", 1309 initialCursor: cursorState{position: 0}, 1310 expectedText: "abc def", 1311 expectedCursor: cursorState{position: 3}, 1312 }, 1313 { 1314 name: "two lines, no indentation, cursor before newline", 1315 inputString: "abc\ndef", 1316 initialCursor: cursorState{position: 2}, 1317 expectedText: "abc def", 1318 expectedCursor: cursorState{position: 3}, 1319 }, 1320 { 1321 name: "two lines, no indentation, cursor on newline", 1322 inputString: "abc\ndef", 1323 initialCursor: cursorState{position: 3}, 1324 expectedText: "abc def", 1325 expectedCursor: cursorState{position: 3}, 1326 }, 1327 { 1328 name: "two lines, second line indented with spaces", 1329 inputString: "abc\n def", 1330 initialCursor: cursorState{position: 2}, 1331 expectedText: "abc def", 1332 expectedCursor: cursorState{position: 3}, 1333 }, 1334 { 1335 name: "two lines, second line indented with tabs", 1336 inputString: "abc\n\t\tdef", 1337 initialCursor: cursorState{position: 2}, 1338 expectedText: "abc def", 1339 expectedCursor: cursorState{position: 3}, 1340 }, 1341 { 1342 name: "multiple lines, on last line", 1343 inputString: "abc\ndef\nghijk", 1344 initialCursor: cursorState{position: 10}, 1345 expectedText: "abc\ndef\nghijk", 1346 expectedCursor: cursorState{position: 10}, 1347 }, 1348 { 1349 name: "second-to-last line, last line is whitespace", 1350 inputString: "abc\n ", 1351 initialCursor: cursorState{position: 2}, 1352 expectedText: "abc", 1353 expectedCursor: cursorState{position: 2}, 1354 }, 1355 { 1356 name: "before empty line", 1357 inputString: "abc\n\ndef", 1358 initialCursor: cursorState{position: 1}, 1359 expectedText: "abc\ndef", 1360 expectedCursor: cursorState{position: 2}, 1361 }, 1362 { 1363 name: "before multiple empty lines", 1364 inputString: "abc\n\n\n\ndef", 1365 initialCursor: cursorState{position: 1}, 1366 expectedText: "abc\n\n\ndef", 1367 expectedCursor: cursorState{position: 2}, 1368 }, 1369 { 1370 name: "on empty line before non-empty line", 1371 inputString: "abc\n\ndef\nxyz", 1372 initialCursor: cursorState{position: 4}, 1373 expectedText: "abc\ndef\nxyz", 1374 expectedCursor: cursorState{position: 4}, 1375 }, 1376 { 1377 name: "on empty line before empty line", 1378 inputString: "abc\n\n\n\ndef", 1379 initialCursor: cursorState{position: 4}, 1380 expectedText: "abc\n\n\ndef", 1381 expectedCursor: cursorState{position: 4}, 1382 }, 1383 { 1384 name: "before line all whitespace", 1385 inputString: "abc\n \ndef", 1386 initialCursor: cursorState{position: 2}, 1387 expectedText: "abc\ndef", 1388 expectedCursor: cursorState{position: 2}, 1389 }, 1390 } 1391 1392 for _, tc := range testCases { 1393 t.Run(tc.name, func(t *testing.T) { 1394 textTree, err := text.NewTreeFromString(tc.inputString) 1395 require.NoError(t, err) 1396 state := NewEditorState(100, 100, nil, nil) 1397 state.documentBuffer.textTree = textTree 1398 state.documentBuffer.cursor = tc.initialCursor 1399 JoinLines(state) 1400 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1401 assert.Equal(t, tc.expectedText, textTree.String()) 1402 }) 1403 } 1404 } 1405 1406 func TestCopyRange(t *testing.T) { 1407 testCases := []struct { 1408 name string 1409 inputString string 1410 loc RangeLocator 1411 expectedClipboard clipboard.PageContent 1412 }{ 1413 { 1414 name: "empty", 1415 inputString: "", 1416 loc: func(p LocatorParams) (uint64, uint64) { return 0, 0 }, 1417 expectedClipboard: clipboard.PageContent{}, 1418 }, 1419 { 1420 name: "start pos equal to end pos", 1421 inputString: "abcd", 1422 loc: func(p LocatorParams) (uint64, uint64) { return 2, 2 }, 1423 expectedClipboard: clipboard.PageContent{}, 1424 }, 1425 { 1426 name: "start pos after end pos", 1427 inputString: "abcd", 1428 loc: func(p LocatorParams) (uint64, uint64) { return 3, 2 }, 1429 expectedClipboard: clipboard.PageContent{}, 1430 }, 1431 { 1432 name: "start pos before end pos", 1433 inputString: "abcd", 1434 loc: func(p LocatorParams) (uint64, uint64) { return 1, 3 }, 1435 expectedClipboard: clipboard.PageContent{Text: "bc"}, 1436 }, 1437 } 1438 1439 for _, tc := range testCases { 1440 t.Run(tc.name, func(t *testing.T) { 1441 textTree, err := text.NewTreeFromString(tc.inputString) 1442 require.NoError(t, err) 1443 state := NewEditorState(100, 100, nil, nil) 1444 state.documentBuffer.textTree = textTree 1445 CopyRange(state, clipboard.PageDefault, tc.loc) 1446 assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault)) 1447 }) 1448 } 1449 } 1450 1451 func TestCopyLine(t *testing.T) { 1452 testCases := []struct { 1453 name string 1454 inputString string 1455 initialCursor cursorState 1456 expectedClipboard clipboard.PageContent 1457 }{ 1458 { 1459 name: "empty", 1460 inputString: "", 1461 initialCursor: cursorState{position: 0}, 1462 expectedClipboard: clipboard.PageContent{ 1463 Linewise: true, 1464 }, 1465 }, 1466 { 1467 name: "single line, cursor at start", 1468 inputString: "abcd", 1469 initialCursor: cursorState{position: 0}, 1470 expectedClipboard: clipboard.PageContent{ 1471 Text: "abcd", 1472 Linewise: true, 1473 }, 1474 }, 1475 { 1476 name: "single line, cursor in middle", 1477 inputString: "abcd", 1478 initialCursor: cursorState{position: 2}, 1479 expectedClipboard: clipboard.PageContent{ 1480 Text: "abcd", 1481 Linewise: true, 1482 }, 1483 }, 1484 { 1485 name: "single line, cursor at end", 1486 inputString: "abcd", 1487 initialCursor: cursorState{position: 4}, 1488 expectedClipboard: clipboard.PageContent{ 1489 Text: "abcd", 1490 Linewise: true, 1491 }, 1492 }, 1493 { 1494 name: "multiple lines, cursor on first line", 1495 inputString: "abcd\nefgh\nijkl", 1496 initialCursor: cursorState{position: 2}, 1497 expectedClipboard: clipboard.PageContent{ 1498 Text: "abcd", 1499 Linewise: true, 1500 }, 1501 }, 1502 { 1503 name: "multiple lines, cursor on middle line", 1504 inputString: "abcd\nefgh\nijkl", 1505 initialCursor: cursorState{position: 5}, 1506 expectedClipboard: clipboard.PageContent{ 1507 Text: "efgh", 1508 Linewise: true, 1509 }, 1510 }, 1511 { 1512 name: "multiple lines, cursor on last line", 1513 inputString: "abcd\nefgh\nijkl", 1514 initialCursor: cursorState{position: 10}, 1515 expectedClipboard: clipboard.PageContent{ 1516 Text: "ijkl", 1517 Linewise: true, 1518 }, 1519 }, 1520 { 1521 name: "cursor on empty line", 1522 inputString: "abcd\n\n\nefgh", 1523 initialCursor: cursorState{position: 5}, 1524 expectedClipboard: clipboard.PageContent{ 1525 Text: "", 1526 Linewise: true, 1527 }, 1528 }, 1529 { 1530 name: "multi-byte unicode", 1531 inputString: "丂丄丅丆丏 ¢ह€한", 1532 initialCursor: cursorState{position: 2}, 1533 expectedClipboard: clipboard.PageContent{ 1534 Text: "丂丄丅丆丏 ¢ह€한", 1535 Linewise: true, 1536 }, 1537 }, 1538 } 1539 1540 for _, tc := range testCases { 1541 t.Run(tc.name, func(t *testing.T) { 1542 textTree, err := text.NewTreeFromString(tc.inputString) 1543 require.NoError(t, err) 1544 state := NewEditorState(100, 100, nil, nil) 1545 state.documentBuffer.textTree = textTree 1546 state.documentBuffer.cursor = tc.initialCursor 1547 CopyLine(state, clipboard.PageDefault) 1548 assert.Equal(t, tc.initialCursor, state.documentBuffer.cursor) 1549 assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault)) 1550 }) 1551 } 1552 } 1553 1554 func TestCopySelection(t *testing.T) { 1555 testCases := []struct { 1556 name string 1557 inputString string 1558 selectionMode selection.Mode 1559 cursorStartPos uint64 1560 cursorEndPos uint64 1561 expectedCursor cursorState 1562 expectedText string 1563 expectedClipboard clipboard.PageContent 1564 }{ 1565 { 1566 name: "empty document, select charwise", 1567 inputString: "", 1568 selectionMode: selection.ModeChar, 1569 cursorStartPos: 0, 1570 cursorEndPos: 0, 1571 expectedCursor: cursorState{position: 0}, 1572 expectedText: "", 1573 expectedClipboard: clipboard.PageContent{Text: ""}, 1574 }, 1575 { 1576 name: "empty document, select linewise", 1577 inputString: "", 1578 selectionMode: selection.ModeLine, 1579 cursorStartPos: 0, 1580 cursorEndPos: 0, 1581 expectedCursor: cursorState{position: 0}, 1582 expectedText: "", 1583 expectedClipboard: clipboard.PageContent{ 1584 Text: "", 1585 Linewise: true, 1586 }, 1587 }, 1588 { 1589 name: "nonempty charwise selection", 1590 inputString: "abcd1234", 1591 selectionMode: selection.ModeChar, 1592 cursorStartPos: 1, 1593 cursorEndPos: 3, 1594 expectedCursor: cursorState{position: 1}, 1595 expectedText: "abcd1234", 1596 expectedClipboard: clipboard.PageContent{Text: "bcd"}, 1597 }, 1598 { 1599 name: "nonempty linewise selection", 1600 inputString: "ab\ncde\nfgh\n12\n34", 1601 selectionMode: selection.ModeLine, 1602 cursorStartPos: 4, 1603 cursorEndPos: 8, 1604 expectedCursor: cursorState{position: 3}, 1605 expectedText: "ab\ncde\nfgh\n12\n34", 1606 expectedClipboard: clipboard.PageContent{ 1607 Text: "cde\nfgh", 1608 Linewise: true, 1609 }, 1610 }, 1611 { 1612 name: "empty line, select charwise", 1613 inputString: "abc\n\ndef", 1614 selectionMode: selection.ModeChar, 1615 cursorStartPos: 4, 1616 cursorEndPos: 4, 1617 expectedCursor: cursorState{position: 4}, 1618 expectedText: "abc\n\ndef", 1619 expectedClipboard: clipboard.PageContent{Text: "\n"}, 1620 }, 1621 { 1622 name: "empty line, select linewise", 1623 inputString: "abc\n\ndef", 1624 selectionMode: selection.ModeLine, 1625 cursorStartPos: 4, 1626 cursorEndPos: 4, 1627 expectedCursor: cursorState{position: 4}, 1628 expectedText: "abc\n\ndef", 1629 expectedClipboard: clipboard.PageContent{ 1630 Text: "", 1631 Linewise: true, 1632 }, 1633 }, 1634 } 1635 1636 for _, tc := range testCases { 1637 t.Run(tc.name, func(t *testing.T) { 1638 textTree, err := text.NewTreeFromString(tc.inputString) 1639 require.NoError(t, err) 1640 state := NewEditorState(100, 100, nil, nil) 1641 state.documentBuffer.textTree = textTree 1642 state.documentBuffer.selector.Start(tc.selectionMode, tc.cursorStartPos) 1643 state.documentBuffer.cursor = cursorState{position: tc.cursorEndPos} 1644 CopySelection(state, clipboard.PageDefault) 1645 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1646 assert.Equal(t, tc.expectedText, textTree.String()) 1647 assert.Equal(t, tc.expectedClipboard, state.clipboard.Get(clipboard.PageDefault)) 1648 assert.Equal(t, false, state.documentBuffer.undoLog.HasUnsavedChanges()) 1649 }) 1650 } 1651 } 1652 1653 func TestPasteAfterCursor(t *testing.T) { 1654 testCases := []struct { 1655 name string 1656 inputString string 1657 initialCursor cursorState 1658 clipboard clipboard.PageContent 1659 expectedCursor cursorState 1660 expectedText string 1661 }{ 1662 { 1663 name: "empty document, empty clipboard", 1664 inputString: "", 1665 initialCursor: cursorState{position: 0}, 1666 clipboard: clipboard.PageContent{}, 1667 expectedCursor: cursorState{position: 0}, 1668 expectedText: "", 1669 }, 1670 { 1671 name: "empty document, empty clipboard insert on next line", 1672 inputString: "", 1673 initialCursor: cursorState{position: 0}, 1674 clipboard: clipboard.PageContent{ 1675 Linewise: true, 1676 }, 1677 expectedCursor: cursorState{position: 1}, 1678 expectedText: "\n", 1679 }, 1680 { 1681 name: "paste after cursor", 1682 inputString: "abcd", 1683 initialCursor: cursorState{position: 2}, 1684 clipboard: clipboard.PageContent{ 1685 Text: "xyz", 1686 Linewise: false, 1687 }, 1688 expectedCursor: cursorState{position: 5}, 1689 expectedText: "abcxyzd", 1690 }, 1691 { 1692 name: "paste after cursor insert on next line", 1693 inputString: "abcd", 1694 initialCursor: cursorState{position: 2}, 1695 clipboard: clipboard.PageContent{ 1696 Text: "xyz", 1697 Linewise: true, 1698 }, 1699 expectedCursor: cursorState{position: 5}, 1700 expectedText: "abcd\nxyz", 1701 }, 1702 { 1703 name: "paste newline after cursor", 1704 inputString: "abcd", 1705 initialCursor: cursorState{position: 1}, 1706 clipboard: clipboard.PageContent{ 1707 Text: "\n", 1708 Linewise: false, 1709 }, 1710 expectedCursor: cursorState{position: 3}, 1711 expectedText: "ab\ncd", 1712 }, 1713 { 1714 name: "multi-byte unicode", 1715 inputString: "abc", 1716 initialCursor: cursorState{position: 1}, 1717 clipboard: clipboard.PageContent{ 1718 Text: "丂丄丅丆丏 ¢ह€한", 1719 Linewise: false, 1720 }, 1721 expectedCursor: cursorState{position: 11}, 1722 expectedText: "ab丂丄丅丆丏 ¢ह€한c", 1723 }, 1724 } 1725 1726 for _, tc := range testCases { 1727 t.Run(tc.name, func(t *testing.T) { 1728 textTree, err := text.NewTreeFromString(tc.inputString) 1729 require.NoError(t, err) 1730 state := NewEditorState(100, 100, nil, nil) 1731 state.documentBuffer.textTree = textTree 1732 state.documentBuffer.cursor = tc.initialCursor 1733 state.clipboard.Set(clipboard.PageDefault, tc.clipboard) 1734 PasteAfterCursor(state, clipboard.PageDefault) 1735 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1736 assert.Equal(t, tc.expectedText, textTree.String()) 1737 }) 1738 } 1739 } 1740 1741 func TestPasteBeforeCursor(t *testing.T) { 1742 testCases := []struct { 1743 name string 1744 inputString string 1745 initialCursor cursorState 1746 clipboard clipboard.PageContent 1747 expectedCursor cursorState 1748 expectedText string 1749 }{ 1750 { 1751 name: "empty document, empty clipboard", 1752 inputString: "", 1753 initialCursor: cursorState{position: 0}, 1754 clipboard: clipboard.PageContent{}, 1755 expectedCursor: cursorState{position: 0}, 1756 expectedText: "", 1757 }, 1758 { 1759 name: "empty document, empty clipboard insert on next line", 1760 inputString: "", 1761 initialCursor: cursorState{position: 0}, 1762 clipboard: clipboard.PageContent{ 1763 Linewise: true, 1764 }, 1765 expectedCursor: cursorState{position: 0}, 1766 expectedText: "\n", 1767 }, 1768 { 1769 name: "paste before cursor", 1770 inputString: "abcd", 1771 initialCursor: cursorState{position: 2}, 1772 clipboard: clipboard.PageContent{ 1773 Text: "xyz", 1774 Linewise: false, 1775 }, 1776 expectedCursor: cursorState{position: 4}, 1777 expectedText: "abxyzcd", 1778 }, 1779 { 1780 name: "paste before cursor insert on next line", 1781 inputString: "abcd", 1782 initialCursor: cursorState{position: 2}, 1783 clipboard: clipboard.PageContent{ 1784 Text: "xyz", 1785 Linewise: true, 1786 }, 1787 expectedCursor: cursorState{position: 0}, 1788 expectedText: "xyz\nabcd", 1789 }, 1790 { 1791 name: "paste newline before cursor", 1792 inputString: "abcd", 1793 initialCursor: cursorState{position: 2}, 1794 clipboard: clipboard.PageContent{ 1795 Text: "\n", 1796 Linewise: false, 1797 }, 1798 expectedCursor: cursorState{position: 1}, 1799 expectedText: "ab\ncd", 1800 }, 1801 { 1802 name: "multi-byte unicode", 1803 inputString: "abc", 1804 initialCursor: cursorState{position: 1}, 1805 clipboard: clipboard.PageContent{ 1806 Text: "丂丄丅丆丏 ¢ह€한", 1807 Linewise: false, 1808 }, 1809 expectedCursor: cursorState{position: 10}, 1810 expectedText: "a丂丄丅丆丏 ¢ह€한bc", 1811 }, 1812 } 1813 1814 for _, tc := range testCases { 1815 t.Run(tc.name, func(t *testing.T) { 1816 textTree, err := text.NewTreeFromString(tc.inputString) 1817 require.NoError(t, err) 1818 state := NewEditorState(100, 100, nil, nil) 1819 state.documentBuffer.textTree = textTree 1820 state.documentBuffer.cursor = tc.initialCursor 1821 state.clipboard.Set(clipboard.PageDefault, tc.clipboard) 1822 PasteBeforeCursor(state, clipboard.PageDefault) 1823 assert.Equal(t, tc.expectedCursor, state.documentBuffer.cursor) 1824 assert.Equal(t, tc.expectedText, textTree.String()) 1825 }) 1826 } 1827 }