1 module nodes.code_input; 2 3 import fluid; 4 5 import std.range; 6 import std.string; 7 import std.algorithm; 8 9 @safe: 10 11 Theme testTheme; 12 13 static this() { 14 testTheme = nullTheme.derive( 15 rule!TextInput( 16 Rule.textColor = color("#00303f"), 17 Rule.backgroundColor = color("#bfefff"), 18 Rule.selectionBackgroundColor = color("#41d2ff"), 19 Rule.fontSize = 14.pt, 20 ), 21 ); 22 } 23 24 @("CodeInput.lineHomeByIndex returns first non-blank character of the line") 25 unittest { 26 27 auto root = codeInput(); 28 root.value = "a\n b"; 29 root.draw(); 30 31 assert(root.lineHomeByIndex(0) == 0); 32 assert(root.lineHomeByIndex(1) == 0); 33 assert(root.lineHomeByIndex(2) == 6); 34 assert(root.lineHomeByIndex(4) == 6); 35 assert(root.lineHomeByIndex(6) == 6); 36 assert(root.lineHomeByIndex(7) == 6); 37 38 } 39 40 @("CodeInput.lineHomeByIndex recognizes tabs") 41 unittest { 42 43 auto root = codeInput(); 44 root.value = "a\n\tb"; 45 root.draw(); 46 47 assert(root.lineHomeByIndex(0) == 0); 48 assert(root.lineHomeByIndex(1) == 0); 49 assert(root.lineHomeByIndex(2) == 3); 50 assert(root.lineHomeByIndex(3) == 3); 51 assert(root.lineHomeByIndex(4) == 3); 52 53 root.value = " \t b"; 54 foreach (i; 0 .. root.value.length) { 55 56 assert(root.lineHomeByIndex(i) == 3); 57 58 } 59 60 } 61 62 @("CodeInput.visualColumns counts tabs as indents") 63 unittest { 64 65 auto root = codeInput(); 66 root.value = " ą bcd"; 67 68 foreach (i; 0 .. root.value.length) { 69 assert(root.visualColumn(i) == i); 70 } 71 72 root.value = "\t \t \t \t\n"; 73 assert(root.visualColumn(0) == 0); // 0 spaces, tab 74 assert(root.visualColumn(1) == 4); // 1 space, tab 75 assert(root.visualColumn(2) == 5); 76 assert(root.visualColumn(3) == 8); // 2 spaces, tab 77 assert(root.visualColumn(4) == 9); 78 assert(root.visualColumn(5) == 10); 79 assert(root.visualColumn(6) == 12); // 3 spaces, tab 80 assert(root.visualColumn(7) == 13); 81 assert(root.visualColumn(8) == 14); 82 assert(root.visualColumn(9) == 15); 83 assert(root.visualColumn(10) == 16); // Line feed 84 assert(root.visualColumn(11) == 0); 85 86 } 87 88 @("CodeInput.indentLevelByIndex returns indent level for the line (spaces)") 89 unittest { 90 91 auto root = codeInput(); 92 root.value = "hello, \n" 93 ~ " world a\n" 94 ~ " \n" 95 ~ " foo\n" 96 ~ " world\n" 97 ~ " world\n"; 98 99 assert(root.indentLevelByIndex(0) == 0); 100 assert(root.indentLevelByIndex(11) == 0); 101 assert(root.indentLevelByIndex(24) == 1); 102 assert(root.indentLevelByIndex(29) == 1); 103 assert(root.indentLevelByIndex(37) == 1); 104 assert(root.indentLevelByIndex(48) == 2); 105 106 } 107 108 @("CodeInput.indentLevelByIndex returns indent level for the line (mixed tabs & spaces)") 109 unittest { 110 111 auto root = codeInput(); 112 root.value = "hello,\t\n" 113 ~ " world\ta\n" 114 ~ "\t\n" 115 ~ "\tfoo\n" 116 ~ " \t world\n" 117 ~ "\t\tworld\n"; 118 119 assert(root.indentLevelByIndex(0) == 0); 120 assert(root.indentLevelByIndex(8) == 0); 121 assert(root.indentLevelByIndex(18) == 1); 122 assert(root.indentLevelByIndex(20) == 1); 123 assert(root.indentLevelByIndex(25) == 1); 124 assert(root.indentLevelByIndex(36) == 2); 125 126 } 127 128 @("CodeInput.insertTab inserts spaces according to current column") 129 unittest { 130 131 auto root = codeInput(); 132 root.insertTab(); 133 assert(root.value == " "); 134 root.push("aa"); 135 root.insertTab(); 136 assert(root.value == " aa "); 137 root.insertTab(); 138 assert(root.value == " aa "); 139 root.push("\n"); 140 root.insertTab(); 141 assert(root.value == " aa \n "); 142 root.insertTab(); 143 assert(root.value == " aa \n "); 144 root.push("||"); 145 root.insertTab(); 146 assert(root.value == " aa \n || "); 147 148 } 149 150 @("CodeInput.insertTab inserts spaces according to current column (2 spaces)") 151 unittest { 152 153 auto root = codeInput(.useSpaces(2)); 154 root.insertTab(); 155 assert(root.value == " "); 156 root.push("aa"); 157 root.insertTab(); 158 assert(root.value == " aa "); 159 root.insertTab(); 160 assert(root.value == " aa "); 161 root.push("\n"); 162 root.insertTab(); 163 assert(root.value == " aa \n "); 164 root.insertTab(); 165 assert(root.value == " aa \n "); 166 root.push("||"); 167 root.insertTab(); 168 assert(root.value == " aa \n || "); 169 root.push("x"); 170 root.insertTab(); 171 assert(root.value == " aa \n || x "); 172 173 } 174 175 @("CodeInput.insertTab inserts tabs") 176 unittest { 177 178 auto root = codeInput(.useTabs); 179 root.insertTab(); 180 assert(root.value == "\t"); 181 root.push("aa"); 182 root.insertTab(); 183 assert(root.value == "\taa\t"); 184 root.insertTab(); 185 assert(root.value == "\taa\t\t"); 186 root.push("\n"); 187 root.insertTab(); 188 assert(root.value == "\taa\t\t\n\t"); 189 root.insertTab(); 190 assert(root.value == "\taa\t\t\n\t\t"); 191 root.push("||"); 192 root.insertTab(); 193 assert(root.value == "\taa\t\t\n\t\t||\t"); 194 195 } 196 197 @("CodeInput.insertTab indents if text is selected") 198 unittest { 199 200 const originalValue = "Fïrst line\nSëcond line\r\n Thirð\n\n line\n Fourth line\nFifth line"; 201 202 auto root = codeInput(); 203 root.push(originalValue); 204 root.selectionStart = 19; 205 root.selectionEnd = 49; 206 207 assert(root.lineByIndex(root.selectionStart) == "Sëcond line"); 208 assert(root.lineByIndex(root.selectionEnd) == " Fourth line"); 209 210 root.insertTab(); 211 212 assert(root.value == "Fïrst line\n Sëcond line\r\n Thirð\n\n line\n Fourth line\nFifth line"); 213 assert(root.lineByIndex(root.selectionStart) == " Sëcond line"); 214 assert(root.lineByIndex(root.selectionEnd) == " Fourth line"); 215 216 root.outdent(); 217 218 assert(root.value == originalValue); 219 assert(root.lineByIndex(root.selectionStart) == "Sëcond line"); 220 assert(root.lineByIndex(root.selectionEnd) == " Fourth line"); 221 222 root.outdent(); 223 assert(root.value == "Fïrst line\nSëcond line\r\nThirð\n\nline\nFourth line\nFifth line"); 224 225 root.insertTab(); 226 assert(root.value == "Fïrst line\n Sëcond line\r\n Thirð\n\n line\n Fourth line\nFifth line"); 227 228 } 229 230 @("CodeInput.insertTab/outdent respect edit history") 231 unittest { 232 233 auto root = codeInput(.useTabs); 234 235 root.push("Hello, World!"); 236 root.caretToStart(); 237 root.insertTab(); 238 assert(root.value == "\tHello, World!"); 239 assert(root.valueBeforeCaret == "\t"); 240 241 root.undo(); 242 assert(root.value == "Hello, World!"); 243 assert(root.valueBeforeCaret == ""); 244 245 root.redo(); 246 assert(root.value == "\tHello, World!"); 247 assert(root.valueBeforeCaret == "\t"); 248 249 root.caretToEnd(); 250 root.outdent(); 251 assert(root.value == "Hello, World!"); 252 assert(root.valueBeforeCaret == root.value); 253 assert(root.valueAfterCaret == ""); 254 255 root.undo(); 256 assert(root.value == "\tHello, World!"); 257 assert(root.valueBeforeCaret == root.value); 258 259 root.undo(); 260 assert(root.value == "Hello, World!"); 261 assert(root.valueBeforeCaret == ""); 262 263 root.undo(); 264 assert(root.value == ""); 265 assert(root.valueBeforeCaret == ""); 266 267 } 268 269 @("CodeInput.indent can insert multiple tabs in selection") 270 unittest { 271 272 auto root = codeInput(); 273 root.value = "a"; 274 root.indent(); 275 assert(root.value == " a"); 276 277 root.value = "abc\ndef\nghi\njkl"; 278 root.selectSlice(4, 9); 279 root.indent(); 280 assert(root.value == "abc\n def\n ghi\njkl"); 281 282 root.indent(2); 283 assert(root.value == "abc\n def\n ghi\njkl"); 284 285 } 286 287 @("CodeInput.indent works well with useSpaces(3)") 288 unittest { 289 290 auto root = codeInput(.useSpaces(3)); 291 root.value = "a"; 292 root.indent(); 293 assert(root.value == " a"); 294 295 root.value = "abc\ndef\nghi\njkl"; 296 assert(root.lineByIndex(4) == "def"); 297 root.selectSlice(4, 9); 298 root.indent(); 299 300 assert(root.value == "abc\n def\n ghi\njkl"); 301 302 root.indent(2); 303 assert(root.value == "abc\n def\n ghi\njkl"); 304 305 } 306 307 @("CodeInput.indent works well with tabs") 308 unittest { 309 310 auto root = codeInput(.useTabs); 311 root.value = "a"; 312 root.indent(); 313 assert(root.value == "\ta"); 314 315 root.value = "abc\ndef\nghi\njkl"; 316 root.selectSlice(4, 9); 317 root.indent(); 318 assert(root.value == "abc\n\tdef\n\tghi\njkl"); 319 320 root.indent(2); 321 assert(root.value == "abc\n\t\t\tdef\n\t\t\tghi\njkl"); 322 323 } 324 325 @("CodeInput.outdent() removes indents for spaces and tabs") 326 unittest { 327 328 auto root = codeInput(); 329 root.outdent(); 330 assert(root.value == ""); 331 332 root.push(" "); 333 root.outdent(); 334 assert(root.value == ""); 335 336 root.push("\t"); 337 root.outdent(); 338 assert(root.value == ""); 339 340 root.push(" "); 341 root.outdent(); 342 assert(root.value == ""); 343 344 root.push(" "); 345 root.outdent(); 346 assert(root.value == " "); 347 348 root.push("foobarbaz "); 349 root.insertTab(); 350 root.outdent(); 351 assert(root.value == "foobarbaz "); 352 353 root.outdent(); 354 assert(root.value == "foobarbaz "); 355 356 root.push('\t'); 357 root.outdent(); 358 assert(root.value == "foobarbaz \t"); 359 360 root.push("\n abc "); 361 root.outdent(); 362 assert(root.value == "foobarbaz \t\nabc "); 363 364 root.push("\n \ta"); 365 root.outdent(); 366 assert(root.value == "foobarbaz \t\nabc \na"); 367 368 root.value = "\t \t\t\ta"; 369 root.outdent(); 370 assert(root.value == " \t\t\ta"); 371 372 root.outdent(); 373 assert(root.value == "\t\t\ta"); 374 375 root.outdent(2); 376 assert(root.value == "\ta"); 377 378 } 379 380 @("Tab inside of CodeInput can indent and outdent") 381 unittest { 382 383 auto map = InputMapping(); 384 map.bindNew!(FluidInputAction.insertTab)(KeyboardIO.codes.tab); 385 map.bindNew!(FluidInputAction.outdent)(KeyboardIO.codes.leftShift, KeyboardIO.codes.tab); 386 387 auto input = codeInput(); 388 auto focus = focusChain(input); 389 auto root = inputMapChain(map, focus); 390 focus.currentFocus = input; 391 root.draw(); 392 393 // Tab twice 394 foreach (i; 0..2) { 395 396 assert(input.value.length == i*4); 397 398 focus.emitEvent(KeyboardIO.press.tab); 399 root.draw(); 400 401 } 402 403 assert(input.value == " "); 404 assert(input.valueBeforeCaret == " "); 405 406 // Outdent 407 focus.emitEvent(KeyboardIO.press.leftShift); 408 focus.emitEvent(KeyboardIO.press.tab); 409 root.draw(); 410 411 assert(input.value == " "); 412 assert(input.valueBeforeCaret == " "); 413 414 } 415 416 @("CodeInput.outdent will remove tabs in .useSpaces(2)") 417 unittest { 418 419 auto root = codeInput(.useSpaces(2)); 420 root.value = " abc"; 421 root.outdent(); 422 assert(root.value == " abc"); 423 root.outdent(); 424 assert(root.value == "abc"); 425 426 } 427 428 @("CodeInput.chop removes treats indents as characters") 429 unittest { 430 431 auto root = codeInput(); 432 root.value = q{ 433 if (condition) { 434 writeln("Hello, World!"); 435 } 436 }; 437 root.runInputAction!(FluidInputAction.nextWord); 438 assert(root.caretIndex == root.value.indexOf("if")); 439 root.chop(); 440 assert(root.value == q{ 441 if (condition) { 442 writeln("Hello, World!"); 443 } 444 }); 445 root.push(' '); 446 assert(root.value == q{ 447 if (condition) { 448 writeln("Hello, World!"); 449 } 450 }); 451 root.chop(); 452 assert(root.value == q{ 453 if (condition) { 454 writeln("Hello, World!"); 455 } 456 }); 457 458 // Jump a word and remove two characters 459 root.runInputAction!(FluidInputAction.nextWord); 460 root.chop(); 461 root.chop(); 462 assert(root.value == q{ 463 i(condition) { 464 writeln("Hello, World!"); 465 } 466 }); 467 468 // Push two spaces, chop one 469 root.push(" "); 470 root.chop(); 471 assert(root.value == q{ 472 i (condition) { 473 writeln("Hello, World!"); 474 } 475 }); 476 477 } 478 479 @("CodeInput.chop works with mixed indents") 480 unittest { 481 482 auto root = codeInput(); 483 // 2 spaces, tab, 7 spaces 484 // Effectively 2.75 of an indent 485 root.value = " \t "; 486 root.caretToEnd(); 487 root.chop(); 488 489 assert(root.value == " \t "); 490 root.chop(); 491 492 // Tabs are not treated specially by chop, though 493 // They could be, maybe, but it's such a dumb edgecase, this should be good enough for everybody 494 // (I've checked that Kate does remove this in a single chop) 495 assert(root.value == " \t"); 496 root.chop(); 497 assert(root.value == " "); 498 root.chop(); 499 assert(root.value == ""); 500 501 root.value = " \t \t \t"; 502 root.caretToEnd(); 503 root.chop(); 504 assert(root.value == " \t \t "); 505 506 root.chop(); 507 assert(root.value == " \t \t"); 508 509 root.chop(); 510 assert(root.value == " \t "); 511 512 root.chop(); 513 assert(root.value == " \t"); 514 515 root.value = "\t\t\t "; 516 root.caretToEnd(); 517 root.chop(); 518 assert(root.value == "\t\t\t"); 519 root.chop(); 520 assert(root.value == "\t\t"); 521 522 } 523 524 @("CodeInput.chop works with indent of 2 spaces") 525 unittest { 526 527 auto root = codeInput(.useSpaces(2)); 528 root.value = " abc"; 529 root.caretIndex = 6; 530 root.chop(); 531 assert(root.value == " abc"); 532 root.chop(); 533 assert(root.value == " abc"); 534 root.chop(); 535 assert(root.value == "abc"); 536 root.chop(); 537 assert(root.value == "abc"); 538 539 root.undo(); 540 assert(root.value == " abc"); 541 assert(root.valueAfterCaret == "abc"); 542 543 544 } 545 546 @("breakLine preserves tabs from last line in CodeInput") 547 unittest { 548 549 auto root = codeInput(); 550 551 root.push("abcdef"); 552 root.runInputAction!(FluidInputAction.breakLine); 553 assert(root.value == "abcdef\n"); 554 555 root.insertTab(); 556 root.runInputAction!(FluidInputAction.breakLine); 557 assert(root.value == "abcdef\n \n "); 558 559 root.insertTab(); 560 root.runInputAction!(FluidInputAction.breakLine); 561 assert(root.value == "abcdef\n \n \n "); 562 563 root.outdent(); 564 root.outdent(); 565 assert(root.value == "abcdef\n \n \n"); 566 567 root.runInputAction!(FluidInputAction.breakLine); 568 assert(root.value == "abcdef\n \n \n\n"); 569 570 root.undo(); 571 assert(root.value == "abcdef\n \n \n"); 572 root.undo(); 573 assert(root.value == "abcdef\n \n \n "); 574 root.undo(); 575 assert(root.value == "abcdef\n \n "); 576 root.undo(); 577 assert(root.value == "abcdef\n \n "); 578 root.undo(); 579 assert(root.value == "abcdef\n "); 580 581 } 582 583 @("CodeInput.breakLine keeps tabs from last line in .useSpaces(2)") 584 unittest { 585 586 auto root = codeInput(.useSpaces(2)); 587 root.push("abcdef\n"); 588 root.insertTab; 589 assert(root.caretLine == " "); 590 root.breakLine(); 591 assert(root.caretLine == " "); 592 root.breakLine(); 593 root.push("a"); 594 assert(root.caretLine == " a"); 595 596 assert(root.value == "abcdef\n \n \n a"); 597 598 } 599 600 @("CodeInput.breakLine keeps tabs from last line if inserted in the middle of the line") 601 unittest { 602 603 auto root = codeInput(); 604 root.value = " abcdef"; 605 root.caretIndex = 8; 606 root.breakLine; 607 assert(root.value == " abcd\n ef"); 608 609 } 610 611 @("CodeInput.reformatLine converts indents to the correct indent character") 612 unittest { 613 614 auto root = codeInput(); 615 616 // 3 tabs -> 3 indents 617 root.push("\t\t\t"); 618 root.breakLine(); 619 assert(root.value == "\t\t\t\n "); 620 621 // mixed tabs (8 width total) -> 2 indents 622 root.value = " \t \t"; 623 root.caretToEnd(); 624 root.breakLine(); 625 assert(root.value == " \t \t\n "); 626 627 // 6 spaces -> 1 indent 628 root.value = " "; 629 root.breakLine(); 630 assert(root.value == " \n "); 631 632 // Same but now with tabs 633 root.useTabs = true; 634 root.reformatLine; 635 assert(root.indentRope(1) == "\t"); 636 assert(root.value == " \n\t"); 637 638 // 3 tabs -> 3 indents 639 root.value = "\t\t\t"; 640 root.breakLine(); 641 assert(root.value == "\t\t\t\n\t\t\t"); 642 643 // mixed tabs (8 width total) -> 2 indents 644 root.value = " \t \t"; 645 root.breakLine(); 646 assert(root.value == " \t \t\n\t\t"); 647 648 // Same but now with 2 spaces 649 root.useTabs = false; 650 root.indentWidth = 2; 651 root.reformatLine; 652 assert(root.indentRope(1) == " "); 653 assert(root.value == " \t \t\n "); 654 655 // 3 tabs -> 3 indents 656 root.value = "\t\t\t\n"; 657 root.caretToStart; 658 root.reformatLine; 659 assert(root.value == " \n"); 660 661 // mixed tabs (8 width total) -> 2 indents 662 root.value = " \t \t"; 663 root.breakLine(); 664 assert(root.value == " \t \t\n "); 665 666 // 6 spaces -> 3 indents 667 root.value = " "; 668 root.breakLine(); 669 assert(root.value == " \n "); 670 671 } 672 673 @("CodeInput.toggleHome moves to a line's home, or start if already at home") 674 unittest { 675 676 auto root = codeInput(); 677 root.value = "int main() {\n return 0;\n}"; 678 root.caretIndex = root.value.countUntil("return"); 679 root.draw(); 680 assert(root.caretIndex == root.lineHomeByIndex(root.caretIndex)); 681 682 const home = root.caretIndex; 683 684 // Toggle home should move to line start, because the cursor is already at home 685 root.toggleHome(); 686 assert(root.caretIndex == home - 4); 687 assert(root.caretIndex == root.lineStartByIndex(home)); 688 689 // Toggle again 690 root.toggleHome(); 691 assert(root.caretIndex == home); 692 693 // Move one character left 694 root.caretIndex = root.caretIndex - 1; 695 assert(root.caretIndex != home); 696 root.toggleHome(); 697 root.draw(); 698 assert(root.caretIndex == home); 699 700 // Move to first line and see if toggle home works well even if there's no indent 701 root.caretIndex = 4; 702 root.updateCaretPosition(); 703 root.toggleHome(); 704 assert(root.caretIndex == 0); 705 706 root.toggleHome(); 707 assert(root.caretIndex == 0); 708 709 // Switch to tabs 710 const previousValue = root.value; 711 root.useTabs = true; 712 root.reformatLine(); 713 assert(root.value == previousValue); 714 715 // Move to line below 716 root.runInputAction!(FluidInputAction.nextLine); 717 root.reformatLine(); 718 assert(root.value == "int main() {\n\treturn 0;\n}"); 719 assert(root.valueBeforeCaret == "int main() {\n\t"); 720 721 const secondLineHome = root.caretIndex; 722 root.draw(); 723 root.toggleHome(); 724 assert(root.caretIndex == secondLineHome - 1); 725 726 root.toggleHome(); 727 assert(root.caretIndex == secondLineHome); 728 729 } 730 731 @("CodeInput.toggleHome works with both tabs and spaces") 732 unittest { 733 734 foreach (useTabs; [false, true]) { 735 736 const tabLength = useTabs ? 1 : 4; 737 738 auto root = codeInput(); 739 root.useTabs = useTabs; 740 root.value = root.indentRope ~ "long line that wraps because the viewport is too small to make it fit"; 741 root.caretIndex = tabLength; 742 root.draw(); 743 744 // Move to start 745 root.toggleHome(); 746 assert(root.caretIndex == 0); 747 748 // Move home 749 root.toggleHome(); 750 assert(root.caretIndex == tabLength); 751 752 // Move to line below 753 root.runInputAction!(FluidInputAction.nextLine); 754 755 // Move to line start 756 root.caretToLineStart(); 757 assert(root.caretIndex > tabLength); 758 759 const secondLineStart = root.caretIndex; 760 761 // Move a few characters to the right, and move to line start again 762 root.caretIndex = root.caretIndex + 5; 763 root.toggleHome(); 764 assert(root.caretIndex == secondLineStart); 765 766 // If the caret is already at the start, it should move home 767 root.toggleHome(); 768 assert(root.caretIndex == tabLength); 769 root.toggleHome(); 770 assert(root.caretIndex == 0); 771 772 } 773 774 } 775 776 @("CodeInput supports syntax highlighting with CodeHighlighter") 777 unittest { 778 779 import std.typecons : BlackHole; 780 781 enum tokenFunction = 1; 782 enum tokenString = 2; 783 784 auto text = `print("Hello, World!")`; 785 auto highlighter = new class BlackHole!CodeHighlighter { 786 787 override CodeSlice query(size_t byteIndex) { 788 789 if (byteIndex == 0) return CodeSlice(0, 5, tokenFunction); 790 if (byteIndex <= 6) return CodeSlice(6, 21, tokenString); 791 return CodeSlice.init; 792 793 } 794 795 }; 796 797 auto root = codeInput(highlighter); 798 root.draw(); 799 800 assert(root.contentLabel.text.styleMap.equal([ 801 TextStyleSlice(0, 5, tokenFunction), 802 TextStyleSlice(6, 21, tokenString), 803 ])); 804 805 } 806 807 @("CodeInput can use CodeIndentor separate from CodeHighlighter") 808 unittest { 809 810 import std.typecons : BlackHole; 811 812 auto originalText 813 = "void foo() {\n" 814 ~ "fun();\n" 815 ~ "functionCall(\n" 816 ~ "stuff()\n" 817 ~ ");\n" 818 ~ " }\n"; 819 auto formattedText 820 = "void foo() {\n" 821 ~ " fun();\n" 822 ~ " functionCall(\n" 823 ~ " stuff()\n" 824 ~ " );\n" 825 ~ "}\n"; 826 827 class Indentor : BlackHole!CodeIndentor { 828 829 struct Indent { 830 ptrdiff_t offset; 831 int indent; 832 } 833 834 Indent[] indents; 835 836 override void parse(Rope rope) { 837 838 bool lineStart; 839 840 indents = [Indent(0, 0)]; 841 842 foreach (i, ch; rope.enumerate) { 843 844 if (ch.among('{', '(')) { 845 indents ~= Indent(i + 1, 1); 846 } 847 848 else if (ch.among('}', ')')) { 849 indents ~= Indent(i, lineStart ? -1 : 0); 850 } 851 852 else if (ch == '\n') lineStart = true; 853 else if (ch != ' ') lineStart = false; 854 855 } 856 857 } 858 859 override int indentDifference(ptrdiff_t offset) { 860 861 return indents 862 .filter!(a => a.offset <= offset) 863 .tail(1) 864 .front 865 .indent; 866 867 } 868 869 } 870 871 auto indentor = new Indentor; 872 auto highlighter = new class Indentor, CodeHighlighter { 873 874 const(char)[] nextTokenName(ubyte) { 875 return null; 876 } 877 878 CodeSlice query(size_t) { 879 return CodeSlice.init; 880 } 881 882 override void parse(Rope value) { 883 super.parse(value); 884 } 885 886 }; 887 888 auto indentorOnlyInput = codeInput(); 889 indentorOnlyInput.indentor = indentor; 890 auto highlighterInput = codeInput(highlighter); 891 892 foreach (root; [indentorOnlyInput, highlighterInput]) { 893 894 root.value = originalText; 895 root.draw(); 896 897 // Reformat first line 898 root.caretIndex = 0; 899 assert(root.targetIndentLevelByIndex(0) == 0); 900 root.reformatLine(); 901 assert(root.value == originalText); 902 903 // Reformat second line 904 root.caretIndex = 13; 905 assert(root.indentor.indentDifference(13) == 1); 906 assert(root.targetIndentLevelByIndex(13) == 1); 907 root.reformatLine(); 908 assert(root.value == formattedText[0..23] ~ originalText[19..$]); 909 910 // Reformat third line 911 root.caretIndex = 24; 912 assert(root.indentor.indentDifference(24) == 0); 913 assert(root.targetIndentLevelByIndex(24) == 1); 914 root.reformatLine(); 915 assert(root.value == formattedText[0..42] ~ originalText[34..$]); 916 917 // Reformat fourth line 918 root.caretIndex = 42; 919 assert(root.indentor.indentDifference(42) == 1); 920 assert(root.targetIndentLevelByIndex(42) == 2); 921 root.reformatLine(); 922 assert(root.value == formattedText[0..58] ~ originalText[42..$]); 923 924 // Reformat fifth line 925 root.caretIndex = 58; 926 assert(root.indentor.indentDifference(58) == -1); 927 assert(root.targetIndentLevelByIndex(58) == 1); 928 root.reformatLine(); 929 assert(root.value == formattedText[0..65] ~ originalText[45..$]); 930 931 // And the last line, finally 932 root.caretIndex = 65; 933 assert(root.indentor.indentDifference(65) == -1); 934 assert(root.targetIndentLevelByIndex(65) == 0); 935 root.reformatLine(); 936 assert(root.value == formattedText); 937 938 } 939 940 } 941 942 @("Spaces and tabs are equivalent in width if configured so in CodeInput") 943 unittest { 944 945 auto roots = [ 946 codeInput(.nullTheme, .useSpaces(2)), 947 codeInput(.nullTheme, .useTabs(2)), 948 ]; 949 950 // Draw each root 951 foreach (i, root; roots) { 952 root.insertTab(); 953 root.push("a"); 954 root.draw(); 955 } 956 957 assert(roots[0].value == " a"); 958 assert(roots[1].value == "\ta"); 959 960 // Drawn text content has to be identical, since both have the same indent width 961 assert(roots.all!(a => a.contentLabel.text.texture.chunks.length == 1)); 962 assert(roots[0].contentLabel.text.texture.chunks[0].image.data 963 == roots[1].contentLabel.text.texture.chunks[0].image.data); 964 965 } 966 967 @("Indent width in CodeInput affects space characters but not tabs") 968 unittest { 969 970 auto roots = [ 971 codeInput(.nullTheme, .useSpaces(1)), 972 codeInput(.nullTheme, .useSpaces(2)), 973 codeInput(.nullTheme, .useSpaces(4)), 974 codeInput(.nullTheme, .useSpaces(8)), 975 codeInput(.nullTheme, .useTabs(1)), 976 codeInput(.nullTheme, .useTabs(2)), 977 codeInput(.nullTheme, .useTabs(4)), 978 codeInput(.nullTheme, .useTabs(8)), 979 ]; 980 981 foreach (root; roots) { 982 root.insertTab(); 983 root.push("a"); 984 root.draw(); 985 } 986 987 assert(roots[0].value == " a"); 988 assert(roots[1].value == " a"); 989 assert(roots[2].value == " a"); 990 assert(roots[3].value == " a"); 991 992 foreach (root; roots[4..8]) { 993 994 assert(root.value == "\ta"); 995 996 } 997 998 float indentWidth(CodeInput root) { 999 return root.contentLabel.text.indentWidth; 1000 } 1001 1002 foreach (i; [0, 4]) { 1003 1004 assert(indentWidth(roots[i + 0]) * 2 == indentWidth(roots[i + 1])); 1005 assert(indentWidth(roots[i + 1]) * 2 == indentWidth(roots[i + 2])); 1006 assert(indentWidth(roots[i + 2]) * 2 == indentWidth(roots[i + 3])); 1007 1008 } 1009 1010 foreach (i; 0..4) { 1011 1012 assert(indentWidth(roots[0 + i]) == indentWidth(roots[4 + i]), 1013 "Indent widths should be the same for both space and tab based roots"); 1014 1015 } 1016 1017 } 1018 1019 @("CodeInput.paste changes indents to match the current text") 1020 unittest { 1021 1022 auto input = codeInput(.useTabs); 1023 auto clipboard = clipboardChain(input); 1024 auto root = clipboard; 1025 root.draw(); 1026 1027 clipboard.value = "text"; 1028 input.insertTab; 1029 input.paste(); 1030 assert(input.value == "\ttext"); 1031 1032 input.breakLine; 1033 input.paste(); 1034 assert(input.value == "\ttext\n\ttext"); 1035 1036 clipboard.value = "text\ntext"; 1037 input.value = ""; 1038 input.paste(); 1039 assert(input.value == "text\ntext"); 1040 1041 input.breakLine; 1042 input.insertTab; 1043 input.paste(); 1044 assert(input.value == "text\ntext\n\ttext\n\ttext"); 1045 1046 clipboard.value = " {\n text\n }\n"; 1047 input.value = ""; 1048 input.paste(); 1049 assert(input.value == "{\n text\n}\n"); 1050 1051 input.value = "\t"; 1052 input.caretToEnd(); 1053 input.paste(); 1054 assert(input.value == "\t{\n\t text\n\t}\n\t"); 1055 1056 input.value = "\t"; 1057 input.caretToStart(); 1058 input.paste(); 1059 assert(input.value == "{\n text\n}\n\t"); 1060 1061 } 1062 1063 @("CodeInput.paste keeps the pasted value as-is if it's composed of spaces or tabs") 1064 unittest { 1065 1066 auto input = codeInput(); 1067 auto clipboard = clipboardChain(); 1068 auto root = chain(clipboard, input); 1069 root.draw(); 1070 1071 foreach (i, value; ["", " ", " ", "\t", "\t\t"]) { 1072 1073 clipboard.value = value; 1074 input.value = ""; 1075 input.paste(); 1076 assert(input.value == value, 1077 format!"Clipboard preset index %s (%s) not preserved"(i, value)); 1078 1079 } 1080 1081 } 1082 1083 @("CodeInput.paste replaces the selection") 1084 unittest { 1085 1086 auto input = codeInput(.useTabs); 1087 auto clipboard = clipboardChain(); 1088 auto root = chain(clipboard, input); 1089 root.draw(); 1090 1091 clipboard.value = "text\ntext"; 1092 input.value = "let foo() {\n\tbar\t\tbaz\n}"; 1093 input.selectSlice( 1094 input.value.indexOf("bar"), 1095 input.value.indexOf("baz"), 1096 ); 1097 input.paste(); 1098 assert(input.value == "let foo() {\n\ttext\n\ttextbaz\n}"); 1099 1100 clipboard.value = "\t\ttext\n\ttext"; 1101 input.value = "let foo() {\n\tbar\t\tbaz\n}"; 1102 input.selectSlice( 1103 input.value.indexOf("bar"), 1104 input.value.indexOf("baz"), 1105 ); 1106 input.paste(); 1107 assert(input.value == "let foo() {\n\t\ttext\n\ttextbaz\n}"); 1108 1109 } 1110 1111 @("CodeInput.paste creates a history entry") 1112 unittest { 1113 1114 auto input = codeInput(.useSpaces(2)); 1115 auto clipboard = clipboardChain(); 1116 auto root = chain(clipboard, input); 1117 1118 root.draw(); 1119 clipboard.value = "World"; 1120 input.push(" Hello,"); 1121 input.runInputAction!(FluidInputAction.breakLine); 1122 input.paste(); 1123 input.push("!"); 1124 assert(input.value == " Hello,\n World!"); 1125 1126 // Undo the exclamation mark 1127 input.undo(); 1128 assert(input.value == " Hello,\n World"); 1129 1130 // Undo moves before pasting 1131 input.undo(); 1132 assert(input.value == " Hello,\n "); 1133 assert(input.valueBeforeCaret == input.value); 1134 1135 // Next undo moves before line break 1136 input.undo(); 1137 assert(input.value == " Hello,"); 1138 1139 // Next undo clears all changes 1140 input.undo(); 1141 assert(input.value == ""); 1142 1143 // No change 1144 input.undo(); 1145 assert(input.value == ""); 1146 1147 // It can all be redone 1148 input.redo(); 1149 assert(input.value == " Hello,"); 1150 assert(input.valueBeforeCaret == input.value); 1151 input.redo(); 1152 assert(input.value == " Hello,\n "); 1153 assert(input.valueBeforeCaret == input.value); 1154 input.redo(); 1155 assert(input.value == " Hello,\n World"); 1156 assert(input.valueBeforeCaret == input.value); 1157 input.redo(); 1158 assert(input.value == " Hello,\n World!"); 1159 assert(input.valueBeforeCaret == input.value); 1160 input.redo(); 1161 assert(input.value == " Hello,\n World!"); 1162 1163 } 1164 1165 @("CodeInput.paste creates a history entry (single line)") 1166 unittest { 1167 1168 // Same test as above, but insert a space instead of line break 1169 1170 auto input = codeInput(.useSpaces(2)); 1171 auto clipboard = clipboardChain(); 1172 auto root = chain(clipboard, input); 1173 root.draw(); 1174 1175 clipboard.value = "World"; 1176 input.push(" Hello,"); 1177 input.push(" "); 1178 input.paste(); 1179 input.push("!"); 1180 assert(input.value == " Hello, World!"); 1181 1182 // Undo the exclamation mark 1183 input.undo(); 1184 assert(input.value == " Hello, World"); 1185 1186 // Next undo moves before pasting, just like above 1187 input.undo(); 1188 assert(input.value == " Hello, "); 1189 assert(input.valueBeforeCaret == input.value); 1190 1191 input.undo(); 1192 assert(input.value == ""); 1193 1194 // No change 1195 input.undo(); 1196 assert(input.value == ""); 1197 1198 input.redo(); 1199 assert(input.value == " Hello, "); 1200 assert(input.valueBeforeCaret == input.value); 1201 1202 } 1203 1204 @("CodeInput.paste strips common indent, even if indent character differs from the editor's") 1205 unittest { 1206 1207 auto input = codeInput(.useTabs); 1208 auto clipboard = clipboardChain(); 1209 auto root = chain(clipboard, input); 1210 root.draw(); 1211 1212 clipboard.value = " foo\n "; 1213 input.value = "let foo() {\n\t\n}"; 1214 input.caretIndex = input.value.indexOf("\n}"); 1215 input.paste(); 1216 assert(input.value == "let foo() {\n\tfoo\n\t\n}"); 1217 1218 clipboard.value = "foo\n bar\n"; 1219 input.value = "let foo() {\n\tx\n}"; 1220 input.caretIndex = input.value.indexOf("x"); 1221 input.paste(); 1222 assert(input.value == "let foo() {\n\tfoo\n\tbar\n\tx\n}"); 1223 1224 } 1225 1226 @("CodeInput correctly displays text and selection in HiDPI") 1227 unittest { 1228 1229 import std.typecons : BlackHole; 1230 1231 enum tokenFunction = 1; 1232 enum tokenString = 2; 1233 auto highlighter = new class BlackHole!CodeHighlighter { 1234 1235 override CodeSlice query(size_t byteIndex) { 1236 1237 if (byteIndex <= 4) return CodeSlice( 4, 7, tokenFunction); 1238 if (byteIndex <= 14) return CodeSlice(14, 28, tokenString); 1239 return CodeSlice.init; 1240 1241 } 1242 1243 }; 1244 1245 auto node = codeInput(.testTheme, highlighter); 1246 auto root = testSpace(node); 1247 1248 node.value = "let foo() {\n\t`Hello, World!`\n}"; 1249 node.selectSlice(4, 19); 1250 1251 // 100% scale 1252 root.drawAndAssert( 1253 node.cropsTo(0, 0, 200, 81), 1254 node.drawsRectangle(28, 0, 52, 27).ofColor("#41d2ff"), 1255 node.drawsRectangle(0, 27, 66, 27).ofColor("#41d2ff"), 1256 node.contentLabel.isDrawn().at(0, 0, 200, 81), 1257 node.contentLabel.drawsHintedImage().at(0, 0, 1024, 1024).ofColor("#ffffff") 1258 .sha256("7d1a992dbe8419432e5c387a88ad8b5117fdd06f9eb51ca80e1c4bb49c6e33a9"), 1259 node.resetsCrop(), 1260 ); 1261 1262 // 125% scale 1263 root.setScale(1.25); 1264 root.drawAndAssert( 1265 node.cropsTo(0, 0, 200, 80), 1266 node.drawsRectangle(28, 0, 51.2, 26.4).ofColor("#41d2ff"), 1267 node.drawsRectangle(0, 26.4, 64.8, 26.4).ofColor("#41d2ff"), 1268 node.contentLabel.isDrawn().at(0, 0, 200, 80), 1269 node.contentLabel.drawsHintedImage().at(0, 0, 819.2, 819.2).ofColor("#ffffff") 1270 .sha256("fe98c96e3d23bf446821cc1732361588236d1177fbf298de43be3df7e6c61778"), 1271 node.resetsCrop(), 1272 ); 1273 1274 }