1 module fluid.code_input;
2 
3 import std.range;
4 import std.string;
5 import std.algorithm;
6 
7 import fluid.text;
8 import fluid.input;
9 import fluid.utils;
10 import fluid.style;
11 import fluid.backend;
12 import fluid.text_input;
13 
14 
15 @safe:
16 
17 
18 /// Node parameter for `CodeInput` enabling tabs as the character used for indents.
19 /// Params:
20 ///     width = Indent width; number of character a single tab corresponds to. Optional, if set to 0 or left default,
21 ///         keeps the old/default value.
22 auto useTabs(int width = 0) {
23 
24     struct UseTabs {
25 
26         int width;
27 
28         void apply(CodeInput node) {
29             node.useTabs = true;
30             if (width)
31                 node.indentWidth = width;
32         }
33 
34     }
35 
36     return UseTabs(width);
37 
38 }
39 
40 /// Node parameter for `CodeInput`, setting spaces as the character used for indents.
41 /// Params:
42 ///     width = Indent width; number of spaces a single indent consists of.
43 auto useSpaces(int width) {
44 
45     struct UseSpaces {
46 
47         int width;
48 
49         void apply(CodeInput node) {
50             node.useTabs = false;
51             node.indentWidth = width;
52         }
53 
54     }
55 
56     return UseSpaces(width);
57 
58 }
59 
60 
61 /// A CodeInput is a special variant of `TextInput` that provides syntax highlighting and a gutter (column with line
62 /// numbers).
63 alias codeInput = simpleConstructor!CodeInput;
64 
65 /// ditto
66 class CodeInput : TextInput {
67 
68     mixin enableInputActions;
69 
70     enum maxIndentWidth = 16;
71 
72     public {
73 
74         CodeHighlighter highlighter;
75         CodeIndentor indentor;
76 
77         /// Additional context to pass to the highlighter. Will not be displayed, but can be used to improve syntax
78         /// highlighting and code analysis.
79         Rope prefix;
80 
81         /// ditto
82         Rope suffix;
83 
84         /// Character width of a single indent level.
85         int indentWidth = 4;
86         invariant(indentWidth <= maxIndentWidth);
87 
88         /// If true, uses the tab character for indents.
89         bool useTabs = false;
90 
91     }
92 
93     public {
94 
95         /// Current token type, used for styling individual token types and **only relevant in themes**.
96         const(char)[] token;
97 
98     }
99 
100     private {
101 
102         struct AutomaticFormat {
103 
104             bool pending;
105             int oldTargetIndent;
106 
107             this(int oldTargetIndent) {
108                 this.pending = true;
109                 this.oldTargetIndent = oldTargetIndent;
110             }
111 
112         }
113 
114         /// If automatic reformatting is to take place, `pending` is set to true, with `oldTargetIndent` set to the
115         /// previous value of the indent. This value is compared against the current target, and the reformatter will
116         /// only activate if there was a change.
117         AutomaticFormat _automaticFormat;
118 
119     }
120 
121     this(CodeHighlighter highlighter = null, void delegate() @safe submitted = null) {
122 
123         this.submitted = submitted;
124         this.highlighter = highlighter;
125         this.indentor = cast(CodeIndentor) highlighter;
126         super.contentLabel = new ContentLabel;
127 
128     }
129 
130     inout(ContentLabel) contentLabel() inout {
131 
132         return cast(inout ContentLabel) super.contentLabel;
133 
134     }
135 
136     override bool multiline() const {
137 
138         return true;
139 
140     }
141 
142     class ContentLabel : TextInput.ContentLabel {
143 
144         /// Use our own `Text`.
145         StyledText!CodeHighlighterRange text;
146         Style[256] styles;
147 
148         this() {
149 
150             text = typeof(text)(this, "", CodeHighlighterRange.init);
151             text.hasFastEdits = true;
152 
153         }
154 
155         override void resizeImpl(Vector2 available) {
156 
157             assert(text.hasFastEdits);
158 
159             auto typeface = style.getTypeface;
160 
161             typeface.setSize(io.dpi, style.fontSize);
162 
163             this.text.value = super.text.value;
164             text.indentWidth = indentWidth * typeface.advance(' ').x / io.hidpiScale.x;
165             text.resize(available);
166             minSize = text.size;
167 
168         }
169 
170         override void drawImpl(Rectangle outer, Rectangle inner) {
171 
172             const style = pickStyle();
173             text.draw(styles, inner.start);
174 
175         }
176 
177     }
178 
179     /// Get the full value of the text, including context provided via `prefix` and `suffix`.
180     Rope sourceValue() const {
181 
182         // TODO This will allocate. Can it be avoided?
183         return prefix ~ value ~ suffix;
184 
185     }
186 
187     /// Get a rope representing given indent level.
188     Rope indentRope(int indentLevel = 1) const {
189 
190         static tabRope = const Rope("\t");
191         static spaceRope = const Rope("                ");
192 
193         static assert(spaceRope.length == maxIndentWidth);
194 
195         Rope result;
196 
197         // TODO this could be more performant by using as much of a single rope as possible
198 
199         // Insert a tab
200         if (useTabs)
201             foreach (i; 0 .. indentLevel) {
202 
203                 result ~= tabRope;
204 
205             }
206 
207         // Insert a space
208         else foreach (i; 0 .. indentLevel) {
209 
210             result ~= spaceRope[0 .. indentWidth];
211 
212         }
213 
214         return result;
215 
216     }
217 
218     ///
219     unittest {
220 
221         auto root = codeInput(.useTabs);
222 
223         assert(root.indentRope == "\t");
224         assert(root.indentRope(2) == "\t\t");
225         assert(root.indentRope(3) == "\t\t\t");
226 
227     }
228 
229     unittest {
230 
231         auto root = codeInput();
232 
233         assert(root.indentRope == "    ");
234         assert(root.indentRope(2) == "        ");
235         assert(root.indentRope(3) == "            ");
236 
237     }
238 
239     protected void reparse() {
240 
241         const fullValue = sourceValue;
242 
243         // Parse the file
244         if (highlighter) {
245 
246             highlighter.parse(fullValue);
247 
248             // Apply highlighting to the label
249             contentLabel.text.styleMap = highlighter.save(cast(int) prefix.length);
250 
251         }
252 
253         // Pass the file to the indentor
254         if (indentor && cast(Object) indentor !is cast(Object) highlighter) {
255 
256             indentor.parse(fullValue);
257 
258         }
259 
260     }
261 
262     unittest {
263 
264         import std.typecons;
265 
266         static abstract class Highlighter : CodeHighlighter {
267 
268             int highlightCount;
269 
270             void parse(Rope) {
271                 highlightCount++;
272             }
273 
274         }
275 
276         static abstract class Indentor : CodeIndentor {
277 
278             int indentCount;
279 
280             void parse(Rope) {
281                 indentCount++;
282             }
283 
284         }
285 
286         auto highlighter = new BlackHole!Highlighter;
287         auto root = codeInput(highlighter);
288         root.reparse();
289 
290         assert(highlighter.highlightCount == 1);
291 
292         auto indentor = new BlackHole!Indentor;
293         root.indentor = indentor;
294         root.reparse();
295 
296         // Parse called once for each
297         assert(highlighter.highlightCount == 2);
298         assert(indentor.indentCount == 1);
299 
300         static abstract class FullHighlighter : CodeHighlighter, CodeIndentor {
301 
302             int highlightCount;
303             int indentCount;
304 
305             void parse(Rope) {
306                 highlightCount++;
307                 indentCount++;
308             }
309 
310         }
311 
312         auto fullHighlighter = new BlackHole!FullHighlighter;
313         root = codeInput(fullHighlighter);
314         root.reparse();
315 
316         // Parse should be called once for the whole class
317         assert(fullHighlighter.highlightCount == 1);
318         assert(fullHighlighter.indentCount == 1);
319 
320     }
321 
322     override void resizeImpl(Vector2 vector) @trusted {
323 
324         // Parse changes
325         reparse();
326 
327         // Reformat the line if requested
328         if (_automaticFormat.pending) {
329 
330             const oldTarget = _automaticFormat.oldTargetIndent;
331             const newTarget = targetIndentLevelByIndex(caretIndex);
332 
333             // Reformat only if the target indent changed; don't force "correct" indents on the programmer
334             if (oldTarget != newTarget)
335                 reformatLine();
336 
337             _automaticFormat.pending = false;
338 
339         }
340 
341         // Resize the field
342         super.resizeImpl(vector);
343 
344     }
345 
346     override void drawImpl(Rectangle outer, Rectangle inner) {
347 
348         // Reload token styles
349         contentLabel.styles[0] = pickStyle();
350 
351         if (highlighter) {
352 
353             CodeToken tokenIndex;
354             while (++tokenIndex) {
355 
356                 token = highlighter.nextTokenName(tokenIndex);
357 
358                 if (token is null) break;
359 
360                 contentLabel.styles[tokenIndex] = pickStyle();
361 
362             }
363 
364         }
365 
366         super.drawImpl(outer, inner);
367 
368     }
369 
370     protected override bool keyboardImpl() {
371 
372         auto oldValue = this.value;
373         auto format = AutomaticFormat(targetIndentLevelByIndex(caretIndex));
374 
375         auto keyboardHandled = super.keyboardImpl();
376 
377         // If the value has changed, trigger automatic reformatting
378         if (oldValue !is this.value)
379             _automaticFormat = format;
380 
381         return keyboardHandled;
382 
383     }
384 
385     protected override bool inputActionImpl(InputActionID id, bool active) {
386 
387         // Request format
388         if (active)
389             _automaticFormat = AutomaticFormat(targetIndentLevelByIndex(caretIndex));
390 
391         return false;
392 
393     }
394 
395     /// Returns the index of the first character in a line that is not a space, given index of any character on
396     /// the same line.
397     size_t lineHomeByIndex(size_t index) {
398 
399         const indentWidth = lineByIndex(index)
400             .until!(a => !a.among(' ', '\t'))
401             .walkLength;
402 
403         return lineStartByIndex(index) + indentWidth;
404 
405     }
406 
407     unittest {
408 
409         auto root = codeInput();
410         root.value = "a\n    b";
411         root.draw();
412 
413         assert(root.lineHomeByIndex(0) == 0);
414         assert(root.lineHomeByIndex(1) == 0);
415         assert(root.lineHomeByIndex(2) == 6);
416         assert(root.lineHomeByIndex(4) == 6);
417         assert(root.lineHomeByIndex(6) == 6);
418         assert(root.lineHomeByIndex(7) == 6);
419 
420     }
421 
422     unittest {
423 
424         auto root = codeInput();
425         root.value = "a\n\tb";
426         root.draw();
427 
428         assert(root.lineHomeByIndex(0) == 0);
429         assert(root.lineHomeByIndex(1) == 0);
430         assert(root.lineHomeByIndex(2) == 3);
431         assert(root.lineHomeByIndex(3) == 3);
432         assert(root.lineHomeByIndex(4) == 3);
433 
434         root.value = " \t b";
435         foreach (i; 0 .. root.value.length) {
436 
437             assert(root.lineHomeByIndex(i) == 3);
438 
439         }
440 
441     }
442 
443     /// Get the column the given index (or caret index) is at, but count tabs as however characters they display as.
444     ptrdiff_t visualColumn(size_t i) {
445 
446         // Select characters on the same before the given index
447         auto indents = lineByIndex(i)[0 .. column!char(i)];
448 
449         return foldIndents(indents);
450 
451     }
452 
453     /// ditto
454     ptrdiff_t visualColumn() {
455 
456         return visualColumn(caretIndex);
457 
458     }
459 
460     unittest {
461 
462         auto root = codeInput();
463         root.value = "    ą bcd";
464 
465         foreach (i; 0 .. root.value.length) {
466             assert(root.visualColumn(i) == i);
467         }
468 
469         root.value = "\t \t  \t   \t\n";
470         assert(root.visualColumn(0) == 0);   // 0 spaces, tab
471         assert(root.visualColumn(1) == 4);   // 1 space, tab
472         assert(root.visualColumn(2) == 5);
473         assert(root.visualColumn(3) == 8);   // 2 spaces, tab
474         assert(root.visualColumn(4) == 9);
475         assert(root.visualColumn(5) == 10);
476         assert(root.visualColumn(6) == 12);  // 3 spaces, tab
477         assert(root.visualColumn(7) == 13);
478         assert(root.visualColumn(8) == 14);
479         assert(root.visualColumn(9) == 15);
480         assert(root.visualColumn(10) == 16);  // Line feed
481         assert(root.visualColumn(11) == 0);
482 
483     }
484 
485     /// Get indent count for offset at given index.
486     int indentLevelByIndex(size_t i) {
487 
488         // Select indents on the given line
489         auto indents = lineByIndex(i).byDchar
490             .until!(a => !a.among(' ', '\t'));
491 
492         return cast(int) foldIndents(indents) / indentWidth;
493 
494     }
495 
496     /// Count width of the given text, counting tabs using their visual size, while other characters are of width of 1
497     private auto foldIndents(T)(T input) {
498 
499         return input.fold!(
500             (a, c) => c == '\t'
501                 ? a + indentWidth - (a % indentWidth)
502                 : a + 1)(0);
503 
504     }
505 
506     unittest {
507 
508         auto root = codeInput();
509         root.value = "hello,    \n"
510             ~ "  world    a\n"
511             ~ "    \n"
512             ~ "    foo\n"
513             ~ "     world\n"
514             ~ "        world\n";
515 
516         assert(root.indentLevelByIndex(0) == 0);
517         assert(root.indentLevelByIndex(11) == 0);
518         assert(root.indentLevelByIndex(24) == 1);
519         assert(root.indentLevelByIndex(29) == 1);
520         assert(root.indentLevelByIndex(37) == 1);
521         assert(root.indentLevelByIndex(48) == 2);
522 
523     }
524 
525     unittest {
526 
527         auto root = codeInput();
528         root.value = "hello,\t\n"
529             ~ "  world\ta\n"
530             ~ "\t\n"
531             ~ "\tfoo\n"
532             ~ "   \t world\n"
533             ~ "\t\tworld\n";
534 
535         assert(root.indentLevelByIndex(0) == 0);
536         assert(root.indentLevelByIndex(8) == 0);
537         assert(root.indentLevelByIndex(18) == 1);
538         assert(root.indentLevelByIndex(20) == 1);
539         assert(root.indentLevelByIndex(25) == 1);
540         assert(root.indentLevelByIndex(36) == 2);
541 
542     }
543 
544     /// Get suitable indent size for the line at given index, according to information from `indentor`.
545     int targetIndentLevelByIndex(size_t i) {
546 
547         const lineStart = lineStartByIndex(i);
548 
549         // Find the previous line so it can be used as reference.
550         // For the first line, `0` is used.
551         const untilPreviousLine = value[0..lineStart].chomp;
552         const previousLineIndent = lineStart == 0
553             ? 0
554             : indentLevelByIndex(untilPreviousLine.length);
555 
556         // Use the indentor if available
557         if (indentor) {
558 
559             const indentEnd = lineHomeByIndex(i);
560 
561             return max(0, previousLineIndent + indentor.indentDifference(indentEnd + prefix.length));
562 
563         }
564 
565         // Perform basic autoindenting if indentor is not available; keep the same indent at all time
566         else return indentLevelByIndex(i);
567 
568     }
569 
570     @(FluidInputAction.insertTab)
571     void insertTab() {
572 
573         // Indent selection
574         if (isSelecting) indent();
575 
576         // Insert a tab character
577         else if (useTabs) {
578 
579             push('\t');
580 
581         }
582 
583         // Align to tab
584         else {
585 
586             char[maxIndentWidth] insertTab = ' ';
587 
588             const newSpace = indentWidth - (column!dchar % indentWidth);
589 
590             push(insertTab[0 .. newSpace]);
591 
592         }
593 
594     }
595 
596     unittest {
597 
598         auto root = codeInput();
599         root.insertTab();
600         assert(root.value == "    ");
601         root.push("aa");
602         root.insertTab();
603         assert(root.value == "    aa  ");
604         root.insertTab();
605         assert(root.value == "    aa      ");
606         root.push("\n");
607         root.insertTab();
608         assert(root.value == "    aa      \n    ");
609         root.insertTab();
610         assert(root.value == "    aa      \n        ");
611         root.push("||");
612         root.insertTab();
613         assert(root.value == "    aa      \n        ||  ");
614 
615     }
616 
617     unittest {
618 
619         auto root = codeInput(.useSpaces(2));
620         root.insertTab();
621         assert(root.value == "  ");
622         root.push("aa");
623         root.insertTab();
624         assert(root.value == "  aa  ");
625         root.insertTab();
626         assert(root.value == "  aa    ");
627         root.push("\n");
628         root.insertTab();
629         assert(root.value == "  aa    \n  ");
630         root.insertTab();
631         assert(root.value == "  aa    \n    ");
632         root.push("||");
633         root.insertTab();
634         assert(root.value == "  aa    \n    ||  ");
635         root.push("x");
636         root.insertTab();
637         assert(root.value == "  aa    \n    ||  x ");
638 
639     }
640 
641     unittest {
642 
643         auto root = codeInput(.useTabs);
644         root.insertTab();
645         assert(root.value == "\t");
646         root.push("aa");
647         root.insertTab();
648         assert(root.value == "\taa\t");
649         root.insertTab();
650         assert(root.value == "\taa\t\t");
651         root.push("\n");
652         root.insertTab();
653         assert(root.value == "\taa\t\t\n\t");
654         root.insertTab();
655         assert(root.value == "\taa\t\t\n\t\t");
656         root.push("||");
657         root.insertTab();
658         assert(root.value == "\taa\t\t\n\t\t||\t");
659 
660     }
661 
662     unittest {
663 
664         const originalValue = "Fïrst line\nSëcond line\r\n Thirð\n\n line\n    Fourth line\nFifth line";
665 
666         auto root = codeInput();
667         root.push(originalValue);
668         root.selectionStart = 19;
669         root.selectionEnd = 49;
670 
671         assert(root.lineByIndex(root.selectionStart) == "Sëcond line");
672         assert(root.lineByIndex(root.selectionEnd) == "    Fourth line");
673 
674         root.insertTab();
675 
676         assert(root.value == "Fïrst line\n    Sëcond line\r\n     Thirð\n\n     line\n        Fourth line\nFifth line");
677         assert(root.lineByIndex(root.selectionStart) == "    Sëcond line");
678         assert(root.lineByIndex(root.selectionEnd) == "        Fourth line");
679 
680         root.outdent();
681 
682         assert(root.value == originalValue);
683         assert(root.lineByIndex(root.selectionStart) == "Sëcond line");
684         assert(root.lineByIndex(root.selectionEnd) == "    Fourth line");
685 
686         root.outdent();
687         assert(root.value == "Fïrst line\nSëcond line\r\nThirð\n\nline\nFourth line\nFifth line");
688 
689         root.insertTab();
690         assert(root.value == "Fïrst line\n    Sëcond line\r\n    Thirð\n\n    line\n    Fourth line\nFifth line");
691 
692     }
693 
694     unittest {
695 
696         auto root = codeInput(.useTabs);
697 
698         root.push("Hello, World!");
699         root.caretToStart();
700         root.insertTab();
701         assert(root.value == "\tHello, World!");
702         assert(root.valueBeforeCaret == "\t");
703 
704         root.undo();
705         assert(root.value == "Hello, World!");
706         assert(root.valueBeforeCaret == "");
707 
708         root.redo();
709         assert(root.value == "\tHello, World!");
710         assert(root.valueBeforeCaret == "\t");
711 
712         root.caretToEnd();
713         root.outdent();
714         assert(root.value == "Hello, World!");
715         assert(root.valueBeforeCaret == root.value);
716         assert(root.valueAfterCaret == "");
717 
718         root.undo();
719         assert(root.value == "\tHello, World!");
720         assert(root.valueBeforeCaret == root.value);
721 
722         root.undo();
723         assert(root.value == "Hello, World!");
724         assert(root.valueBeforeCaret == "");
725 
726         root.undo();
727         assert(root.value == "");
728         assert(root.valueBeforeCaret == "");
729 
730     }
731 
732     @(FluidInputAction.indent)
733     void indent() {
734 
735         indent(1);
736 
737     }
738 
739     void indent(int indentCount, bool includeEmptyLines = false) {
740 
741         // Write an undo/redo history entry
742         auto shot = snapshot();
743         scope (success) pushSnapshot(shot);
744 
745         // Indent every selected line
746         foreach (ref line; eachSelectedLine) {
747 
748             // Skip empty lines
749             if (!includeEmptyLines && line == "") continue;
750 
751             // Prepend the indent
752             line = indentRope(indentCount) ~ line;
753 
754         }
755 
756     }
757 
758     unittest {
759 
760         auto root = codeInput();
761         root.value = "a";
762         root.indent();
763         assert(root.value == "    a");
764 
765         root.value = "abc\ndef\nghi\njkl";
766         root.selectSlice(4, 9);
767         root.indent();
768         assert(root.value == "abc\n    def\n    ghi\njkl");
769 
770         root.indent(2);
771         assert(root.value == "abc\n            def\n            ghi\njkl");
772 
773     }
774 
775     unittest {
776 
777         auto root = codeInput(.useSpaces(3));
778         root.value = "a";
779         root.indent();
780         assert(root.value == "   a");
781 
782         root.value = "abc\ndef\nghi\njkl";
783         assert(root.lineByIndex(4) == "def");
784         root.selectSlice(4, 9);
785         root.indent();
786 
787         assert(root.value == "abc\n   def\n   ghi\njkl");
788 
789         root.indent(2);
790         assert(root.value == "abc\n         def\n         ghi\njkl");
791 
792     }
793 
794     unittest {
795 
796         auto root = codeInput(.useTabs);
797         root.value = "a";
798         root.indent();
799         assert(root.value == "\ta");
800 
801         root.value = "abc\ndef\nghi\njkl";
802         root.selectSlice(4, 9);
803         root.indent();
804         assert(root.value == "abc\n\tdef\n\tghi\njkl");
805 
806         root.indent(2);
807         assert(root.value == "abc\n\t\t\tdef\n\t\t\tghi\njkl");
808 
809     }
810 
811     @(FluidInputAction.outdent)
812     void outdent() {
813 
814         outdent(1);
815 
816     }
817     
818     void outdent(int i) {
819 
820         // Write an undo/redo history entry
821         auto shot = snapshot();
822         scope (success) pushSnapshot(shot);
823 
824         // Outdent every selected line
825         foreach (ref line; eachSelectedLine) {
826 
827             // Do it for each indent
828             foreach (j; 0..i) {
829 
830                 const leadingWidth = line.take(indentWidth)
831                     .until!(a => !a.among(' ', '\t'))
832                     .until("\t", No.openRight)
833                     .walkLength;
834 
835                 // Remove the tab
836                 line = line[leadingWidth .. $];
837 
838             }
839 
840         }
841 
842     }
843 
844     unittest {
845 
846         auto root = codeInput();
847         root.outdent();
848         assert(root.value == "");
849 
850         root.push("  ");
851         root.outdent();
852         assert(root.value == "");
853 
854         root.push("\t");
855         root.outdent();
856         assert(root.value == "");
857 
858         root.push("    ");
859         root.outdent();
860         assert(root.value == "");
861 
862         root.push("     ");
863         root.outdent();
864         assert(root.value == " ");
865 
866         root.push("foobarbaz  ");
867         root.insertTab();
868         root.outdent();
869         assert(root.value == "foobarbaz      ");
870 
871         root.outdent();
872         assert(root.value == "foobarbaz      ");
873 
874         root.push('\t');
875         root.outdent();
876         assert(root.value == "foobarbaz      \t");
877 
878         root.push("\n   abc  ");
879         root.outdent();
880         assert(root.value == "foobarbaz      \t\nabc  ");
881 
882         root.push("\n   \ta");
883         root.outdent();
884         assert(root.value == "foobarbaz      \t\nabc  \na");
885 
886         root.value = "\t    \t\t\ta";
887         root.outdent();
888         assert(root.value == "    \t\t\ta");
889 
890         root.outdent();
891         assert(root.value == "\t\t\ta");
892 
893         root.outdent(2);
894         assert(root.value == "\ta");
895 
896     }
897 
898     unittest {
899 
900         auto io = new HeadlessBackend;
901         auto root = codeInput();
902         root.io = io;
903         root.focus();
904 
905         // Tab twice
906         foreach (i; 0..2) {
907 
908             assert(root.value.length == i*4);
909 
910             io.nextFrame;
911             io.press(KeyboardKey.tab);
912             root.draw();
913 
914             io.nextFrame;
915             io.release(KeyboardKey.tab);
916             root.draw();
917 
918         }
919 
920         io.nextFrame;
921         root.draw();
922 
923         assert(root.value == "        ");
924         assert(root.valueBeforeCaret == "        ");
925 
926         // Outdent
927         io.nextFrame;
928         io.press(KeyboardKey.leftShift);
929         io.press(KeyboardKey.tab);
930         root.draw();
931 
932         io.nextFrame;
933         root.draw();
934 
935         assert(root.value == "    ");
936         assert(root.valueBeforeCaret == "    ");
937 
938     }
939 
940     unittest {
941 
942         auto root = codeInput(.useSpaces(2));
943         root.value = "    abc";
944         root.outdent();
945         assert(root.value == "  abc");
946         root.outdent();
947         assert(root.value == "abc");
948 
949     }
950 
951     override void chop(bool forward = false) {
952 
953         // Make it possible to backspace space-based indents
954         if (!forward && !isSelecting) {
955 
956             const lineStart = lineStartByIndex(caretIndex);
957             const lineHome = lineHomeByIndex(caretIndex);
958             const isIndent = caretIndex > lineStart && caretIndex <= lineHome;
959 
960             // This is an indent
961             if (isIndent) {
962 
963                 const line = caretLine;
964                 const col = column!char;
965                 const tabWidth = either(visualColumn % indentWidth, indentWidth);
966                 const tabStart = max(0, col - tabWidth);
967                 const allSpaces = line[tabStart .. col].all!(a => a == ' ');
968 
969                 // Remove spaces as if they were tabs
970                 if (allSpaces) {
971 
972                     const oldCaretIndex = caretIndex;
973 
974                     // Write an undo/redo history entry
975                     auto shot = snapshot();
976                     scope (success) pushSnapshot(shot);
977 
978                     caretLine = line[0 .. tabStart] ~ line[col .. $];
979                     caretIndex = oldCaretIndex - tabWidth;
980 
981                     return;
982 
983                 }
984 
985             }
986 
987         }
988 
989         super.chop(forward);
990 
991     }
992 
993     unittest {
994 
995         auto root = codeInput();
996         root.value = q{
997                 if (condition) {
998                     writeln("Hello, World!");
999                 }
1000         };
1001         root.runInputAction!(FluidInputAction.nextWord);
1002         assert(root.caretIndex == root.value.indexOf("if"));
1003         root.chop();
1004         assert(root.value == q{
1005             if (condition) {
1006                     writeln("Hello, World!");
1007                 }
1008         });
1009         root.push(' ');
1010         assert(root.value == q{
1011              if (condition) {
1012                     writeln("Hello, World!");
1013                 }
1014         });
1015         root.chop();
1016         assert(root.value == q{
1017             if (condition) {
1018                     writeln("Hello, World!");
1019                 }
1020         });
1021 
1022         // Jump a word and remove two characters
1023         root.runInputAction!(FluidInputAction.nextWord);
1024         root.chop();
1025         root.chop();
1026         assert(root.value == q{
1027             i(condition) {
1028                     writeln("Hello, World!");
1029                 }
1030         });
1031 
1032         // Push two spaces, chop one
1033         root.push("  ");
1034         root.chop();
1035         assert(root.value == q{
1036             i (condition) {
1037                     writeln("Hello, World!");
1038                 }
1039         });
1040 
1041     }
1042 
1043     unittest {
1044 
1045         auto root = codeInput();
1046         // 2 spaces, tab, 7 spaces
1047         // Effectively 2.75 of an indent
1048         root.value = "  \t       ";
1049         root.caretToEnd();
1050         root.chop();
1051 
1052         assert(root.value == "  \t    ");
1053         root.chop();
1054 
1055         // Tabs are not treated specially by chop, though
1056         // They could be, maybe, but it's such a dumb edgecase, this should be good enough for everybody
1057         // (I've checked that Kate does remove this in a single chop)
1058         assert(root.value == "  \t");
1059         root.chop();
1060         assert(root.value == "  ");
1061         root.chop();
1062         assert(root.value == "");
1063 
1064         root.value = "  \t  \t  \t";
1065         root.caretToEnd();
1066         root.chop();
1067         assert(root.value == "  \t  \t  ");
1068 
1069         root.chop();
1070         assert(root.value == "  \t  \t");
1071 
1072         root.chop();
1073         assert(root.value == "  \t  ");
1074 
1075         root.chop();
1076         assert(root.value == "  \t");
1077 
1078         root.value = "\t\t\t ";
1079         root.caretToEnd();
1080         root.chop();
1081         assert(root.value == "\t\t\t");
1082         root.chop();
1083         assert(root.value == "\t\t");
1084 
1085     }
1086 
1087     unittest {
1088 
1089         auto root = codeInput(.useSpaces(2));
1090         root.value = "      abc";
1091         root.caretIndex = 6;
1092         root.chop();
1093         assert(root.value == "    abc");
1094         root.chop();
1095         assert(root.value == "  abc");
1096         root.chop();
1097         assert(root.value == "abc");
1098         root.chop();
1099         assert(root.value == "abc");
1100 
1101         root.undo();
1102         assert(root.value == "      abc");
1103         assert(root.valueAfterCaret == "abc");
1104 
1105 
1106     }
1107 
1108     @(FluidInputAction.breakLine)
1109     protected override bool breakLine() {
1110 
1111         const currentIndent = indentLevelByIndex(caretIndex);
1112 
1113         // Break the line
1114         if (super.breakLine()) {
1115 
1116             // Copy indent from the previous line
1117             // Enable continuous input to merge the indent with the line break in the history
1118             _isContinuous = true;
1119             push(indentRope(currentIndent));
1120             reparse();
1121 
1122             // Ask the autoindentor to complete the job
1123             reformatLine();
1124             _isContinuous = false;
1125 
1126             return true;
1127 
1128         }
1129 
1130         return false;
1131 
1132     }
1133 
1134     unittest {
1135 
1136         auto root = codeInput();
1137 
1138         root.push("abcdef");
1139         root.runInputAction!(FluidInputAction.breakLine);
1140         assert(root.value == "abcdef\n");
1141 
1142         root.insertTab();
1143         root.runInputAction!(FluidInputAction.breakLine);
1144         assert(root.value == "abcdef\n    \n    ");
1145 
1146         root.insertTab();
1147         root.runInputAction!(FluidInputAction.breakLine);
1148         assert(root.value == "abcdef\n    \n        \n        ");
1149 
1150         root.outdent();
1151         root.outdent();
1152         assert(root.value == "abcdef\n    \n        \n");
1153 
1154         root.runInputAction!(FluidInputAction.breakLine);
1155         assert(root.value == "abcdef\n    \n        \n\n");
1156 
1157         root.undo();
1158         assert(root.value == "abcdef\n    \n        \n");
1159         root.undo();
1160         assert(root.value == "abcdef\n    \n        \n        ");
1161         root.undo();
1162         assert(root.value == "abcdef\n    \n        ");
1163         root.undo();
1164         assert(root.value == "abcdef\n    \n    ");
1165         root.undo();
1166         assert(root.value == "abcdef\n    ");
1167 
1168     }
1169 
1170     unittest {
1171 
1172         auto root = codeInput(.useSpaces(2));
1173         root.push("abcdef\n");
1174         root.insertTab;
1175         assert(root.caretLine == "  ");
1176         root.breakLine();
1177         assert(root.caretLine == "  ");
1178         root.breakLine();
1179         root.push("a");
1180         assert(root.caretLine == "  a");
1181 
1182         assert(root.value == "abcdef\n  \n  \n  a");
1183 
1184     }
1185 
1186     unittest {
1187 
1188         auto root = codeInput();
1189         root.value = "    abcdef";
1190         root.caretIndex = 8;
1191         root.breakLine;
1192         assert(root.value == "    abcd\n    ef");
1193 
1194     }
1195 
1196     /// Reformat a line by index of any character it contains.
1197     void reformatLineByIndex(size_t index) {
1198 
1199         import std.math;
1200 
1201         // TODO Implement reformatLine for selections
1202         if (isSelecting) return;
1203 
1204         const newIndentLevel = targetIndentLevelByIndex(index);
1205 
1206         const line = lineByIndex(index);
1207         const lineStart = lineStartByIndex(index);
1208         const lineHome = lineHomeByIndex(index);
1209         const lineEnd = lineEndByIndex(index);
1210         const newIndent = indentRope(newIndentLevel);
1211         const oldIndentLength = lineHome - lineStart;
1212 
1213         // Ignore if indent is the same
1214         if (newIndent.length == oldIndentLength) return;
1215 
1216         const oldCaretIndex = caretIndex;
1217         const newLine = newIndent ~ line[oldIndentLength .. $];
1218 
1219         // Write the new indent, replacing the old one
1220         lineByIndex(index, newLine);
1221 
1222         // Update caret index
1223         if (oldCaretIndex >= lineStart && oldCaretIndex <= lineEnd)
1224             caretIndex = clamp(oldCaretIndex + newIndent.length - oldIndentLength,
1225                 lineStart + newIndent.length,
1226                 lineStart + newLine.length);
1227 
1228         // Parse again
1229         reparse();
1230 
1231     }
1232 
1233     /// Reformat the current line.
1234     void reformatLine() {
1235 
1236         reformatLineByIndex(caretIndex);
1237 
1238     }
1239 
1240     unittest {
1241 
1242         auto root = codeInput();
1243 
1244         // 3 tabs -> 3 indents
1245         root.push("\t\t\t");
1246         root.breakLine();
1247         assert(root.value == "\t\t\t\n            ");
1248 
1249         // mixed tabs (8 width total) -> 2 indents
1250         root.value = "  \t  \t";
1251         root.caretToEnd();
1252         root.breakLine();
1253         assert(root.value == "  \t  \t\n        ");
1254 
1255         // 6 spaces -> 1 indent
1256         root.value = "      ";
1257         root.breakLine();
1258         assert(root.value == "      \n    ");
1259 
1260         // Same but now with tabs
1261         root.useTabs = true;
1262         root.reformatLine;
1263         assert(root.indentRope(1) == "\t");
1264         assert(root.value == "      \n\t");
1265 
1266         // 3 tabs -> 3 indents
1267         root.value = "\t\t\t";
1268         root.breakLine();
1269         assert(root.value == "\t\t\t\n\t\t\t");
1270 
1271         // mixed tabs (8 width total) -> 2 indents
1272         root.value = "  \t  \t";
1273         root.breakLine();
1274         assert(root.value == "  \t  \t\n\t\t");
1275 
1276         // Same but now with 2 spaces
1277         root.useTabs = false;
1278         root.indentWidth = 2;
1279         root.reformatLine;
1280         assert(root.indentRope(1) == "  ");
1281         assert(root.value == "  \t  \t\n    ");
1282 
1283         // 3 tabs -> 3 indents
1284         root.value = "\t\t\t\n";
1285         root.caretToStart;
1286         root.reformatLine;
1287         assert(root.value == "      \n");
1288 
1289         // mixed tabs (8 width total) -> 2 indents
1290         root.value = "  \t  \t";
1291         root.breakLine();
1292         assert(root.value == "  \t  \t\n        ");
1293 
1294         // 6 spaces -> 3 indents
1295         root.value = "      ";
1296         root.breakLine();
1297         assert(root.value == "      \n      ");
1298 
1299     }
1300 
1301     /// CodeInput moves `toLineStart` action handler to `toggleHome`
1302     override void caretToLineStart() {
1303 
1304         super.caretToLineStart();
1305 
1306     }
1307 
1308     /// Move the caret to the "home" position of the line, see `lineHomeByIndex`.
1309     void caretToLineHome() {
1310 
1311         caretIndex = lineHomeByIndex(caretIndex);
1312         updateCaretPosition(true);
1313         moveOrClearSelection();
1314         horizontalAnchor = caretPosition.x;
1315 
1316     }
1317 
1318     /// Move the caret to the "home" position of the line — or if the caret is already at that position, move it to
1319     /// line start. This function perceives the line visually, so if the text wraps, it will go to the beginning of the
1320     /// visible line, instead of the hard line break or the home.
1321     ///
1322     /// See_Also: `caretToLineHome` and `lineHomeByIndex`
1323     @(FluidInputAction.toLineStart)
1324     void toggleHome() {
1325 
1326         const home = lineHomeByIndex(caretIndex);
1327         const oldIndex = caretIndex;
1328 
1329         // Move to visual start of line
1330         caretToLineStart();
1331 
1332         const shouldMove = caretIndex < home
1333             || caretIndex == oldIndex;
1334 
1335         // Unless the caret was already at home, or it didn't move to start, navigate home
1336         if (oldIndex != home && shouldMove) {
1337 
1338             caretToLineHome();
1339 
1340         }
1341 
1342     }
1343 
1344     unittest {
1345 
1346         auto root = codeInput();
1347         root.value = "int main() {\n    return 0;\n}";
1348         root.caretIndex = root.value.countUntil("return");
1349         root.draw();
1350         assert(root.caretIndex == root.lineHomeByIndex(root.caretIndex));
1351 
1352         const home = root.caretIndex;
1353 
1354         // Toggle home should move to line start, because the cursor is already at home
1355         root.toggleHome();
1356         assert(root.caretIndex == home - 4);
1357         assert(root.caretIndex == root.lineStartByIndex(home));
1358 
1359         // Toggle again
1360         root.toggleHome();
1361         assert(root.caretIndex == home);
1362 
1363         // Move one character left
1364         root.caretIndex = root.caretIndex - 1;
1365         assert(root.caretIndex != home);
1366         root.toggleHome();
1367         root.draw();
1368         assert(root.caretIndex == home);
1369 
1370         // Move to first line and see if toggle home works well even if there's no indent
1371         root.caretIndex = 4;
1372         root.updateCaretPosition();
1373         root.toggleHome();
1374         assert(root.caretIndex == 0);
1375 
1376         root.toggleHome();
1377         assert(root.caretIndex == 0);
1378 
1379         // Switch to tabs
1380         const previousValue = root.value;
1381         root.useTabs = true;
1382         root.reformatLine();
1383         assert(root.value == previousValue);
1384 
1385         // Move to line below
1386         root.runInputAction!(FluidInputAction.nextLine);
1387         root.reformatLine();
1388         assert(root.value == "int main() {\n\treturn 0;\n}");
1389         assert(root.valueBeforeCaret == "int main() {\n\t");
1390 
1391         const secondLineHome = root.caretIndex;
1392         root.draw();
1393         root.toggleHome();
1394         assert(root.caretIndex == secondLineHome - 1);
1395 
1396         root.toggleHome();
1397         assert(root.caretIndex == secondLineHome);
1398 
1399     }
1400 
1401     unittest {
1402 
1403         foreach (useTabs; [false, true]) {
1404 
1405             const tabLength = useTabs ? 1 : 4;
1406 
1407             auto io = new HeadlessBackend;
1408             auto root = codeInput();
1409             root.io = io;
1410             root.useTabs = useTabs;
1411             root.value = root.indentRope ~ "long line that wraps because the viewport is too small to make it fit";
1412             root.caretIndex = tabLength;
1413             root.draw();
1414 
1415             // Move to start
1416             root.toggleHome();
1417             assert(root.caretIndex == 0);
1418 
1419             // Move home
1420             root.toggleHome();
1421             assert(root.caretIndex == tabLength);
1422 
1423             // Move to line below
1424             root.runInputAction!(FluidInputAction.nextLine);
1425 
1426             // Move to line start
1427             root.caretToLineStart();
1428             assert(root.caretIndex > tabLength);
1429 
1430             const secondLineStart = root.caretIndex;
1431 
1432             // Move a few characters to the right, and move to line start again
1433             root.caretIndex = root.caretIndex + 5;
1434             root.toggleHome();
1435             assert(root.caretIndex == secondLineStart);
1436 
1437             // If the caret is already at the start, it should move home
1438             root.toggleHome();
1439             assert(root.caretIndex == tabLength);
1440             root.toggleHome();
1441             assert(root.caretIndex == 0);
1442 
1443         }
1444 
1445     }
1446 
1447     @(FluidInputAction.paste)
1448     override void paste() {
1449 
1450         import fluid.typeface : Typeface;
1451 
1452         // Write an undo/redo history entry
1453         auto shot = snapshot();
1454         scope (success) forcePushSnapshot(shot);
1455 
1456         const pasteStart = selectionLowIndex;
1457         const clipboard = io.clipboard;
1458         auto indentLevel = indentLevelByIndex(pasteStart);
1459 
1460         // Find the smallest indent in the clipboard
1461         // Skip the first line because it's likely to be without indent when copy-pasting
1462         auto lines = Typeface.lineSplitter(clipboard).drop(1);
1463 
1464         // Count indents on each line, skip blank lines
1465         auto significantIndents = lines
1466             .map!(a => a
1467                 .countUntil!(a => !a.among(' ', '\t')))
1468             .filter!(a => a != -1);
1469 
1470         // Test blank lines only if all lines are blank
1471         const commonIndent
1472             = !significantIndents.empty ? significantIndents.minElement()
1473             : !lines.empty ? lines.front.length
1474             : 0;
1475 
1476         // Remove the common indent
1477         auto outdentedClipboard = Typeface.lineSplitter!(Yes.keepTerminator)(clipboard)
1478             .map!((a) {
1479                 const localIndent = a
1480                     .until!(a => !a.among(' ', '\t'))
1481                     .walkLength;
1482 
1483                 return a.drop(min(commonIndent, localIndent));
1484             })
1485             .map!(a => Rope(a))
1486             .array;
1487 
1488         // Push the clipboard
1489         push(Rope.merge(outdentedClipboard));
1490 
1491         reparse();
1492 
1493         const pasteEnd = caretIndex;
1494 
1495         // Reformat each line
1496         foreach (index, ref line; eachLineByIndex(pasteStart, pasteEnd)) {
1497 
1498             // Save indent of the first line, but don't reformat
1499             // `min` is used in case text is pasted inside the indent
1500             if (index <= pasteStart) {
1501                 indentLevel = min(indentLevel, indentLevelByIndex(pasteStart));
1502                 continue;
1503             }
1504 
1505             // Use the reformatter if available
1506             if (indentor) {
1507                 reformatLineByIndex(index);
1508                 line = lineByIndex(index);
1509             }
1510 
1511             // If not, prepend the indent
1512             else {
1513                 line = indentRope(indentLevel) ~ line;
1514             }
1515 
1516         }
1517 
1518         // Make sure the input is parsed completely
1519         reparse();
1520 
1521     }
1522 
1523     unittest {
1524 
1525         auto io = new HeadlessBackend;
1526         auto root = codeInput(.useTabs);
1527 
1528         io.clipboard = "text";
1529         root.io = io;
1530         root.insertTab;
1531         root.paste();
1532         assert(root.value == "\ttext");
1533 
1534         root.breakLine;
1535         root.paste();
1536         assert(root.value == "\ttext\n\ttext");
1537 
1538         io.clipboard = "text\ntext";
1539         root.value = "";
1540         root.paste();
1541         assert(root.value == "text\ntext");
1542 
1543         root.breakLine;
1544         root.insertTab;
1545         root.paste();
1546         assert(root.value == "text\ntext\n\ttext\n\ttext");
1547 
1548         io.clipboard = "  {\n    text\n  }\n";
1549         root.value = "";
1550         root.paste();
1551         assert(root.value == "{\n  text\n}\n");
1552 
1553         root.value = "\t";
1554         root.caretToEnd();
1555         root.paste();
1556         assert(root.value == "\t{\n\t  text\n\t}\n\t");
1557 
1558         root.value = "\t";
1559         root.caretToStart();
1560         root.paste();
1561         assert(root.value == "{\n  text\n}\n\t");
1562 
1563     }
1564 
1565     unittest {
1566 
1567         auto io = new HeadlessBackend;
1568         auto root = codeInput();
1569         root.io = io;
1570 
1571         foreach (i, clipboard; ["", "  ", "    ", "\t", "\t\t"]) {
1572 
1573             io.clipboard = clipboard;
1574             root.value = "";
1575             root.paste();
1576             assert(root.value == clipboard,
1577                 format!"Clipboard preset index %s (%s) not preserved"(i, clipboard));
1578 
1579         }
1580 
1581     }
1582 
1583     unittest {
1584 
1585         auto io = new HeadlessBackend;
1586         auto root = codeInput(.useTabs);
1587 
1588         io.clipboard = "text\ntext";
1589         root.io = io;
1590         root.value = "let foo() {\n\tbar\t\tbaz\n}";
1591         root.selectSlice(
1592             root.value.indexOf("bar"),
1593             root.value.indexOf("baz"),
1594         );
1595         root.paste();
1596         assert(root.value == "let foo() {\n\ttext\n\ttextbaz\n}");
1597 
1598         io.clipboard = "\t\ttext\n\ttext";
1599         root.value = "let foo() {\n\tbar\t\tbaz\n}";
1600         root.selectSlice(
1601             root.value.indexOf("bar"),
1602             root.value.indexOf("baz"),
1603         );
1604         root.paste();
1605         assert(root.value == "let foo() {\n\t\ttext\n\ttextbaz\n}");
1606 
1607     }
1608 
1609     unittest {
1610 
1611         auto io = new HeadlessBackend;
1612         auto root = codeInput(.useSpaces(2));
1613         root.io = io;
1614 
1615         io.clipboard = "World";
1616         root.push("  Hello,");
1617         root.runInputAction!(FluidInputAction.breakLine);
1618         root.paste();
1619         assert(!root._isContinuous);
1620         root.push("!");
1621         assert(root.value == "  Hello,\n  World!");
1622 
1623         // Undo the exclamation mark
1624         root.undo();
1625         assert(root.value == "  Hello,\n  World");
1626 
1627         // Undo moves before pasting
1628         root.undo();
1629         assert(root.value == "  Hello,\n  ");
1630         assert(root.valueBeforeCaret == root.value);
1631 
1632         // Next undo moves before line break
1633         root.undo();
1634         assert(root.value == "  Hello,");
1635 
1636         // Next undo clears all changes
1637         root.undo();
1638         assert(root.value == "");
1639 
1640         // No change
1641         root.undo();
1642         assert(root.value == "");
1643 
1644         // It can all be redone
1645         root.redo();
1646         assert(root.value == "  Hello,");
1647         assert(root.valueBeforeCaret == root.value);
1648         root.redo();
1649         assert(root.value == "  Hello,\n  ");
1650         assert(root.valueBeforeCaret == root.value);
1651         root.redo();
1652         assert(root.value == "  Hello,\n  World");
1653         assert(root.valueBeforeCaret == root.value);
1654         root.redo();
1655         assert(root.value == "  Hello,\n  World!");
1656         assert(root.valueBeforeCaret == root.value);
1657         root.redo();
1658         assert(root.value == "  Hello,\n  World!");
1659 
1660     }
1661 
1662     unittest {
1663 
1664         // Same test as above, but insert a space instead of line break
1665 
1666         auto io = new HeadlessBackend;
1667         auto root = codeInput(.useSpaces(2));
1668         root.io = io;
1669 
1670         io.clipboard = "World";
1671         root.push("  Hello,");
1672         root.push(" ");
1673         root.paste();
1674         root.push("!");
1675         assert(root.value == "  Hello, World!");
1676 
1677         // Undo the exclamation mark
1678         root.undo();
1679         assert(root.value == "  Hello, World");
1680 
1681         // Next undo moves before pasting, just like above
1682         root.undo();
1683         assert(root.value == "  Hello, ");
1684         assert(root.valueBeforeCaret == root.value);
1685 
1686         root.undo();
1687         assert(root.value == "");
1688 
1689         // No change
1690         root.undo();
1691         assert(root.value == "");
1692 
1693         root.redo();
1694         assert(root.value == "  Hello, ");
1695         assert(root.valueBeforeCaret == root.value);
1696 
1697     }
1698 
1699     unittest {
1700 
1701         auto indentor = new class CodeIndentor {
1702 
1703             Rope text;
1704 
1705             void parse(Rope text) {
1706 
1707                 this.text = text;
1708 
1709             }
1710 
1711             int indentDifference(ptrdiff_t offset) {
1712 
1713                 int lastLine;
1714                 int current;
1715                 int nextLine;
1716 
1717                 foreach (ch; text[0 .. offset+1].byDchar) {
1718 
1719                     if (ch == '{')
1720                         nextLine++;
1721                     else if (ch == '}')
1722                         current--;
1723                     else if (ch == '\n') {
1724                         lastLine = current;
1725                         current = nextLine;
1726                     }
1727 
1728                 }
1729 
1730                 return current - lastLine;
1731 
1732             }
1733 
1734         };
1735         auto io = new HeadlessBackend;
1736         auto root = codeInput(.useTabs);
1737 
1738         // In this test, the indentor does nothing but preserve last indent
1739         io.clipboard = "text\ntext";
1740         root.io = io;
1741         root.indentor = indentor;
1742         root.insertTab;
1743         root.paste();
1744         assert(root.value == "\ttext\n\ttext");
1745 
1746         io.clipboard = "let foo() {\n\tbar\n}";
1747         root.value = "";
1748         root.paste();
1749         assert(root.value == "let foo() {\n\tbar\n}");
1750 
1751         root.caretIndex = root.value.indexOf("bar");
1752         root.runInputAction!(FluidInputAction.selectNextWord);
1753         assert(root.selectedValue == "bar");
1754 
1755         root.paste();
1756         assert(root.value == "let foo() {\n\tlet foo() {\n\t\tbar\n\t}\n}");
1757 
1758     }
1759 
1760     unittest {
1761 
1762         auto io = new HeadlessBackend;
1763         auto root = codeInput(.useTabs);
1764 
1765         io.clipboard = "  foo\n  ";
1766         root.io = io;
1767         root.value = "let foo() {\n\t\n}";
1768         root.caretIndex = root.value.indexOf("\n}");
1769         root.paste();
1770         assert(root.value == "let foo() {\n\tfoo\n\t\n}");
1771 
1772         io.clipboard = "foo\n  bar\n";
1773         root.value = "let foo() {\n\tx\n}";
1774         root.caretIndex = root.value.indexOf("x");
1775         root.paste();
1776         assert(root.value == "let foo() {\n\tfoo\n\tbar\n\tx\n}");
1777 
1778     }
1779 
1780 }
1781 
1782 ///
1783 unittest {
1784 
1785     // Start a code editor
1786     codeInput();
1787 
1788     // Start a code editor that uses tabs
1789     codeInput(
1790         .useTabs
1791     );
1792 
1793     // Or, 2 spaces, if you prefer — the default is 4 spaces
1794     codeInput(
1795         .useSpaces(2)
1796     );
1797 
1798 }
1799 
1800 alias CodeToken = ubyte;
1801 alias CodeSlice = TextStyleSlice;
1802 
1803 // Note: This was originally a member of CodeHighlighter, but it broke the vtable sometimes...? I wasn't able to
1804 // produce a minimal example to open a bug ticket, sorry.
1805 alias CodeHighlighterRange = typeof(CodeHighlighter.save());
1806 
1807 /// Implements syntax highlighting for `CodeInput`.
1808 /// Warning: This API is unstable and might change without warning.
1809 interface CodeHighlighter {
1810 
1811     /// Get a name for the token at given index. Returns null if there isn't a token at given index. Indices must be
1812     /// sequential. Starts at 1.
1813     const(char)[] nextTokenName(CodeToken index);
1814 
1815     /// Parse the given text to use with other functions in the highlighter.
1816     void parse(Rope text);
1817 
1818     /// Find the next important range starting with the byte at given index.
1819     ///
1820     /// Tip: Query is likely to be called again with `byteIndex` set to the value of `range.end`.
1821     ///
1822     /// Returns:
1823     ///     The next relevant code range. Parts with no highlighting should be ignored. If there is nothing left to
1824     ///     highlight, should return `init`.
1825     CodeSlice query(size_t byteIndex)
1826     in (byteIndex != size_t.max, "Invalid byte index (-1)")
1827     out (r; r.end != byteIndex, "query() must not return empty ranges");
1828 
1829     /// Produce a TextStyleSlice range using the result.
1830     /// Params:
1831     ///     offset = Number of bytes to skip. Apply the offset to all resulting items.
1832     /// Returns: `CodeHighlighterRange` suitable for use as a `Text` style map.
1833     final save(int offset = 0) {
1834 
1835         static struct HighlighterRange {
1836 
1837             CodeHighlighter highlighter;
1838             TextStyleSlice front;
1839             int offset;
1840 
1841             bool empty() const {
1842 
1843                 return front is front.init;
1844 
1845             }
1846 
1847             // Continue where the last token ended
1848             void popFront() {
1849 
1850                 do front = highlighter.query(front.end + offset).offset(-offset);
1851 
1852                 // Pop again if got a null token
1853                 while (front.styleIndex == 0 && front !is front.init);
1854 
1855             }
1856 
1857             HighlighterRange save() {
1858 
1859                 return this;
1860 
1861             }
1862 
1863         }
1864 
1865         return HighlighterRange(this, query(offset).offset(-offset), offset);
1866 
1867     }
1868 
1869 }
1870 
1871 unittest {
1872 
1873     import std.typecons : BlackHole;
1874 
1875     enum tokenFunction = 1;
1876     enum tokenString = 2;
1877 
1878     auto text = `print("Hello, World!")`;
1879     auto highlighter = new class BlackHole!CodeHighlighter {
1880 
1881         override CodeSlice query(size_t byteIndex) {
1882 
1883             if (byteIndex == 0) return CodeSlice(0, 5, tokenFunction);
1884             if (byteIndex <= 6) return CodeSlice(6, 21, tokenString);
1885             return CodeSlice.init;
1886 
1887         }
1888 
1889     };
1890 
1891     auto root = codeInput(highlighter);
1892     root.draw();
1893 
1894     assert(root.contentLabel.text.styleMap.equal([
1895         TextStyleSlice(0, 5, tokenFunction),
1896         TextStyleSlice(6, 21, tokenString),
1897     ]));
1898 
1899 }
1900 
1901 interface CodeIndentor {
1902 
1903     /// Parse the given text.
1904     void parse(Rope text);
1905 
1906     /// Get indent level for the given offset, relative to the previous line.
1907     ///
1908     /// `CodeInput` will use the first non-white character on a line as a reference for reformatting.
1909     int indentDifference(ptrdiff_t offset);
1910 
1911 }
1912 
1913 unittest {
1914 
1915     import std.typecons : BlackHole;
1916 
1917     auto originalText
1918        = "void foo() {\n"
1919        ~ "fun();\n"
1920        ~ "functionCall(\n"
1921        ~ "stuff()\n"
1922        ~ ");\n"
1923        ~ "    }\n";
1924     auto formattedText
1925        = "void foo() {\n"
1926        ~ "    fun();\n"
1927        ~ "    functionCall(\n"
1928        ~ "        stuff()\n"
1929        ~ "    );\n"
1930        ~ "}\n";
1931 
1932     class Indentor : BlackHole!CodeIndentor {
1933 
1934         struct Indent {
1935             ptrdiff_t offset;
1936             int indent;
1937         }
1938 
1939         Indent[] indents;
1940 
1941         override void parse(Rope rope) {
1942 
1943             bool lineStart;
1944 
1945             indents = [Indent(0, 0)];
1946 
1947             foreach (i, ch; rope.enumerate) {
1948 
1949                 if (ch.among('{', '(')) {
1950                     indents ~= Indent(i + 1, 1);
1951                 }
1952 
1953                 else if (ch.among('}', ')')) {
1954                     indents ~= Indent(i, lineStart ? -1 : 0);
1955                 }
1956 
1957                 else if (ch == '\n') lineStart = true;
1958                 else if (ch != ' ') lineStart = false;
1959 
1960             }
1961 
1962         }
1963 
1964         override int indentDifference(ptrdiff_t offset) {
1965 
1966             return indents
1967                 .filter!(a => a.offset <= offset)
1968                 .tail(1)
1969                 .front
1970                 .indent;
1971 
1972         }
1973 
1974     }
1975 
1976     auto indentor = new Indentor;
1977     auto highlighter = new class Indentor, CodeHighlighter {
1978 
1979         const(char)[] nextTokenName(ubyte) {
1980             return null;
1981         }
1982 
1983         CodeSlice query(size_t) {
1984             return CodeSlice.init;
1985         }
1986 
1987         override void parse(Rope value) {
1988             super.parse(value);
1989         }
1990 
1991     };
1992 
1993     auto indentorOnlyInput = codeInput();
1994     indentorOnlyInput.indentor = indentor;
1995     auto highlighterInput = codeInput(highlighter);
1996 
1997     foreach (root; [indentorOnlyInput, highlighterInput]) {
1998 
1999         root.value = originalText;
2000         root.draw();
2001 
2002         // Reformat first line
2003         root.caretIndex = 0;
2004         assert(root.targetIndentLevelByIndex(0) == 0);
2005         root.reformatLine();
2006         assert(root.value == originalText);
2007 
2008         // Reformat second line
2009         root.caretIndex = 13;
2010         assert(root.indentor.indentDifference(13) == 1);
2011         assert(root.targetIndentLevelByIndex(13) == 1);
2012         root.reformatLine();
2013         assert(root.value == formattedText[0..23] ~ originalText[19..$]);
2014 
2015         // Reformat third line
2016         root.caretIndex = 24;
2017         assert(root.indentor.indentDifference(24) == 0);
2018         assert(root.targetIndentLevelByIndex(24) == 1);
2019         root.reformatLine();
2020         assert(root.value == formattedText[0..42] ~ originalText[34..$]);
2021 
2022         // Reformat fourth line
2023         root.caretIndex = 42;
2024         assert(root.indentor.indentDifference(42) == 1);
2025         assert(root.targetIndentLevelByIndex(42) == 2);
2026         root.reformatLine();
2027         assert(root.value == formattedText[0..58] ~ originalText[42..$]);
2028 
2029         // Reformat fifth line
2030         root.caretIndex = 58;
2031         assert(root.indentor.indentDifference(58) == -1);
2032         assert(root.targetIndentLevelByIndex(58) == 1);
2033         root.reformatLine();
2034         assert(root.value == formattedText[0..65] ~ originalText[45..$]);
2035 
2036         // And the last line, finally
2037         root.caretIndex = 65;
2038         assert(root.indentor.indentDifference(65) == -1);
2039         assert(root.targetIndentLevelByIndex(65) == 0);
2040         root.reformatLine();
2041         assert(root.value == formattedText);
2042 
2043     }
2044 
2045 }
2046 
2047 unittest {
2048 
2049     import std.typecons : BlackHole;
2050 
2051     class Indentor : BlackHole!CodeIndentor {
2052 
2053         bool outdent;
2054 
2055         override void parse(Rope rope) {
2056 
2057             outdent = rope.canFind("end");
2058 
2059         }
2060 
2061         override int indentDifference(ptrdiff_t offset) {
2062 
2063             if (outdent)
2064                 return -1;
2065             else
2066                 return 1;
2067 
2068         }
2069 
2070     }
2071 
2072     // Every new line indents. If "end" is found in the text, every new line *outdents*, effectively making the text
2073     // flat.
2074     auto io = new HeadlessBackend;
2075     auto root = codeInput();
2076     root.io = io;
2077     root.indentor = new Indentor;
2078     root.value = "begin";
2079     root.focus();
2080     root.draw();
2081     assert(root.value == "begin");
2082 
2083     // The difference defaults to 1 in this case, so the line should be indented
2084     root.reformatLine();
2085     assert(root.value == "    begin");
2086 
2087     // But, if the "end" keyword is added, it should outdent automatically
2088     io.nextFrame;
2089     io.inputCharacter = " end";
2090     root.caretToEnd();
2091     root.draw();
2092     io.nextFrame;
2093     root.draw();
2094     assert(root.value == "begin end");
2095 
2096     // Backspace also triggers updates
2097     io.nextFrame;
2098     io.press(KeyboardKey.backspace);
2099     root.draw();
2100     io.nextFrame;
2101     io.release(KeyboardKey.backspace);
2102     root.draw();
2103     assert(root.value == "    begin en");
2104 
2105     // However, no change should be made if the keyword was in place before
2106     io.nextFrame;
2107     io.inputCharacter = " ";
2108     root.value = "    begin end";
2109     root.caretToEnd();
2110     root.draw();
2111     io.nextFrame;
2112     root.draw();
2113     assert(root.value == "    begin end ");
2114 
2115     io.nextFrame;
2116     root.value = "Hello\n    bar";
2117     root.clearHistory();
2118     root.caretIndex = 5;
2119     root.runInputAction!(FluidInputAction.breakLine);
2120     assert(root.value == "Hello\n    \n    bar");
2121 
2122     root.runInputAction!(FluidInputAction.undo);
2123     assert(root.value == "Hello\n    bar");
2124 
2125     root.indent();
2126     assert(root.value == "    Hello\n    bar");
2127 
2128     root.caretIndex = 9;
2129     root.runInputAction!(FluidInputAction.breakLine);
2130     assert(root.value == "    Hello\n        \n    bar");
2131 
2132     root.runInputAction!(FluidInputAction.undo);
2133     assert(root.value == "    Hello\n    bar");
2134 
2135 }
2136 
2137 unittest {
2138 
2139     auto roots = [
2140         codeInput(.nullTheme, .useSpaces(2)),
2141         codeInput(.nullTheme, .useTabs(2)),
2142     ];
2143 
2144     // Draw each root
2145     foreach (i, root; roots) {
2146         root.insertTab();
2147         root.push("a");
2148         root.draw();
2149     }
2150 
2151     assert(roots[0].value == "  a");
2152     assert(roots[1].value == "\ta");
2153 
2154     // Drawn text content has to be identical, since both have the same indent width
2155     assert(roots.all!(a => a.contentLabel.text.texture.chunks.length == 1));
2156     assert(roots[0].contentLabel.text.texture.chunks[0].image.data
2157         == roots[1].contentLabel.text.texture.chunks[0].image.data);
2158 
2159 }
2160 
2161 unittest {
2162 
2163     auto roots = [
2164         codeInput(.nullTheme, .useSpaces(1)),
2165         codeInput(.nullTheme, .useSpaces(2)),
2166         codeInput(.nullTheme, .useSpaces(4)),
2167         codeInput(.nullTheme, .useSpaces(8)),
2168         codeInput(.nullTheme, .useTabs(1)),
2169         codeInput(.nullTheme, .useTabs(2)),
2170         codeInput(.nullTheme, .useTabs(4)),
2171         codeInput(.nullTheme, .useTabs(8)),
2172     ];
2173 
2174     foreach (root; roots) {
2175         root.insertTab();
2176         root.push("a");
2177         root.draw();
2178     }
2179 
2180     assert(roots[0].value == " a");
2181     assert(roots[1].value == "  a");
2182     assert(roots[2].value == "    a");
2183     assert(roots[3].value == "        a");
2184 
2185     foreach (root; roots[4..8]) {
2186 
2187         assert(root.value == "\ta");
2188 
2189     }
2190 
2191     float indentWidth(CodeInput root) {
2192         return root.contentLabel.text.indentWidth;
2193     }
2194 
2195     foreach (i; [0, 4]) {
2196 
2197         assert(indentWidth(roots[i + 0]) * 2 == indentWidth(roots[i + 1]));
2198         assert(indentWidth(roots[i + 1]) * 2 == indentWidth(roots[i + 2]));
2199         assert(indentWidth(roots[i + 2]) * 2 == indentWidth(roots[i + 3]));
2200 
2201     }
2202 
2203     foreach (i; 0..4) {
2204 
2205         assert(indentWidth(roots[0 + i]) == indentWidth(roots[4 + i]),
2206             "Indent widths should be the same for both space and tab based roots");
2207 
2208     }
2209 
2210 }