1use tui_textarea::{CursorMove, TextArea};
18
19#[derive(Debug)]
20pub struct InputState {
21 pub lines: Vec<String>,
22 pub cursor_row: usize,
23 pub cursor_col: usize,
24 pub version: u64,
27 pub paste_blocks: Vec<String>,
31 editor: TextArea<'static>,
32}
33
34const PASTE_PREFIX: &str = "[Pasted Text ";
36const PASTE_SUFFIX: &str = "]";
37pub const PASTE_PLACEHOLDER_LINE_THRESHOLD: usize = 10;
39
40impl InputState {
41 pub fn new() -> Self {
42 Self {
43 lines: vec![String::new()],
44 cursor_row: 0,
45 cursor_col: 0,
46 version: 0,
47 paste_blocks: Vec::new(),
48 editor: TextArea::default(),
49 }
50 }
51
52 #[must_use]
53 pub fn text(&self) -> String {
54 if self.paste_blocks.is_empty() {
55 return self.lines.join("\n");
56 }
57 let mut result = String::new();
59 for (i, line) in self.lines.iter().enumerate() {
60 if i > 0 {
61 result.push('\n');
62 }
63 if let Some((idx, suffix_end)) = parse_paste_placeholder_with_suffix(line) {
64 if let Some(content) = self.paste_blocks.get(idx) {
65 result.push_str(content);
66 if suffix_end < line.len() {
67 result.push_str(&line[suffix_end..]);
68 }
69 } else {
70 result.push_str(line);
71 }
72 } else {
73 result.push_str(line);
74 }
75 }
76 result
77 }
78
79 #[must_use]
80 pub fn is_empty(&self) -> bool {
81 self.lines.len() == 1 && self.lines[0].is_empty()
82 }
83
84 pub fn clear(&mut self) {
85 self.lines = vec![String::new()];
86 self.cursor_row = 0;
87 self.cursor_col = 0;
88 self.paste_blocks.clear();
89 self.version += 1;
90 self.rebuild_editor_from_snapshot();
91 }
92
93 pub fn set_text(&mut self, text: &str) {
95 self.lines = text.split('\n').map(String::from).collect();
96 if self.lines.is_empty() {
97 self.lines.push(String::new());
98 }
99 self.cursor_row = self.lines.len() - 1;
100 self.cursor_col = self.lines[self.cursor_row].chars().count();
101 self.paste_blocks.clear();
102 self.version += 1;
103 self.rebuild_editor_from_snapshot();
104 }
105
106 pub fn insert_char(&mut self, c: char) {
107 let line = &mut self.lines[self.cursor_row];
108 let byte_idx = char_to_byte_index(line, self.cursor_col);
109 line.insert(byte_idx, c);
110 self.cursor_col += 1;
111 self.version += 1;
112 }
113
114 fn as_textarea(&self) -> TextArea<'static> {
115 let mut textarea = TextArea::from(self.lines.clone());
116 textarea.move_cursor(CursorMove::Jump(
117 u16::try_from(self.cursor_row).unwrap_or(u16::MAX),
118 u16::try_from(self.cursor_col).unwrap_or(u16::MAX),
119 ));
120 textarea
121 }
122
123 fn sync_snapshot_from_editor(&mut self) -> bool {
124 let (row, col) = self.editor.cursor();
125 let lines = self.editor.lines().to_vec();
126 if self.lines == lines && self.cursor_row == row && self.cursor_col == col {
127 return false;
128 }
129 self.lines = lines;
130 self.cursor_row = row;
131 self.cursor_col = col;
132 self.version += 1;
133 true
134 }
135
136 fn rebuild_editor_from_snapshot(&mut self) {
137 self.editor = self.as_textarea();
138 }
139
140 pub fn sync_textarea_engine(&mut self) {
141 self.rebuild_editor_from_snapshot();
142 }
143
144 fn ensure_editor_synced_from_snapshot(&mut self) {
145 if self.editor.cursor() != (self.cursor_row, self.cursor_col)
146 || self.editor.lines() != self.lines.as_slice()
147 {
148 self.rebuild_editor_from_snapshot();
149 }
150 }
151
152 fn apply_textarea_edit(&mut self, edit: impl FnOnce(&mut TextArea<'_>)) -> bool {
153 self.ensure_editor_synced_from_snapshot();
154 edit(&mut self.editor);
155 self.sync_snapshot_from_editor()
156 }
157
158 pub fn textarea_insert_char(&mut self, c: char) -> bool {
159 self.apply_textarea_edit(|textarea| {
160 textarea.insert_char(c);
161 })
162 }
163
164 pub fn textarea_insert_newline(&mut self) -> bool {
165 self.apply_textarea_edit(|textarea| {
166 textarea.insert_newline();
167 })
168 }
169
170 pub fn textarea_delete_char_before(&mut self) -> bool {
171 self.apply_textarea_edit(|textarea| {
172 let _ = textarea.delete_char();
173 })
174 }
175
176 pub fn textarea_delete_char_after(&mut self) -> bool {
177 self.apply_textarea_edit(|textarea| {
178 let _ = textarea.delete_next_char();
179 })
180 }
181
182 pub fn textarea_move_left(&mut self) -> bool {
183 self.apply_textarea_edit(|textarea| {
184 textarea.move_cursor(CursorMove::Back);
185 })
186 }
187
188 pub fn textarea_move_right(&mut self) -> bool {
189 self.apply_textarea_edit(|textarea| {
190 textarea.move_cursor(CursorMove::Forward);
191 })
192 }
193
194 pub fn textarea_move_up(&mut self) -> bool {
195 self.apply_textarea_edit(|textarea| {
196 textarea.move_cursor(CursorMove::Up);
197 })
198 }
199
200 pub fn textarea_move_down(&mut self) -> bool {
201 self.apply_textarea_edit(|textarea| {
202 textarea.move_cursor(CursorMove::Down);
203 })
204 }
205
206 pub fn textarea_move_home(&mut self) -> bool {
207 self.apply_textarea_edit(|textarea| {
208 textarea.move_cursor(CursorMove::Head);
209 })
210 }
211
212 pub fn textarea_move_end(&mut self) -> bool {
213 self.apply_textarea_edit(|textarea| {
214 textarea.move_cursor(CursorMove::End);
215 })
216 }
217
218 pub fn textarea_undo(&mut self) -> bool {
219 self.apply_textarea_edit(|textarea| {
220 let _ = textarea.undo();
221 })
222 }
223
224 pub fn textarea_redo(&mut self) -> bool {
225 self.apply_textarea_edit(|textarea| {
226 let _ = textarea.redo();
227 })
228 }
229
230 pub fn textarea_move_word_left(&mut self) -> bool {
231 self.apply_textarea_edit(|textarea| {
232 textarea.move_cursor(CursorMove::WordBack);
233 })
234 }
235
236 pub fn textarea_move_word_right(&mut self) -> bool {
237 self.apply_textarea_edit(|textarea| {
238 textarea.move_cursor(CursorMove::WordForward);
239 })
240 }
241
242 pub fn textarea_delete_word_before(&mut self) -> bool {
243 self.apply_textarea_edit(|textarea| {
244 let _ = textarea.delete_word();
245 })
246 }
247
248 pub fn textarea_delete_word_after(&mut self) -> bool {
249 self.apply_textarea_edit(|textarea| {
250 let _ = textarea.delete_next_word();
251 })
252 }
253
254 pub fn insert_newline(&mut self) {
255 let line = &mut self.lines[self.cursor_row];
256 let byte_idx = char_to_byte_index(line, self.cursor_col);
257 let rest = line[byte_idx..].to_string();
258 line.truncate(byte_idx);
259 self.cursor_row += 1;
260 self.lines.insert(self.cursor_row, rest);
261 self.cursor_col = 0;
262 self.version += 1;
263 }
264
265 pub fn insert_str(&mut self, s: &str) {
266 for c in s.chars() {
267 if c == '\n' || c == '\r' {
268 self.insert_newline();
269 } else {
270 self.insert_char(c);
271 }
272 }
273 }
274
275 pub fn insert_paste_block(&mut self, text: &str) -> String {
279 let idx = self.paste_blocks.len();
280 let placeholder = paste_placeholder_label(idx, count_text_lines(text));
281 self.paste_blocks.push(text.to_owned());
282
283 let current_line = &mut self.lines[self.cursor_row];
286 let byte_idx = char_to_byte_index(current_line, self.cursor_col);
287 let after = current_line[byte_idx..].to_string();
288 current_line.truncate(byte_idx);
289 let before_empty = current_line.is_empty();
290
291 if before_empty {
292 current_line.clone_from(&placeholder);
294 if !after.is_empty() {
295 self.lines.insert(self.cursor_row + 1, after);
296 }
297 self.cursor_col = placeholder.chars().count();
298 } else {
299 self.cursor_row += 1;
301 self.lines.insert(self.cursor_row, placeholder.clone());
302 if !after.is_empty() {
303 self.lines.insert(self.cursor_row + 1, after);
304 }
305 self.cursor_col = placeholder.chars().count();
306 }
307
308 self.version += 1;
309 placeholder
310 }
311
312 pub fn append_to_active_paste_block(&mut self, text: &str) -> bool {
318 let Some(current_line) = self.lines.get(self.cursor_row) else {
319 return false;
320 };
321 let Some(idx) = parse_paste_placeholder(current_line) else {
322 return false;
323 };
324 if self.cursor_col != current_line.chars().count() {
326 return false;
327 }
328
329 let Some(block) = self.paste_blocks.get_mut(idx) else {
330 return false;
331 };
332 block.push_str(text);
333
334 self.lines[self.cursor_row] = paste_placeholder_label(idx, count_text_lines(block));
335 self.cursor_col = self.lines[self.cursor_row].chars().count();
336 self.version += 1;
337 true
338 }
339
340 pub fn delete_char_before(&mut self) {
341 if self.cursor_col > 0 {
342 let line = &mut self.lines[self.cursor_row];
343 self.cursor_col -= 1;
344 let byte_idx = char_to_byte_index(line, self.cursor_col);
345 line.remove(byte_idx);
346 self.version += 1;
347 } else if self.cursor_row > 0 {
348 let removed = self.lines.remove(self.cursor_row);
349 self.cursor_row -= 1;
350 self.cursor_col = self.lines[self.cursor_row].chars().count();
351 self.lines[self.cursor_row].push_str(&removed);
352 self.version += 1;
353 }
354 }
355
356 pub fn delete_char_after(&mut self) {
357 let line_len = self.lines[self.cursor_row].chars().count();
358 if self.cursor_col < line_len {
359 let line = &mut self.lines[self.cursor_row];
360 let byte_idx = char_to_byte_index(line, self.cursor_col);
361 line.remove(byte_idx);
362 self.version += 1;
363 } else if self.cursor_row + 1 < self.lines.len() {
364 let next = self.lines.remove(self.cursor_row + 1);
365 self.lines[self.cursor_row].push_str(&next);
366 self.version += 1;
367 }
368 }
369
370 pub fn move_left(&mut self) {
371 if self.cursor_col > 0 {
372 self.cursor_col -= 1;
373 self.version += 1;
374 } else if self.cursor_row > 0 {
375 self.cursor_row -= 1;
376 self.cursor_col = self.lines[self.cursor_row].chars().count();
377 self.version += 1;
378 }
379 }
380
381 pub fn move_right(&mut self) {
382 let line_len = self.lines[self.cursor_row].chars().count();
383 if self.cursor_col < line_len {
384 self.cursor_col += 1;
385 self.version += 1;
386 } else if self.cursor_row + 1 < self.lines.len() {
387 self.cursor_row += 1;
388 self.cursor_col = 0;
389 self.version += 1;
390 }
391 }
392
393 pub fn move_up(&mut self) {
394 if self.cursor_row > 0 {
395 self.cursor_row -= 1;
396 let line_len = self.lines[self.cursor_row].chars().count();
397 self.cursor_col = self.cursor_col.min(line_len);
398 self.version += 1;
399 }
400 }
401
402 pub fn move_down(&mut self) {
403 if self.cursor_row + 1 < self.lines.len() {
404 self.cursor_row += 1;
405 let line_len = self.lines[self.cursor_row].chars().count();
406 self.cursor_col = self.cursor_col.min(line_len);
407 self.version += 1;
408 }
409 }
410
411 pub fn move_home(&mut self) {
412 self.cursor_col = 0;
413 self.version += 1;
414 }
415
416 pub fn move_end(&mut self) {
417 self.cursor_col = self.lines[self.cursor_row].chars().count();
418 self.version += 1;
419 }
420
421 #[must_use]
422 pub fn line_count(&self) -> u16 {
423 u16::try_from(self.lines.len()).unwrap_or(u16::MAX)
424 }
425}
426
427impl Default for InputState {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433fn char_to_byte_index(s: &str, char_idx: usize) -> usize {
435 s.char_indices().nth(char_idx).map_or(s.len(), |(i, _)| i)
436}
437
438#[must_use]
440pub fn count_text_lines(text: &str) -> usize {
441 let mut lines = 1;
443 let bytes = text.as_bytes();
444 let mut i = 0;
445 while i < bytes.len() {
446 match bytes[i] {
447 b'\n' => lines += 1,
448 b'\r' => {
449 lines += 1;
450 if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
451 i += 1;
452 }
453 }
454 _ => {}
455 }
456 i += 1;
457 }
458 lines
459}
460
461#[must_use]
463pub fn trim_trailing_line_breaks(mut text: &str) -> &str {
464 while let Some(stripped) = text.strip_suffix('\n').or_else(|| text.strip_suffix('\r')) {
465 text = stripped;
466 }
467 text
468}
469
470fn paste_placeholder_label(idx: usize, line_count: usize) -> String {
471 format!("{PASTE_PREFIX}{} - {line_count} lines{PASTE_SUFFIX}", idx + 1)
472}
473
474pub fn parse_paste_placeholder_with_suffix(line: &str) -> Option<(usize, usize)> {
478 let rest = line.strip_prefix(PASTE_PREFIX)?;
479 let close_rel = rest.find(PASTE_SUFFIX)?;
480 let rest = &rest[..close_rel];
481 let num_str = rest.split(" - ").next()?;
482 let n: usize = num_str.parse().ok()?;
483 if n == 0 {
484 return None;
485 }
486 let end = PASTE_PREFIX.len() + close_rel + PASTE_SUFFIX.len();
487 Some((n - 1, end))
488}
489
490pub fn parse_paste_placeholder(line: &str) -> Option<usize> {
492 let (idx, suffix_end) = parse_paste_placeholder_with_suffix(line)?;
493 if suffix_end == line.len() { Some(idx) } else { None }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
503 use pretty_assertions::assert_eq;
504
505 #[test]
508 fn char_to_byte_index_ascii() {
509 assert_eq!(char_to_byte_index("hello", 0), 0);
510 assert_eq!(char_to_byte_index("hello", 2), 2);
511 assert_eq!(char_to_byte_index("hello", 5), 5); }
513
514 #[test]
515 fn char_to_byte_index_multibyte_utf8() {
516 let s = "cafe\u{0301}"; assert_eq!(char_to_byte_index(s, 4), 4); }
520
521 #[test]
522 fn char_to_byte_index_emoji() {
523 let s = "\u{1F600}hello"; assert_eq!(char_to_byte_index(s, 0), 0);
525 assert_eq!(char_to_byte_index(s, 1), 4); }
527
528 #[test]
529 fn char_to_byte_index_beyond_string() {
530 assert_eq!(char_to_byte_index("ab", 10), 2); }
532
533 #[test]
534 fn char_to_byte_index_empty_string() {
535 assert_eq!(char_to_byte_index("", 0), 0);
536 assert_eq!(char_to_byte_index("", 5), 0);
537 }
538
539 #[test]
542 fn new_creates_empty_state() {
543 let input = InputState::new();
544 assert_eq!(input.lines, vec![String::new()]);
545 assert_eq!(input.cursor_row, 0);
546 assert_eq!(input.cursor_col, 0);
547 assert_eq!(input.version, 0);
548 }
549
550 #[test]
551 fn default_equals_new() {
552 let a = InputState::new();
553 let b = InputState::default();
554 assert_eq!(a.lines, b.lines);
555 assert_eq!(a.cursor_row, b.cursor_row);
556 assert_eq!(a.cursor_col, b.cursor_col);
557 assert_eq!(a.version, b.version);
558 }
559
560 #[test]
563 fn text_single_empty_line() {
564 let input = InputState::new();
565 assert_eq!(input.text(), "");
566 }
567
568 #[test]
569 fn text_joins_lines_with_newline() {
570 let mut input = InputState::new();
571 input.insert_str("line1\nline2\nline3");
572 assert_eq!(input.text(), "line1\nline2\nline3");
573 }
574
575 #[test]
578 fn is_empty_true_for_new() {
579 assert!(InputState::new().is_empty());
580 }
581
582 #[test]
583 fn is_empty_false_after_insert() {
584 let mut input = InputState::new();
585 input.insert_char('a');
586 assert!(!input.is_empty());
587 }
588
589 #[test]
590 fn is_empty_false_for_empty_multiline() {
591 let mut input = InputState::new();
593 input.insert_newline();
594 assert!(!input.is_empty());
595 }
596
597 #[test]
600 fn clear_resets_to_empty() {
601 let mut input = InputState::new();
602 input.insert_str("hello\nworld");
603 let v_before = input.version;
604 input.clear();
605 assert!(input.is_empty());
606 assert_eq!(input.cursor_row, 0);
607 assert_eq!(input.cursor_col, 0);
608 assert!(input.version > v_before);
609 }
610
611 #[test]
614 fn insert_char_ascii() {
615 let mut input = InputState::new();
616 input.insert_char('h');
617 input.insert_char('i');
618 assert_eq!(input.lines[0], "hi");
619 assert_eq!(input.cursor_col, 2);
620 }
621
622 #[test]
623 fn insert_char_unicode_emoji() {
624 let mut input = InputState::new();
625 input.insert_char('\u{1F600}'); assert_eq!(input.cursor_col, 1);
627 assert_eq!(input.lines[0], "\u{1F600}");
628 }
629
630 #[test]
631 fn insert_char_cjk() {
632 let mut input = InputState::new();
633 input.insert_char('\u{4F60}'); input.insert_char('\u{597D}'); assert_eq!(input.lines[0], "\u{4F60}\u{597D}");
636 assert_eq!(input.cursor_col, 2);
637 }
638
639 #[test]
640 fn insert_char_mid_line() {
641 let mut input = InputState::new();
642 input.insert_str("ac");
643 input.move_left(); input.insert_char('b');
645 assert_eq!(input.lines[0], "abc");
646 assert_eq!(input.cursor_col, 2);
647 }
648
649 #[test]
650 fn insert_char_bumps_version() {
651 let mut input = InputState::new();
652 let v = input.version;
653 input.insert_char('x');
654 assert!(input.version > v);
655 }
656
657 #[test]
660 fn insert_newline_at_end() {
661 let mut input = InputState::new();
662 input.insert_str("hello");
663 input.insert_newline();
664 assert_eq!(input.lines, vec!["hello", ""]);
665 assert_eq!(input.cursor_row, 1);
666 assert_eq!(input.cursor_col, 0);
667 }
668
669 #[test]
670 fn insert_newline_mid_line() {
671 let mut input = InputState::new();
672 input.insert_str("helloworld");
673 input.cursor_col = 5;
675 input.insert_newline();
676 assert_eq!(input.lines, vec!["hello", "world"]);
677 assert_eq!(input.cursor_row, 1);
678 assert_eq!(input.cursor_col, 0);
679 }
680
681 #[test]
682 fn insert_newline_at_start() {
683 let mut input = InputState::new();
684 input.insert_str("hello");
685 input.move_home();
686 input.insert_newline();
687 assert_eq!(input.lines, vec!["", "hello"]);
688 }
689
690 #[test]
693 fn insert_str_multiline() {
694 let mut input = InputState::new();
695 input.insert_str("line1\nline2\nline3");
696 assert_eq!(input.lines, vec!["line1", "line2", "line3"]);
697 assert_eq!(input.cursor_row, 2);
698 assert_eq!(input.cursor_col, 5);
699 }
700
701 #[test]
702 fn insert_str_with_carriage_returns() {
703 let mut input = InputState::new();
704 input.insert_str("a\rb\rc");
705 assert_eq!(input.lines, vec!["a", "b", "c"]);
707 }
708
709 #[test]
710 fn insert_str_empty() {
711 let mut input = InputState::new();
712 let v = input.version;
713 input.insert_str("");
714 assert_eq!(input.version, v); }
716
717 #[test]
720 fn backspace_mid_line() {
721 let mut input = InputState::new();
722 input.insert_str("abc");
723 input.delete_char_before();
724 assert_eq!(input.lines[0], "ab");
725 assert_eq!(input.cursor_col, 2);
726 }
727
728 #[test]
729 fn backspace_start_of_line_joins() {
730 let mut input = InputState::new();
731 input.insert_str("hello\nworld");
732 input.move_home();
734 input.delete_char_before();
735 assert_eq!(input.lines, vec!["helloworld"]);
736 assert_eq!(input.cursor_row, 0);
737 assert_eq!(input.cursor_col, 5); }
739
740 #[test]
741 fn backspace_start_of_buffer_noop() {
742 let mut input = InputState::new();
743 input.insert_str("hi");
744 input.move_home();
745 let v = input.version;
746 input.delete_char_before(); assert_eq!(input.lines[0], "hi");
748 assert_eq!(input.version, v); }
750
751 #[test]
752 fn backspace_unicode() {
753 let mut input = InputState::new();
754 input.insert_char('\u{1F600}');
755 input.insert_char('x');
756 input.delete_char_before();
757 assert_eq!(input.lines[0], "\u{1F600}");
758 }
759
760 #[test]
763 fn delete_mid_line() {
764 let mut input = InputState::new();
765 input.insert_str("abc");
766 input.move_home();
767 input.delete_char_after();
768 assert_eq!(input.lines[0], "bc");
769 assert_eq!(input.cursor_col, 0);
770 }
771
772 #[test]
773 fn delete_end_of_line_joins_next() {
774 let mut input = InputState::new();
775 input.insert_str("hello\nworld");
776 input.cursor_row = 0;
777 input.cursor_col = 5; input.delete_char_after();
779 assert_eq!(input.lines, vec!["helloworld"]);
780 }
781
782 #[test]
783 fn delete_end_of_buffer_noop() {
784 let mut input = InputState::new();
785 input.insert_str("hi");
786 let v = input.version;
788 input.delete_char_after();
789 assert_eq!(input.lines[0], "hi");
790 assert_eq!(input.version, v);
791 }
792
793 #[test]
796 fn move_left_within_line() {
797 let mut input = InputState::new();
798 input.insert_str("abc");
799 input.move_left();
800 assert_eq!(input.cursor_col, 2);
801 }
802
803 #[test]
804 fn move_left_wraps_to_previous_line() {
805 let mut input = InputState::new();
806 input.insert_str("ab\ncd");
807 input.move_home(); input.move_left();
809 assert_eq!(input.cursor_row, 0);
810 assert_eq!(input.cursor_col, 2); }
812
813 #[test]
814 fn move_left_at_origin_noop() {
815 let mut input = InputState::new();
816 input.insert_char('a');
817 input.move_home();
818 let v = input.version;
819 input.move_left();
820 assert_eq!(input.cursor_col, 0);
821 assert_eq!(input.cursor_row, 0);
822 assert_eq!(input.version, v); }
824
825 #[test]
826 fn move_right_within_line() {
827 let mut input = InputState::new();
828 input.insert_str("abc");
829 input.move_home();
830 input.move_right();
831 assert_eq!(input.cursor_col, 1);
832 }
833
834 #[test]
835 fn move_right_wraps_to_next_line() {
836 let mut input = InputState::new();
837 input.insert_str("ab\ncd");
838 input.cursor_row = 0;
839 input.cursor_col = 2; input.move_right();
841 assert_eq!(input.cursor_row, 1);
842 assert_eq!(input.cursor_col, 0);
843 }
844
845 #[test]
846 fn move_right_at_end_noop() {
847 let mut input = InputState::new();
848 input.insert_str("ab");
849 let v = input.version;
850 input.move_right(); assert_eq!(input.version, v);
852 }
853
854 #[test]
857 fn move_up_clamps_col() {
858 let mut input = InputState::new();
859 input.insert_str("ab\nhello");
860 input.move_up();
862 assert_eq!(input.cursor_row, 0);
863 assert_eq!(input.cursor_col, 2); }
865
866 #[test]
867 fn move_up_at_top_noop() {
868 let mut input = InputState::new();
869 input.insert_str("hello");
870 let v = input.version;
871 input.move_up();
872 assert_eq!(input.cursor_row, 0);
873 assert_eq!(input.version, v);
874 }
875
876 #[test]
877 fn move_down_clamps_col() {
878 let mut input = InputState::new();
879 input.insert_str("hello\nab");
880 input.cursor_row = 0;
881 input.cursor_col = 5;
882 input.move_down();
883 assert_eq!(input.cursor_row, 1);
884 assert_eq!(input.cursor_col, 2); }
886
887 #[test]
888 fn move_down_at_bottom_noop() {
889 let mut input = InputState::new();
890 input.insert_str("hello");
891 let v = input.version;
892 input.move_down();
893 assert_eq!(input.version, v);
894 }
895
896 #[test]
899 fn move_home_sets_col_zero() {
900 let mut input = InputState::new();
901 input.insert_str("hello");
902 input.move_home();
903 assert_eq!(input.cursor_col, 0);
904 }
905
906 #[test]
907 fn move_end_sets_col_to_line_len() {
908 let mut input = InputState::new();
909 input.insert_str("hello");
910 input.move_home();
911 input.move_end();
912 assert_eq!(input.cursor_col, 5);
913 }
914
915 #[test]
916 fn move_home_always_bumps_version() {
917 let mut input = InputState::new();
918 input.insert_str("hello");
919 input.move_home(); let v = input.version;
921 input.move_home(); assert!(input.version > v);
923 }
924
925 #[test]
928 fn line_count_single() {
929 assert_eq!(InputState::new().line_count(), 1);
930 }
931
932 #[test]
933 fn line_count_multi() {
934 let mut input = InputState::new();
935 input.insert_str("a\nb\nc");
936 assert_eq!(input.line_count(), 3);
937 }
938
939 #[test]
942 fn version_increments_on_every_mutation() {
943 let mut input = InputState::new();
944 let mut v = input.version;
945
946 input.insert_char('a');
947 assert!(input.version > v);
948 v = input.version;
949
950 input.insert_newline();
951 assert!(input.version > v);
952 v = input.version;
953
954 input.delete_char_before();
955 assert!(input.version > v);
956 v = input.version;
957
958 input.move_left();
959 assert!(input.version > v);
960 v = input.version;
961
962 input.clear();
963 assert!(input.version > v);
964 }
965
966 #[test]
967 fn rapid_insert_delete_cycle() {
968 let mut input = InputState::new();
969 for _ in 0..100 {
970 input.insert_char('x');
971 }
972 assert_eq!(input.lines[0].len(), 100);
973 for _ in 0..100 {
974 input.delete_char_before();
975 }
976 assert!(input.is_empty());
977 }
978
979 #[test]
980 fn mixed_unicode_operations() {
981 let mut input = InputState::new();
982 input.insert_str("hi\u{1F600}\u{4F60}");
984 assert_eq!(input.cursor_col, 4); input.move_home();
986 input.move_right(); input.move_right(); input.delete_char_after(); assert_eq!(input.lines[0], "hi\u{4F60}");
990 }
991
992 #[test]
993 fn multiline_editing_stress() {
994 let mut input = InputState::new();
995 for i in 0..10 {
997 input.insert_str(&format!("line{i}"));
998 if i < 9 {
999 input.insert_newline();
1000 }
1001 }
1002 assert_eq!(input.lines.len(), 10);
1003
1004 input.cursor_row = 5;
1006 input.cursor_col = 0;
1007 input.delete_char_before(); assert_eq!(input.lines.len(), 9);
1009
1010 let text = input.text();
1012 assert!(text.contains("line4line5"));
1013 }
1014
1015 #[test]
1016 fn insert_str_with_only_newlines() {
1017 let mut input = InputState::new();
1018 input.insert_str("\n\n\n");
1019 assert_eq!(input.lines, vec!["", "", "", ""]);
1020 assert_eq!(input.cursor_row, 3);
1021 assert_eq!(input.cursor_col, 0);
1022 }
1023
1024 #[test]
1025 fn cursor_clamping_on_vertical_nav() {
1026 let mut input = InputState::new();
1027 input.insert_str("long line here\nab\nmedium line");
1028 input.move_up(); assert_eq!(input.cursor_col, 2);
1031 input.move_up(); assert_eq!(input.cursor_col, 2);
1033 input.move_end(); input.move_down(); assert_eq!(input.cursor_col, 2);
1036 }
1037
1038 #[test]
1041 fn insert_tab_character() {
1042 let mut input = InputState::new();
1043 input.insert_char('\t');
1044 assert_eq!(input.lines[0], "\t");
1045 assert_eq!(input.cursor_col, 1);
1046 }
1047
1048 #[test]
1049 fn insert_null_byte() {
1050 let mut input = InputState::new();
1051 input.insert_char('\0');
1052 assert_eq!(input.lines[0].len(), 1);
1053 assert_eq!(input.cursor_col, 1);
1054 }
1055
1056 #[test]
1057 fn insert_control_chars() {
1058 let mut input = InputState::new();
1059 input.insert_char('\x07');
1061 input.insert_char('\x08');
1062 input.insert_char('\x1B');
1063 assert_eq!(input.cursor_col, 3);
1064 assert_eq!(input.lines[0].chars().count(), 3);
1065 }
1066
1067 #[test]
1068 fn windows_crlf_line_endings() {
1069 let mut input = InputState::new();
1071 input.insert_str("a\r\nb");
1072 assert_eq!(input.lines, vec!["a", "", "b"]);
1074 }
1075
1076 #[test]
1077 fn insert_zero_width_joiner_sequence() {
1078 let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
1080 let mut input = InputState::new();
1081 input.insert_str(family);
1082 assert_eq!(input.cursor_col, family.chars().count());
1084 assert_eq!(input.text(), family);
1085 }
1086
1087 #[test]
1088 fn insert_flag_emoji() {
1089 let flag = "\u{1F1FA}\u{1F1F8}";
1091 let mut input = InputState::new();
1092 input.insert_str(flag);
1093 assert_eq!(input.cursor_col, 2); assert_eq!(input.text(), flag);
1095 }
1096
1097 #[test]
1098 fn insert_combining_diacritical_marks() {
1099 let mut input = InputState::new();
1101 input.insert_char('e');
1102 input.insert_char('\u{0301}'); input.insert_char('\u{0327}'); assert_eq!(input.cursor_col, 3);
1105 input.delete_char_before();
1107 assert_eq!(input.cursor_col, 2);
1108 assert_eq!(input.lines[0], "e\u{0301}");
1109 }
1110
1111 #[test]
1112 fn insert_right_to_left_text() {
1113 let arabic = "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}";
1115 let mut input = InputState::new();
1116 input.insert_str(arabic);
1117 assert_eq!(input.cursor_col, 5);
1118 assert_eq!(input.text(), arabic);
1119 input.move_home();
1121 input.delete_char_after();
1122 assert_eq!(input.cursor_col, 0);
1123 assert_eq!(input.lines[0].chars().count(), 4);
1124 }
1125
1126 #[test]
1127 fn insert_very_long_single_line() {
1128 let mut input = InputState::new();
1129 let long_str: String = "x".repeat(10_000);
1130 input.insert_str(&long_str);
1131 assert_eq!(input.cursor_col, 10_000);
1132 assert_eq!(input.lines[0].len(), 10_000);
1133 input.move_home();
1135 for _ in 0..5000 {
1136 input.move_right();
1137 }
1138 assert_eq!(input.cursor_col, 5000);
1139 input.insert_char('Y');
1141 assert_eq!(input.lines[0].len(), 10_001);
1142 }
1143
1144 #[test]
1145 fn insert_many_short_lines() {
1146 let mut input = InputState::new();
1147 for i in 0..500 {
1148 input.insert_str(&format!("{i}"));
1149 input.insert_newline();
1150 }
1151 assert_eq!(input.lines.len(), 501); assert_eq!(input.cursor_row, 500);
1153 }
1154
1155 #[test]
1158 fn type_then_backspace_all_then_retype() {
1159 let mut input = InputState::new();
1160 input.insert_str("hello world");
1161 for _ in 0..11 {
1163 input.delete_char_before();
1164 }
1165 assert!(input.is_empty());
1166 assert_eq!(input.cursor_col, 0);
1167 input.insert_str("new text");
1169 assert_eq!(input.text(), "new text");
1170 }
1171
1172 #[test]
1173 fn alternating_insert_and_navigate() {
1174 let mut input = InputState::new();
1175 input.insert_char('a');
1177 input.move_left();
1178 input.insert_char('b');
1179 input.move_left();
1180 input.insert_char('c');
1181 assert_eq!(input.lines[0], "cba");
1182 assert_eq!(input.cursor_col, 1); }
1184
1185 #[test]
1186 fn home_end_rapid_cycle() {
1187 let mut input = InputState::new();
1188 input.insert_str("hello");
1189 for _ in 0..50 {
1190 input.move_home();
1191 assert_eq!(input.cursor_col, 0);
1192 input.move_end();
1193 assert_eq!(input.cursor_col, 5);
1194 }
1195 }
1196
1197 #[test]
1198 fn left_right_round_trip_preserves_position() {
1199 let mut input = InputState::new();
1200 input.insert_str("abcdef");
1201 input.move_home();
1202 input.move_right();
1203 input.move_right();
1204 input.move_right(); let col = input.cursor_col;
1206 input.move_left();
1208 input.move_left();
1209 input.move_right();
1210 input.move_right();
1211 assert_eq!(input.cursor_col, col);
1212 }
1213
1214 #[test]
1215 fn up_down_round_trip_with_short_line() {
1216 let mut input = InputState::new();
1217 input.insert_str("longline\na\nlongline");
1218 input.cursor_row = 0;
1219 input.cursor_col = 7; input.move_down(); assert_eq!(input.cursor_col, 1);
1222 input.move_down(); assert_eq!(input.cursor_col, 1);
1224 }
1225
1226 #[test]
1227 fn newline_then_immediate_backspace() {
1228 let mut input = InputState::new();
1229 input.insert_str("hello");
1230 input.insert_newline();
1231 assert_eq!(input.lines.len(), 2);
1232 input.delete_char_before(); assert_eq!(input.lines.len(), 1);
1234 assert_eq!(input.lines[0], "hello");
1235 assert_eq!(input.cursor_col, 5);
1236 }
1237
1238 #[test]
1239 fn delete_forward_through_multiple_line_joins() {
1240 let mut input = InputState::new();
1241 input.insert_str("a\nb\nc\nd");
1242 assert_eq!(input.lines.len(), 4);
1243 input.cursor_row = 0;
1245 input.cursor_col = 0;
1246 input.move_right(); input.delete_char_after(); assert_eq!(input.lines[0], "ab");
1250 input.move_right(); input.delete_char_after(); assert_eq!(input.lines[0], "abc");
1253 input.move_right(); input.delete_char_after(); assert_eq!(input.lines, vec!["abcd"]);
1256 }
1257
1258 #[test]
1259 fn backspace_collapses_all_lines_to_one() {
1260 let mut input = InputState::new();
1261 input.insert_str("a\nb\nc\nd\ne");
1262 assert_eq!(input.lines.len(), 5);
1263 let total_chars = input.text().len(); for _ in 0..total_chars {
1266 input.delete_char_before();
1267 }
1268 assert!(input.is_empty());
1269 assert_eq!(input.lines.len(), 1);
1270 assert_eq!(input.cursor_row, 0);
1271 assert_eq!(input.cursor_col, 0);
1272 }
1273
1274 #[test]
1277 fn type_on_multiple_lines_then_clear() {
1278 let mut input = InputState::new();
1279 input.insert_str("line1\nline2\nline3");
1280 input.move_up();
1281 input.move_home();
1282 input.insert_str("prefix_");
1283 assert_eq!(input.lines[1], "prefix_line2");
1284 input.clear();
1285 assert!(input.is_empty());
1286 assert_eq!(input.cursor_row, 0);
1287 }
1288
1289 #[test]
1290 fn insert_between_emoji() {
1291 let mut input = InputState::new();
1292 input.insert_char('\u{1F600}');
1293 input.insert_char('\u{1F601}');
1294 input.move_left(); input.insert_char('X');
1297 assert_eq!(input.lines[0], "\u{1F600}X\u{1F601}");
1298 assert_eq!(input.cursor_col, 2);
1299 }
1300
1301 #[test]
1302 fn delete_char_after_on_multibyte_boundary() {
1303 let mut input = InputState::new();
1304 input.insert_str("\u{1F600}\u{1F601}\u{1F602}");
1305 input.move_home();
1306 input.move_right(); input.delete_char_after(); assert_eq!(input.lines[0], "\u{1F600}\u{1F602}");
1309 }
1310
1311 #[test]
1312 fn text_consistent_after_every_operation() {
1313 let mut input = InputState::new();
1314
1315 input.insert_str("hello");
1316 assert_eq!(input.text(), "hello");
1317
1318 input.insert_newline();
1319 assert_eq!(input.text(), "hello\n");
1320
1321 input.insert_str("world");
1322 assert_eq!(input.text(), "hello\nworld");
1323
1324 input.move_up();
1325 input.move_end();
1326 input.insert_char('!');
1327 assert_eq!(input.text(), "hello!\nworld");
1328
1329 input.delete_char_before();
1330 assert_eq!(input.text(), "hello\nworld");
1331
1332 input.move_down();
1333 input.move_home();
1334 input.delete_char_before(); assert_eq!(input.text(), "helloworld");
1336
1337 input.clear();
1338 assert_eq!(input.text(), "");
1339 }
1340
1341 #[test]
1342 fn navigate_through_empty_lines() {
1343 let mut input = InputState::new();
1344 input.insert_str("\n\n\n");
1345 assert_eq!(input.cursor_row, 3);
1347 input.move_up();
1348 assert_eq!(input.cursor_row, 2);
1349 assert_eq!(input.cursor_col, 0);
1350 input.move_up();
1351 input.move_up();
1352 assert_eq!(input.cursor_row, 0);
1353 input.insert_char('x');
1355 assert_eq!(input.lines[0], "x");
1356 assert_eq!(input.lines.len(), 4);
1357 }
1358
1359 #[test]
1360 fn insert_str_into_middle_of_existing_content() {
1361 let mut input = InputState::new();
1362 input.insert_str("hd");
1363 input.move_left(); input.insert_str("ello worl");
1365 assert_eq!(input.lines[0], "hello world");
1366 }
1367
1368 #[test]
1369 fn multiline_paste_into_middle_of_line() {
1370 let mut input = InputState::new();
1371 input.insert_str("start end");
1372 input.move_home();
1374 for _ in 0..6 {
1375 input.move_right();
1376 }
1377 input.insert_str("line1\nline2\nline3 ");
1378 assert_eq!(input.lines[0], "start line1");
1379 assert_eq!(input.lines[1], "line2");
1380 assert_eq!(input.lines[2], "line3 end");
1381 assert_eq!(input.cursor_row, 2);
1382 }
1383
1384 #[test]
1385 fn version_never_wraps_in_reasonable_use() {
1386 let mut input = InputState::new();
1387 for _ in 0..500 {
1389 input.insert_char('a');
1390 input.delete_char_before();
1391 }
1392 assert_eq!(input.version, 1000);
1393 }
1394
1395 #[test]
1396 fn mixed_cr_and_lf_in_paste() {
1397 let mut input = InputState::new();
1398 input.insert_str("a\rb\nc\r\nd");
1400 assert_eq!(input.lines[0], "a");
1406 assert_eq!(input.lines.last().unwrap(), "d");
1407 assert!(input.text().contains('d'));
1409 }
1410
1411 #[test]
1412 fn parse_placeholder_with_trailing_suffix_text() {
1413 let line = "[Pasted Text 2 - 42 lines]tail";
1414 let parsed = parse_paste_placeholder_with_suffix(line).unwrap();
1415 assert_eq!(parsed.0, 1);
1416 assert_eq!(&line[..parsed.1], "[Pasted Text 2 - 42 lines]");
1417 }
1418
1419 #[test]
1420 fn text_expands_placeholder_even_with_trailing_text() {
1421 let mut input = InputState::new();
1422 input.insert_paste_block("line1\nline2");
1423 input.lines[0].push_str(" + extra");
1424 input.cursor_col = input.lines[0].chars().count();
1425 assert_eq!(input.text(), "line1\nline2 + extra");
1426 }
1427
1428 #[test]
1429 fn append_to_active_paste_block_merges_chunks_and_updates_label() {
1430 let mut input = InputState::new();
1431 let original = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk";
1432 input.insert_paste_block(original);
1433 assert!(input.append_to_active_paste_block("\nl\nm"));
1434 assert_eq!(input.lines[0], "[Pasted Text 1 - 13 lines]");
1435 assert_eq!(input.text(), "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm");
1436 }
1437
1438 #[test]
1439 fn append_to_active_paste_block_rejects_dirty_placeholder_line() {
1440 let mut input = InputState::new();
1441 input.insert_paste_block("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk");
1442 input.lines[0].push_str("tail");
1443 input.cursor_col = input.lines[0].chars().count();
1444 assert!(!input.append_to_active_paste_block("x"));
1445 }
1446
1447 #[test]
1448 fn count_text_lines_handles_mixed_line_endings() {
1449 assert_eq!(count_text_lines("a\r\nb\nc\rd"), 4);
1450 assert_eq!(count_text_lines("single"), 1);
1451 assert_eq!(count_text_lines("x\r\n"), 2);
1452 }
1453
1454 #[test]
1455 fn trim_trailing_line_breaks_handles_crlf_and_lf() {
1456 assert_eq!(trim_trailing_line_breaks("a\r\n\r\n"), "a");
1457 assert_eq!(trim_trailing_line_breaks("a\n\n"), "a");
1458 assert_eq!(trim_trailing_line_breaks("a\r\r"), "a");
1459 assert_eq!(trim_trailing_line_breaks("a"), "a");
1460 }
1461}