1use crate::cursor::{Cursor, Mode as CursorMode, blink_cmd};
19use crate::key::{Binding, matches};
20use crate::runeutil::Sanitizer;
21use crate::viewport::Viewport;
22use bubbletea::{Cmd, KeyMsg, Message, Model};
23use lipgloss::Style;
24use unicode_width::UnicodeWidthStr;
25
26const MIN_HEIGHT: usize = 1;
27const DEFAULT_HEIGHT: usize = 6;
28const DEFAULT_WIDTH: usize = 40;
29const DEFAULT_MAX_HEIGHT: usize = 99;
30const DEFAULT_MAX_WIDTH: usize = 500;
31const MAX_LINES: usize = 10000;
32
33#[derive(Debug, Clone)]
35pub struct KeyMap {
36 pub character_forward: Binding,
38 pub character_backward: Binding,
40 pub delete_after_cursor: Binding,
42 pub delete_before_cursor: Binding,
44 pub delete_character_backward: Binding,
46 pub delete_character_forward: Binding,
48 pub delete_word_backward: Binding,
50 pub delete_word_forward: Binding,
52 pub insert_newline: Binding,
54 pub line_end: Binding,
56 pub line_next: Binding,
58 pub line_previous: Binding,
60 pub line_start: Binding,
62 pub paste: Binding,
64 pub word_backward: Binding,
66 pub word_forward: Binding,
68 pub input_begin: Binding,
70 pub input_end: Binding,
72 pub uppercase_word_forward: Binding,
74 pub lowercase_word_forward: Binding,
76 pub capitalize_word_forward: Binding,
78 pub transpose_character_backward: Binding,
80}
81
82impl Default for KeyMap {
83 fn default() -> Self {
84 Self {
85 character_forward: Binding::new()
86 .keys(&["right", "ctrl+f"])
87 .help("right", "character forward"),
88 character_backward: Binding::new()
89 .keys(&["left", "ctrl+b"])
90 .help("left", "character backward"),
91 word_forward: Binding::new()
92 .keys(&["alt+right", "alt+f"])
93 .help("alt+right", "word forward"),
94 word_backward: Binding::new()
95 .keys(&["alt+left", "alt+b"])
96 .help("alt+left", "word backward"),
97 line_next: Binding::new()
98 .keys(&["down", "ctrl+n"])
99 .help("down", "next line"),
100 line_previous: Binding::new()
101 .keys(&["up", "ctrl+p"])
102 .help("up", "previous line"),
103 delete_word_backward: Binding::new()
104 .keys(&["alt+backspace", "ctrl+w"])
105 .help("alt+backspace", "delete word backward"),
106 delete_word_forward: Binding::new()
107 .keys(&["alt+delete", "alt+d"])
108 .help("alt+delete", "delete word forward"),
109 delete_after_cursor: Binding::new()
110 .keys(&["ctrl+k"])
111 .help("ctrl+k", "delete after cursor"),
112 delete_before_cursor: Binding::new()
113 .keys(&["ctrl+u"])
114 .help("ctrl+u", "delete before cursor"),
115 insert_newline: Binding::new()
116 .keys(&["enter", "ctrl+m"])
117 .help("enter", "insert newline"),
118 delete_character_backward: Binding::new()
119 .keys(&["backspace", "ctrl+h"])
120 .help("backspace", "delete character backward"),
121 delete_character_forward: Binding::new()
122 .keys(&["delete", "ctrl+d"])
123 .help("delete", "delete character forward"),
124 line_start: Binding::new()
125 .keys(&["home", "ctrl+a"])
126 .help("home", "line start"),
127 line_end: Binding::new()
128 .keys(&["end", "ctrl+e"])
129 .help("end", "line end"),
130 paste: Binding::new().keys(&["ctrl+v"]).help("ctrl+v", "paste"),
131 input_begin: Binding::new()
132 .keys(&["alt+<", "ctrl+home"])
133 .help("alt+<", "input begin"),
134 input_end: Binding::new()
135 .keys(&["alt+>", "ctrl+end"])
136 .help("alt+>", "input end"),
137 capitalize_word_forward: Binding::new()
138 .keys(&["alt+c"])
139 .help("alt+c", "capitalize word forward"),
140 lowercase_word_forward: Binding::new()
141 .keys(&["alt+l"])
142 .help("alt+l", "lowercase word forward"),
143 uppercase_word_forward: Binding::new()
144 .keys(&["alt+u"])
145 .help("alt+u", "uppercase word forward"),
146 transpose_character_backward: Binding::new()
147 .keys(&["ctrl+t"])
148 .help("ctrl+t", "transpose character backward"),
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct Styles {
156 pub base: Style,
158 pub cursor_line: Style,
160 pub cursor_line_number: Style,
162 pub end_of_buffer: Style,
164 pub line_number: Style,
166 pub placeholder: Style,
168 pub prompt: Style,
170 pub text: Style,
172}
173
174impl Default for Styles {
175 fn default() -> Self {
176 Self {
177 base: Style::new(),
178 cursor_line: Style::new(),
179 cursor_line_number: Style::new(),
181 end_of_buffer: Style::new(),
182 line_number: Style::new(),
183 placeholder: Style::new(),
184 prompt: Style::new(),
185 text: Style::new(),
186 }
187 }
188}
189
190#[derive(Debug, Clone)]
192pub struct PasteMsg(pub String);
193
194#[derive(Debug, Clone)]
196pub struct PasteErrMsg(pub String);
197
198#[derive(Debug, Clone)]
200pub struct TextArea {
201 pub err: Option<String>,
203 pub prompt: String,
205 pub placeholder: String,
207 pub show_line_numbers: bool,
209 pub end_of_buffer_character: char,
211 pub key_map: KeyMap,
213 pub focused_style: Styles,
215 pub blurred_style: Styles,
217 pub cursor: Cursor,
219 pub char_limit: usize,
221 pub max_height: usize,
223 pub max_width: usize,
225 use_focused_style: bool,
227 prompt_width: usize,
229 width: usize,
231 height: usize,
233 value: Vec<Vec<char>>,
235 focus: bool,
237 col: usize,
239 row: usize,
241 last_char_offset: usize,
243 viewport: Viewport,
245 sanitizer: Sanitizer,
247}
248
249impl Default for TextArea {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255impl TextArea {
256 #[must_use]
258 pub fn new() -> Self {
259 let viewport = Viewport::new(0, 0);
260
261 let mut ta = Self {
262 err: None,
263 prompt: "β ".to_string(),
264 placeholder: String::new(),
265 show_line_numbers: true,
266 end_of_buffer_character: ' ',
267 key_map: KeyMap::default(),
268 focused_style: Styles::default(),
269 blurred_style: Styles::default(),
270 use_focused_style: false,
271 cursor: Cursor::new(),
272 char_limit: 0,
273 max_height: DEFAULT_MAX_HEIGHT,
274 max_width: DEFAULT_MAX_WIDTH,
275 prompt_width: 2, width: DEFAULT_WIDTH,
277 height: DEFAULT_HEIGHT,
278 value: vec![Vec::new()],
279 focus: false,
280 col: 0,
281 row: 0,
282 last_char_offset: 0,
283 viewport,
284 sanitizer: Sanitizer::new(),
285 };
286
287 ta.set_height(DEFAULT_HEIGHT);
288 ta.set_width(DEFAULT_WIDTH);
289 ta
290 }
291
292 pub fn set_value(&mut self, s: &str) {
294 self.reset();
295 self.insert_string(s);
296 }
297
298 pub fn insert_string(&mut self, s: &str) {
300 self.insert_runes_from_user_input(&s.chars().collect::<Vec<_>>());
301 }
302
303 pub fn insert_rune(&mut self, r: char) {
305 self.insert_runes_from_user_input(&[r]);
306 }
307
308 fn insert_runes_from_user_input(&mut self, runes: &[char]) {
309 let runes = self.sanitizer.sanitize(runes);
310
311 let runes = if self.char_limit > 0 {
312 let current_len = self.length();
313 let avail = self.char_limit.saturating_sub(current_len);
314 if avail == 0 {
315 return;
316 }
317 if runes.len() > avail {
318 runes[..avail].to_vec()
319 } else {
320 runes
321 }
322 } else {
323 runes
324 };
325
326 let mut lines: Vec<Vec<char>> = Vec::new();
328 let mut current_line = Vec::new();
329
330 for c in &runes {
331 if *c == '\n' {
332 lines.push(current_line);
333 current_line = Vec::new();
334 } else {
335 current_line.push(*c);
336 }
337 }
338 lines.push(current_line);
339
340 if MAX_LINES > 0 && self.value.len() + lines.len() - 1 > MAX_LINES {
342 let allowed = MAX_LINES.saturating_sub(self.value.len()) + 1;
343 lines.truncate(allowed);
344 }
345
346 if lines.is_empty() {
347 return;
348 }
349
350 let tail: Vec<char> = self.value[self.row][self.col..].to_vec();
352
353 self.value[self.row].truncate(self.col);
355 self.value[self.row].extend_from_slice(&lines[0]);
356 self.col += lines[0].len();
357
358 if lines.len() > 1 {
360 for line in lines.into_iter().skip(1) {
362 self.row += 1;
363 self.value.insert(self.row, line.clone());
364 self.col = line.len();
365 }
366 }
367
368 self.value[self.row].extend_from_slice(&tail);
370 self.set_cursor_col(self.col);
371 }
372
373 #[must_use]
375 pub fn value(&self) -> String {
376 self.value
377 .iter()
378 .map(|line| line.iter().collect::<String>())
379 .collect::<Vec<_>>()
380 .join("\n")
381 }
382
383 #[must_use]
385 pub fn length(&self) -> usize {
386 let char_count: usize = self.value.iter().map(|line| line.len()).sum();
387 char_count + self.value.len().saturating_sub(1)
389 }
390
391 #[must_use]
393 pub fn line_count(&self) -> usize {
394 self.value.len()
395 }
396
397 #[must_use]
399 pub fn line(&self) -> usize {
400 self.row
401 }
402
403 #[must_use]
405 pub fn cursor_col(&self) -> usize {
406 self.col
407 }
408
409 #[must_use]
411 pub fn cursor_pos(&self) -> (usize, usize) {
412 (self.row, self.col)
413 }
414
415 #[must_use]
417 pub fn cursor_byte_offset(&self) -> usize {
418 if self.value.is_empty() {
419 return 0;
420 }
421
422 let row = self.row.min(self.value.len().saturating_sub(1));
423 let col = self.col.min(self.value[row].len());
424
425 let mut offset = 0usize;
426
427 for line in &self.value[..row] {
428 offset = offset.saturating_add(line.iter().map(|c| c.len_utf8()).sum::<usize>());
429 offset = offset.saturating_add(1); }
431
432 offset.saturating_add(
433 self.value[row][..col]
434 .iter()
435 .map(|c| c.len_utf8())
436 .sum::<usize>(),
437 )
438 }
439
440 pub fn set_cursor_byte_offset(&mut self, offset: usize) {
445 if self.value.is_empty() {
446 self.value = vec![Vec::new()];
447 }
448
449 fn col_for_byte_offset(line: &[char], byte_offset: usize) -> usize {
450 let mut col = 0usize;
451 let mut used = 0usize;
452
453 for c in line {
454 let len = c.len_utf8();
455 if used.saturating_add(len) > byte_offset {
456 break;
457 }
458 used = used.saturating_add(len);
459 col = col.saturating_add(1);
460 }
461
462 col
463 }
464
465 let mut remaining = offset;
466
467 for (idx, line) in self.value.iter().enumerate() {
468 let line_bytes = line.iter().map(|c| c.len_utf8()).sum::<usize>();
469
470 if remaining <= line_bytes {
471 self.row = idx;
472 let col = col_for_byte_offset(line, remaining);
473 self.set_cursor_col(col);
474 return;
475 }
476
477 remaining = remaining.saturating_sub(line_bytes);
478
479 if idx + 1 < self.value.len() {
480 if remaining == 0 {
482 self.row = idx;
483 self.set_cursor_col(line.len());
484 return;
485 }
486
487 remaining = remaining.saturating_sub(1);
488 if remaining == 0 {
489 self.row = idx + 1;
490 self.set_cursor_col(0);
491 return;
492 }
493 }
494 }
495
496 self.row = self.value.len().saturating_sub(1);
498 let last_len = self.value[self.row].len();
499 self.set_cursor_col(last_len);
500 }
501
502 pub fn cursor_down(&mut self) {
504 if self.row < self.value.len() - 1 {
505 self.row += 1;
506 self.col = self.col.min(self.value[self.row].len());
507 }
508 }
509
510 pub fn cursor_up(&mut self) {
512 if self.row > 0 {
513 self.row -= 1;
514 self.col = self.col.min(self.value[self.row].len());
515 }
516 }
517
518 pub fn set_cursor_col(&mut self, col: usize) {
520 self.col = col.min(self.value[self.row].len());
521 self.last_char_offset = 0;
522 }
523
524 pub fn cursor_start(&mut self) {
526 self.set_cursor_col(0);
527 }
528
529 pub fn cursor_end(&mut self) {
531 self.set_cursor_col(self.value[self.row].len());
532 }
533
534 pub fn cursor_left(&mut self) {
536 self.character_left(false);
537 }
538
539 pub fn cursor_right(&mut self) {
541 self.character_right();
542 }
543
544 #[must_use]
546 pub fn focused(&self) -> bool {
547 self.focus
548 }
549
550 pub fn focus(&mut self) -> Option<Cmd> {
552 self.focus = true;
553 self.use_focused_style = true;
554 self.cursor.focus()
555 }
556
557 pub fn blur(&mut self) {
559 self.focus = false;
560 self.use_focused_style = false;
561 self.cursor.blur();
562 }
563
564 pub fn reset(&mut self) {
566 self.value = vec![Vec::new()];
567 self.col = 0;
568 self.row = 0;
569 self.viewport.goto_top();
570 self.set_cursor_col(0);
571 }
572
573 fn current_style(&self) -> &Styles {
574 if self.use_focused_style {
575 &self.focused_style
576 } else {
577 &self.blurred_style
578 }
579 }
580
581 fn delete_before_cursor(&mut self) {
582 self.value[self.row] = self.value[self.row][self.col..].to_vec();
583 self.set_cursor_col(0);
584 }
585
586 fn delete_after_cursor(&mut self) {
587 self.value[self.row].truncate(self.col);
588 self.set_cursor_col(self.value[self.row].len());
589 }
590
591 fn transpose_left(&mut self) {
592 let len = self.value[self.row].len();
593 if self.col == 0 || len < 2 {
594 return;
595 }
596 if self.col >= len {
598 self.set_cursor_col(len - 1);
599 }
600 self.value[self.row].swap(self.col - 1, self.col);
601 if self.col < self.value[self.row].len() {
602 self.set_cursor_col(self.col + 1);
603 }
604 }
605
606 fn delete_word_left(&mut self) {
607 if self.col == 0 || self.value[self.row].is_empty() {
608 return;
609 }
610
611 let old_col = self.col;
612 self.set_cursor_col(self.col.saturating_sub(1));
613
614 while self.col > 0
616 && self.value[self.row]
617 .get(self.col)
618 .is_some_and(|c| c.is_whitespace())
619 {
620 self.set_cursor_col(self.col.saturating_sub(1));
621 }
622
623 while self.col > 0 {
625 if !self.value[self.row]
626 .get(self.col)
627 .is_some_and(|c| c.is_whitespace())
628 {
629 self.set_cursor_col(self.col.saturating_sub(1));
630 } else {
631 if self.col > 0 {
632 self.set_cursor_col(self.col + 1);
633 }
634 break;
635 }
636 }
637
638 let mut new_line = self.value[self.row][..self.col].to_vec();
639 if old_col <= self.value[self.row].len() {
640 new_line.extend_from_slice(&self.value[self.row][old_col..]);
641 }
642 self.value[self.row] = new_line;
643 }
644
645 fn delete_word_right(&mut self) {
646 if self.col >= self.value[self.row].len() || self.value[self.row].is_empty() {
647 return;
648 }
649
650 let old_col = self.col;
651
652 while self.col < self.value[self.row].len()
654 && self.value[self.row]
655 .get(self.col)
656 .is_some_and(|c| c.is_whitespace())
657 {
658 self.set_cursor_col(self.col + 1);
659 }
660
661 while self.col < self.value[self.row].len() {
663 if !self.value[self.row]
664 .get(self.col)
665 .is_some_and(|c| c.is_whitespace())
666 {
667 self.set_cursor_col(self.col + 1);
668 } else {
669 break;
670 }
671 }
672
673 let mut new_line = self.value[self.row][..old_col].to_vec();
674 if self.col <= self.value[self.row].len() {
675 new_line.extend_from_slice(&self.value[self.row][self.col..]);
676 }
677 self.value[self.row] = new_line;
678 self.set_cursor_col(old_col);
679 }
680
681 fn character_right(&mut self) {
682 if self.col < self.value[self.row].len() {
683 self.set_cursor_col(self.col + 1);
684 } else if self.row < self.value.len() - 1 {
685 self.row += 1;
686 self.cursor_start();
687 }
688 }
689
690 fn character_left(&mut self, inside_line: bool) {
691 if self.col == 0 && self.row > 0 {
692 self.row -= 1;
693 self.cursor_end();
694 if !inside_line {
695 return;
696 }
697 }
698 if self.col > 0 {
699 self.set_cursor_col(self.col - 1);
700 }
701 }
702
703 fn word_left(&mut self) {
704 loop {
705 self.character_left(true);
706 if self.col < self.value[self.row].len()
707 && !self.value[self.row]
708 .get(self.col)
709 .is_some_and(|c| c.is_whitespace())
710 {
711 break;
712 }
713 if self.col == 0 && self.row == 0 {
714 break;
715 }
716 }
717
718 while self.col > 0 {
719 if self.value[self.row]
720 .get(self.col - 1)
721 .is_some_and(|c| c.is_whitespace())
722 {
723 break;
724 }
725 self.set_cursor_col(self.col - 1);
726 }
727 }
728
729 fn word_right(&mut self) {
730 while self.col >= self.value[self.row].len()
732 || self.value[self.row]
733 .get(self.col)
734 .is_some_and(|c| c.is_whitespace())
735 {
736 if self.row == self.value.len() - 1 && self.col == self.value[self.row].len() {
737 break;
738 }
739 self.character_right();
740 }
741
742 while self.col < self.value[self.row].len() {
744 if self.value[self.row]
745 .get(self.col)
746 .is_some_and(|c| c.is_whitespace())
747 {
748 break;
749 }
750 self.set_cursor_col(self.col + 1);
751 }
752 }
753
754 fn uppercase_right(&mut self) {
755 self.do_word_right(|line, i| {
756 line[i] = line[i].to_uppercase().next().unwrap_or(line[i]);
757 });
758 }
759
760 fn lowercase_right(&mut self) {
761 self.do_word_right(|line, i| {
762 line[i] = line[i].to_lowercase().next().unwrap_or(line[i]);
763 });
764 }
765
766 fn capitalize_right(&mut self) {
767 let mut char_idx = 0;
768 self.do_word_right(|line, i| {
769 if char_idx == 0 {
770 line[i] = line[i].to_uppercase().next().unwrap_or(line[i]);
771 }
772 char_idx += 1;
773 });
774 }
775
776 fn do_word_right<F>(&mut self, mut f: F)
777 where
778 F: FnMut(&mut Vec<char>, usize),
779 {
780 while self.col >= self.value[self.row].len()
782 || self.value[self.row]
783 .get(self.col)
784 .is_some_and(|c| c.is_whitespace())
785 {
786 if self.row == self.value.len() - 1 && self.col == self.value[self.row].len() {
787 break;
788 }
789 self.character_right();
790 }
791
792 while self.col < self.value[self.row].len() {
793 if self.value[self.row]
794 .get(self.col)
795 .is_some_and(|c| c.is_whitespace())
796 {
797 break;
798 }
799 f(&mut self.value[self.row], self.col);
800 self.set_cursor_col(self.col + 1);
801 }
802 }
803
804 fn move_to_begin(&mut self) {
805 self.row = 0;
806 self.set_cursor_col(0);
807 }
808
809 fn move_to_end(&mut self) {
810 self.row = self.value.len().saturating_sub(1);
811 self.set_cursor_col(self.value[self.row].len());
812 }
813
814 pub fn set_width(&mut self, w: usize) {
816 self.prompt_width = UnicodeWidthStr::width(self.prompt.as_str());
817
818 let reserved_outer = 0; let mut reserved_inner = self.prompt_width;
820
821 if self.show_line_numbers {
822 reserved_inner += 4; }
824
825 let min_width = reserved_inner + reserved_outer + 1;
826 let mut input_width = w.max(min_width);
827
828 if self.max_width > 0 {
829 input_width = input_width.min(self.max_width);
830 }
831
832 self.viewport.width = input_width.saturating_sub(reserved_outer);
833 self.width = input_width
834 .saturating_sub(reserved_outer)
835 .saturating_sub(reserved_inner);
836 }
837
838 #[must_use]
840 pub fn width(&self) -> usize {
841 self.width
842 }
843
844 pub fn set_height(&mut self, h: usize) {
846 if self.max_height > 0 {
847 self.height = h.clamp(MIN_HEIGHT, self.max_height);
848 self.viewport.height = h.clamp(MIN_HEIGHT, self.max_height);
849 } else {
850 self.height = h.max(MIN_HEIGHT);
851 self.viewport.height = h.max(MIN_HEIGHT);
852 }
853 }
854
855 #[must_use]
857 pub fn height(&self) -> usize {
858 self.height
859 }
860
861 fn merge_line_below(&mut self, row: usize) {
862 if row >= self.value.len() - 1 {
863 return;
864 }
865
866 let below = self.value.remove(row + 1);
867 self.value[row].extend(below);
868 }
869
870 fn merge_line_above(&mut self, row: usize) {
871 if row == 0 {
872 return;
873 }
874
875 self.col = self.value[row - 1].len();
876 let current = self.value.remove(row);
877 self.value[row - 1].extend(current);
878 self.row -= 1;
879 }
880
881 fn split_line(&mut self, row: usize, col: usize) {
882 let tail = self.value[row][col..].to_vec();
883 self.value[row].truncate(col);
884 self.value.insert(row + 1, tail);
885 self.col = 0;
886 self.row += 1;
887 }
888
889 fn reposition_view(&mut self) {
890 let minimum = self.viewport.y_offset();
891 let maximum = minimum + self.viewport.height.saturating_sub(1);
892
893 if self.row < minimum {
894 self.viewport.scroll_up(minimum - self.row);
895 } else if self.row > maximum {
896 self.viewport.scroll_down(self.row - maximum);
897 }
898 }
899
900 pub fn update(&mut self, msg: Message) -> Option<Cmd> {
902 if !self.focus {
903 self.cursor.blur();
904 return None;
905 }
906
907 let old_row = self.row;
908 let old_col = self.col;
909
910 if let Some(paste) = msg.downcast_ref::<PasteMsg>() {
912 self.insert_runes_from_user_input(&paste.0.chars().collect::<Vec<_>>());
913 }
914
915 if let Some(paste_err) = msg.downcast_ref::<PasteErrMsg>() {
916 self.err = Some(paste_err.0.clone());
917 }
918
919 if let Some(key) = msg.downcast_ref::<KeyMsg>() {
920 let key_str = key.to_string();
921
922 if matches(&key_str, &[&self.key_map.delete_after_cursor]) {
923 self.col = self.col.min(self.value[self.row].len());
924 if self.col >= self.value[self.row].len() {
925 self.merge_line_below(self.row);
926 } else {
927 self.delete_after_cursor();
928 }
929 } else if matches(&key_str, &[&self.key_map.delete_before_cursor]) {
930 self.col = self.col.min(self.value[self.row].len());
931 if self.col == 0 {
932 self.merge_line_above(self.row);
933 } else {
934 self.delete_before_cursor();
935 }
936 } else if matches(&key_str, &[&self.key_map.delete_character_backward]) {
937 self.col = self.col.min(self.value[self.row].len());
938 if self.col == 0 {
939 self.merge_line_above(self.row);
940 } else if !self.value[self.row].is_empty() {
941 self.value[self.row].remove(self.col - 1);
942 self.set_cursor_col(self.col.saturating_sub(1));
943 }
944 } else if matches(&key_str, &[&self.key_map.delete_character_forward]) {
945 if !self.value[self.row].is_empty() && self.col < self.value[self.row].len() {
946 self.value[self.row].remove(self.col);
947 }
948 if self.col >= self.value[self.row].len() {
949 self.merge_line_below(self.row);
950 }
951 } else if matches(&key_str, &[&self.key_map.delete_word_backward]) {
952 if self.col == 0 {
953 self.merge_line_above(self.row);
954 } else {
955 self.delete_word_left();
956 }
957 } else if matches(&key_str, &[&self.key_map.delete_word_forward]) {
958 self.col = self.col.min(self.value[self.row].len());
959 if self.col >= self.value[self.row].len() {
960 self.merge_line_below(self.row);
961 } else {
962 self.delete_word_right();
963 }
964 } else if matches(&key_str, &[&self.key_map.insert_newline]) {
965 if self.max_height == 0 || self.value.len() < self.max_height {
966 self.col = self.col.min(self.value[self.row].len());
967 self.split_line(self.row, self.col);
968 }
969 } else if matches(&key_str, &[&self.key_map.line_end]) {
970 self.cursor_end();
971 } else if matches(&key_str, &[&self.key_map.line_start]) {
972 self.cursor_start();
973 } else if matches(&key_str, &[&self.key_map.character_forward]) {
974 self.character_right();
975 } else if matches(&key_str, &[&self.key_map.line_next]) {
976 self.cursor_down();
977 } else if matches(&key_str, &[&self.key_map.word_forward]) {
978 self.word_right();
979 } else if matches(&key_str, &[&self.key_map.character_backward]) {
980 self.character_left(false);
981 } else if matches(&key_str, &[&self.key_map.line_previous]) {
982 self.cursor_up();
983 } else if matches(&key_str, &[&self.key_map.word_backward]) {
984 self.word_left();
985 } else if matches(&key_str, &[&self.key_map.input_begin]) {
986 self.move_to_begin();
987 } else if matches(&key_str, &[&self.key_map.input_end]) {
988 self.move_to_end();
989 } else if matches(&key_str, &[&self.key_map.lowercase_word_forward]) {
990 self.lowercase_right();
991 } else if matches(&key_str, &[&self.key_map.uppercase_word_forward]) {
992 self.uppercase_right();
993 } else if matches(&key_str, &[&self.key_map.capitalize_word_forward]) {
994 self.capitalize_right();
995 } else if matches(&key_str, &[&self.key_map.transpose_character_backward]) {
996 self.transpose_left();
997 } else if !matches(&key_str, &[&self.key_map.paste]) {
998 let runes: Vec<char> = key.runes.clone();
1000 if !runes.is_empty() {
1001 self.insert_runes_from_user_input(&runes);
1002 }
1003 }
1004 }
1005
1006 self.viewport.update(&msg);
1007
1008 let mut cmds: Vec<Option<Cmd>> = Vec::new();
1009
1010 if let Some(cmd) = self.cursor.update(msg) {
1011 cmds.push(Some(cmd));
1012 }
1013
1014 if (self.row != old_row || self.col != old_col) && self.cursor.mode() == CursorMode::Blink {
1015 cmds.push(Some(blink_cmd()));
1017 }
1018
1019 self.reposition_view();
1020
1021 bubbletea::batch(cmds)
1022 }
1023
1024 #[must_use]
1026 pub fn view(&self) -> String {
1027 if self.value() == "" && self.row == 0 && self.col == 0 && !self.placeholder.is_empty() {
1028 return self.placeholder_view();
1029 }
1030
1031 let style = self.current_style();
1032 let mut lines = Vec::new();
1033
1034 for (l, line) in self.value.iter().enumerate() {
1035 let is_cursor_line = self.row == l;
1036 let line_style = if is_cursor_line {
1037 &style.cursor_line
1038 } else {
1039 &style.text
1040 };
1041
1042 let mut s = String::new();
1043
1044 s.push_str(&style.prompt.render(&self.prompt));
1046
1047 if self.show_line_numbers {
1049 let ln_style = if is_cursor_line {
1050 &style.cursor_line_number
1051 } else {
1052 &style.line_number
1053 };
1054 s.push_str(&ln_style.render(&format!("{:>3} ", l + 1)));
1055 }
1056
1057 let line_str: String = line.iter().collect();
1059 if is_cursor_line && self.focus {
1060 let before: String = line[..self.col.min(line.len())].iter().collect();
1061 s.push_str(&line_style.render(&before));
1062
1063 if self.col < line.len() {
1064 let cursor_char: String = line[self.col..self.col + 1].iter().collect();
1065 let mut cursor = self.cursor.clone();
1066 cursor.set_char(&cursor_char);
1067 s.push_str(&cursor.view());
1068
1069 let after: String = line[self.col + 1..].iter().collect();
1070 s.push_str(&line_style.render(&after));
1071 } else {
1072 let mut cursor = self.cursor.clone();
1073 cursor.set_char(" ");
1074 s.push_str(&cursor.view());
1075 }
1076 } else {
1077 s.push_str(&line_style.render(&line_str));
1078 }
1079
1080 let mut current_line_width: usize = line
1082 .iter()
1083 .map(|c| unicode_width::UnicodeWidthChar::width(*c).unwrap_or(0))
1084 .sum();
1085 if is_cursor_line && self.focus && self.col >= line.len() {
1086 current_line_width += 1; }
1088
1089 let padding = self.width.saturating_sub(current_line_width);
1090 if padding > 0 {
1091 s.push_str(&line_style.render(&" ".repeat(padding)));
1092 }
1093
1094 lines.push(s);
1095 }
1096
1097 while lines.len() < self.height {
1099 let mut s = String::new();
1100 s.push_str(&style.prompt.render(&self.prompt));
1101 if self.show_line_numbers {
1102 s.push_str(&style.line_number.render(" "));
1103 }
1104 s.push_str(
1105 &style
1106 .end_of_buffer
1107 .render(&format!("{}", self.end_of_buffer_character)),
1108 );
1109 let padding = self.width.saturating_sub(1);
1110 s.push_str(&" ".repeat(padding));
1111 lines.push(s);
1112 }
1113
1114 let start = self.viewport.y_offset();
1116 let end = (start + self.height).min(lines.len());
1117 let visible: String = lines[start..end].join("\n");
1118
1119 style.base.render(&visible)
1120 }
1121
1122 fn placeholder_view(&self) -> String {
1123 let style = self.current_style();
1124 let mut lines = Vec::new();
1125
1126 let placeholder_lines: Vec<&str> = self.placeholder.lines().collect();
1127 let reserved = self.prompt_width + if self.show_line_numbers { 4 } else { 0 };
1128 let total_width = reserved + self.width;
1129
1130 for i in 0..self.height {
1131 let mut s = String::new();
1132
1133 s.push_str(&style.prompt.render(&self.prompt));
1135
1136 if self.show_line_numbers {
1138 let ln_style = if i == 0 && self.focus {
1139 &style.cursor_line_number
1140 } else {
1141 &style.line_number
1142 };
1143 if i == 0 {
1144 s.push_str(&ln_style.render(&format!("{:>3} ", 1)));
1145 } else {
1146 s.push_str(&ln_style.render(" "));
1147 }
1148 }
1149
1150 if i < placeholder_lines.len() {
1151 let line = placeholder_lines[i];
1152 if i == 0 && self.focus && !line.is_empty() {
1153 let first: String = line.chars().take(1).collect();
1155 let rest: String = line.chars().skip(1).collect();
1156
1157 let mut cursor = self.cursor.clone();
1158 cursor.text_style = style.placeholder.clone();
1159 cursor.set_char(&first);
1160 s.push_str(&cursor.view());
1161 s.push_str(&style.placeholder.render(&rest));
1162 } else {
1163 s.push_str(&style.placeholder.render(line));
1164 }
1165 } else {
1166 s.push_str(
1167 &style
1168 .end_of_buffer
1169 .render(&format!("{}", self.end_of_buffer_character)),
1170 );
1171 }
1172
1173 let line_width = lipgloss::width(&s);
1175 if line_width < total_width {
1176 s.push_str(&" ".repeat(total_width - line_width));
1177 }
1178
1179 lines.push(s);
1180 }
1181
1182 style.base.render(&lines.join("\n"))
1183 }
1184}
1185
1186impl Model for TextArea {
1187 fn init(&self) -> Option<Cmd> {
1189 if self.focus { Some(blink_cmd()) } else { None }
1190 }
1191
1192 fn update(&mut self, msg: Message) -> Option<Cmd> {
1194 TextArea::update(self, msg)
1195 }
1196
1197 fn view(&self) -> String {
1199 TextArea::view(self)
1200 }
1201}
1202
1203#[cfg(test)]
1204mod tests {
1205 use super::*;
1206
1207 #[test]
1208 fn test_textarea_new() {
1209 let ta = TextArea::new();
1210 assert_eq!(ta.height, DEFAULT_HEIGHT);
1211 assert!(ta.show_line_numbers);
1212 assert!(!ta.focused());
1213 }
1214
1215 #[test]
1216 fn test_textarea_set_value() {
1217 let mut ta = TextArea::new();
1218 ta.set_value("Hello\nWorld");
1219 assert_eq!(ta.value(), "Hello\nWorld");
1220 assert_eq!(ta.line_count(), 2);
1221 }
1222
1223 #[test]
1224 fn test_textarea_cursor_navigation() {
1225 let mut ta = TextArea::new();
1226 ta.set_value("Line 1\nLine 2\nLine 3");
1227
1228 assert_eq!(ta.row, 2); ta.move_to_begin();
1230 assert_eq!(ta.row, 0);
1231 assert_eq!(ta.col, 0);
1232
1233 ta.cursor_end();
1234 assert_eq!(ta.col, 6);
1235
1236 ta.cursor_down();
1237 assert_eq!(ta.row, 1);
1238 }
1239
1240 #[test]
1241 fn test_textarea_focus_blur() {
1242 let mut ta = TextArea::new();
1243 assert!(!ta.focused());
1244
1245 ta.focus();
1246 assert!(ta.focused());
1247
1248 ta.blur();
1249 assert!(!ta.focused());
1250 }
1251
1252 #[test]
1253 fn test_textarea_reset() {
1254 let mut ta = TextArea::new();
1255 ta.set_value("Hello\nWorld");
1256 ta.reset();
1257 assert_eq!(ta.value(), "");
1258 assert_eq!(ta.line_count(), 1);
1259 }
1260
1261 #[test]
1262 fn test_textarea_insert_newline() {
1263 let mut ta = TextArea::new();
1264 ta.set_value("Hello");
1265 ta.move_to_begin();
1266 ta.set_cursor_col(2); ta.split_line(0, 2);
1268
1269 assert_eq!(ta.line_count(), 2);
1270 assert_eq!(ta.value(), "He\nllo");
1271 }
1272
1273 #[test]
1274 fn test_textarea_delete_line() {
1275 let mut ta = TextArea::new();
1276 ta.set_value("Line 1\nLine 2\nLine 3");
1277 ta.move_to_begin();
1278 ta.row = 1;
1279 ta.col = 0;
1280 ta.merge_line_above(1);
1281
1282 assert_eq!(ta.line_count(), 2);
1283 assert_eq!(ta.value(), "Line 1Line 2\nLine 3");
1284 }
1285
1286 #[test]
1287 fn test_textarea_char_limit() {
1288 let mut ta = TextArea::new();
1289 ta.char_limit = 10;
1290 ta.set_value("This is a very long string");
1291 assert!(ta.length() <= 10);
1292 }
1293
1294 #[test]
1295 fn test_textarea_dimensions() {
1296 let mut ta = TextArea::new();
1297 ta.set_width(80);
1298 ta.set_height(24);
1299
1300 assert_eq!(ta.height(), 24);
1301 }
1302
1303 #[test]
1304 fn test_textarea_view() {
1305 let mut ta = TextArea::new();
1306 ta.set_value("Hello\nWorld");
1307 let view = ta.view();
1308
1309 assert!(view.contains("Hello"));
1310 assert!(view.contains("World"));
1311 }
1312
1313 #[test]
1314 fn test_textarea_placeholder() {
1315 let mut ta = TextArea::new();
1316 ta.placeholder = "Enter text...".to_string();
1317 let view = ta.view();
1318 assert!(view.contains("E"), "View should contain cursor char 'E'");
1321 assert!(
1322 view.contains("nter text..."),
1323 "View should contain rest of placeholder"
1324 );
1325 }
1326
1327 #[test]
1328 fn test_keymap_default() {
1329 let km = KeyMap::default();
1330 assert!(!km.character_forward.get_keys().is_empty());
1331 assert!(!km.insert_newline.get_keys().is_empty());
1332 }
1333
1334 #[test]
1336 fn test_model_init_unfocused() {
1337 let ta = TextArea::new();
1338 let cmd = Model::init(&ta);
1340 assert!(cmd.is_none());
1341 }
1342
1343 #[test]
1344 fn test_model_init_focused() {
1345 let mut ta = TextArea::new();
1346 ta.focus();
1347 let cmd = Model::init(&ta);
1349 assert!(cmd.is_some());
1350 }
1351
1352 #[test]
1353 fn test_model_view() {
1354 let mut ta = TextArea::new();
1355 ta.set_value("Test content");
1356 let model_view = Model::view(&ta);
1358 let textarea_view = TextArea::view(&ta);
1359 assert_eq!(model_view, textarea_view);
1360 }
1361
1362 #[test]
1363 fn test_model_update_handles_paste_msg() {
1364 use bubbletea::Message;
1365
1366 let mut ta = TextArea::new();
1367 ta.focus();
1368 assert_eq!(ta.value(), "");
1369
1370 let paste_msg = Message::new(PasteMsg("hello world".to_string()));
1371 let _ = Model::update(&mut ta, paste_msg);
1372
1373 assert_eq!(
1374 ta.value(),
1375 "hello world",
1376 "TextArea should insert pasted text"
1377 );
1378 }
1379
1380 #[test]
1381 fn test_model_update_unfocused_ignores_input() {
1382 use bubbletea::{KeyMsg, Message};
1383
1384 let mut ta = TextArea::new();
1385 assert!(!ta.focused());
1387 assert_eq!(ta.value(), "");
1388
1389 let key_msg = Message::new(KeyMsg::from_char('a'));
1390 let _ = Model::update(&mut ta, key_msg);
1391
1392 assert_eq!(ta.value(), "", "Unfocused textarea should ignore key input");
1393 }
1394
1395 #[test]
1396 fn test_model_update_handles_key_input() {
1397 use bubbletea::{KeyMsg, Message};
1398
1399 let mut ta = TextArea::new();
1400 ta.focus();
1401 assert_eq!(ta.value(), "");
1402
1403 let key_msg = Message::new(KeyMsg::from_char('H'));
1404 let _ = Model::update(&mut ta, key_msg);
1405
1406 assert_eq!(
1407 ta.value(),
1408 "H",
1409 "Focused textarea should insert typed character"
1410 );
1411 }
1412
1413 #[test]
1414 fn test_model_update_handles_navigation() {
1415 use bubbletea::{KeyMsg, KeyType, Message};
1416
1417 let mut ta = TextArea::new();
1418 ta.focus();
1419 ta.set_value("Hello\nWorld");
1420 ta.move_to_begin();
1421 assert_eq!(ta.row, 0);
1422 assert_eq!(ta.col, 0);
1423
1424 let down_msg = Message::new(KeyMsg::from_type(KeyType::Down));
1426 let _ = Model::update(&mut ta, down_msg);
1427
1428 assert_eq!(ta.row, 1, "TextArea should navigate down on Down key");
1429 }
1430
1431 #[test]
1432 fn test_textarea_satisfies_model_bounds() {
1433 fn requires_model<T: Model + Send + 'static>() {}
1434 requires_model::<TextArea>();
1435 }
1436
1437 #[test]
1442 fn test_model_update_backspace_deletes_char() {
1443 use bubbletea::{KeyMsg, KeyType, Message};
1444
1445 let mut ta = TextArea::new();
1446 ta.focus();
1447 ta.set_value("Hello");
1448 ta.move_to_begin();
1449 ta.col = 5; let backspace_msg = Message::new(KeyMsg::from_type(KeyType::Backspace));
1452 let _ = Model::update(&mut ta, backspace_msg);
1453
1454 assert_eq!(
1455 ta.value(),
1456 "Hell",
1457 "Backspace should delete character before cursor"
1458 );
1459 }
1460
1461 #[test]
1462 fn test_model_update_backspace_at_start_noop() {
1463 use bubbletea::{KeyMsg, KeyType, Message};
1464
1465 let mut ta = TextArea::new();
1466 ta.focus();
1467 ta.set_value("Hello");
1468 ta.move_to_begin();
1469 assert_eq!(ta.row, 0);
1470 assert_eq!(ta.col, 0);
1471
1472 let backspace_msg = Message::new(KeyMsg::from_type(KeyType::Backspace));
1473 let _ = Model::update(&mut ta, backspace_msg);
1474
1475 assert_eq!(ta.value(), "Hello", "Backspace at start should do nothing");
1476 assert_eq!(ta.col, 0);
1477 }
1478
1479 #[test]
1480 fn test_model_update_delete_forward() {
1481 use bubbletea::{KeyMsg, KeyType, Message};
1482
1483 let mut ta = TextArea::new();
1484 ta.focus();
1485 ta.set_value("Hello");
1486 ta.move_to_begin();
1487 ta.col = 0;
1488
1489 let delete_msg = Message::new(KeyMsg::from_type(KeyType::Delete));
1490 let _ = Model::update(&mut ta, delete_msg);
1491
1492 assert_eq!(
1493 ta.value(),
1494 "ello",
1495 "Delete should remove character at cursor"
1496 );
1497 }
1498
1499 #[test]
1500 fn test_model_update_cursor_left() {
1501 use bubbletea::{KeyMsg, KeyType, Message};
1502
1503 let mut ta = TextArea::new();
1504 ta.focus();
1505 ta.set_value("Hello");
1506 ta.move_to_begin();
1507 ta.col = 3;
1508
1509 let left_msg = Message::new(KeyMsg::from_type(KeyType::Left));
1510 let _ = Model::update(&mut ta, left_msg);
1511
1512 assert_eq!(ta.col, 2, "Left arrow should move cursor left");
1513 }
1514
1515 #[test]
1516 fn test_model_update_cursor_right() {
1517 use bubbletea::{KeyMsg, KeyType, Message};
1518
1519 let mut ta = TextArea::new();
1520 ta.focus();
1521 ta.set_value("Hello");
1522 ta.move_to_begin();
1523 ta.col = 0;
1524
1525 let right_msg = Message::new(KeyMsg::from_type(KeyType::Right));
1526 let _ = Model::update(&mut ta, right_msg);
1527
1528 assert_eq!(ta.col, 1, "Right arrow should move cursor right");
1529 }
1530
1531 #[test]
1532 fn test_model_update_cursor_up() {
1533 use bubbletea::{KeyMsg, KeyType, Message};
1534
1535 let mut ta = TextArea::new();
1536 ta.focus();
1537 ta.set_value("Line1\nLine2\nLine3");
1538 ta.row = 2;
1539 ta.col = 0;
1540
1541 let up_msg = Message::new(KeyMsg::from_type(KeyType::Up));
1542 let _ = Model::update(&mut ta, up_msg);
1543
1544 assert_eq!(ta.row, 1, "Up arrow should move cursor up");
1545 }
1546
1547 #[test]
1548 fn test_model_update_enter_splits_line() {
1549 use bubbletea::{KeyMsg, KeyType, Message};
1550
1551 let mut ta = TextArea::new();
1552 ta.focus();
1553 ta.set_value("Hello World");
1554 ta.move_to_begin();
1555 ta.col = 5;
1556
1557 let enter_msg = Message::new(KeyMsg::from_type(KeyType::Enter));
1558 let _ = Model::update(&mut ta, enter_msg);
1559
1560 assert_eq!(ta.line_count(), 2, "Enter should split into two lines");
1561 assert!(ta.value().contains('\n'), "Value should contain newline");
1562 }
1563
1564 #[test]
1565 fn test_textarea_view_shows_line_numbers() {
1566 let mut ta = TextArea::new();
1567 ta.show_line_numbers = true;
1568 ta.set_value("Line 1\nLine 2\nLine 3");
1569
1570 let view = ta.view();
1571
1572 assert!(
1574 view.contains('1') && view.contains('2') && view.contains('3'),
1575 "View should contain line numbers"
1576 );
1577 }
1578
1579 #[test]
1580 fn test_textarea_view_hides_line_numbers() {
1581 let mut ta = TextArea::new();
1582 ta.show_line_numbers = false;
1583 ta.set_value("A\nB\nC");
1584
1585 let view = ta.view();
1586
1587 assert!(
1589 view.contains('A') && view.contains('B') && view.contains('C'),
1590 "View should contain content"
1591 );
1592 }
1593
1594 #[test]
1595 fn test_textarea_empty_operations() {
1596 let mut ta = TextArea::new();
1597 ta.focus();
1598 assert_eq!(ta.value(), "");
1599
1600 ta.cursor_up();
1602 ta.cursor_down();
1603 ta.cursor_start();
1604 ta.cursor_end();
1605 ta.move_to_begin();
1606 ta.move_to_end();
1607
1608 assert_eq!(
1609 ta.value(),
1610 "",
1611 "Empty textarea should remain empty after navigation"
1612 );
1613 assert_eq!(ta.row, 0);
1614 assert_eq!(ta.col, 0);
1615 }
1616
1617 #[test]
1618 fn test_textarea_unicode_characters() {
1619 let mut ta = TextArea::new();
1620 ta.focus();
1621 ta.set_value("Hello δΈη π¦");
1622
1623 assert_eq!(ta.value(), "Hello δΈη π¦");
1624 let view = ta.view();
1625 assert!(view.contains("δΈη"), "View should render CJK characters");
1626 assert!(view.contains("π¦"), "View should render emoji");
1627 }
1628
1629 #[test]
1630 fn test_textarea_unicode_cursor_navigation() {
1631 use bubbletea::{KeyMsg, KeyType, Message};
1632
1633 let mut ta = TextArea::new();
1634 ta.focus();
1635 ta.set_value("ζ₯ζ¬θͺ");
1636 ta.move_to_begin();
1637
1638 let right_msg = Message::new(KeyMsg::from_type(KeyType::Right));
1640 let _ = Model::update(&mut ta, right_msg);
1641
1642 assert!(ta.col > 0, "Cursor should advance through unicode");
1643 }
1644
1645 #[test]
1646 fn test_textarea_very_long_line() {
1647 let mut ta = TextArea::new();
1648 ta.set_width(20);
1649 let long_line = "A".repeat(100);
1650 ta.set_value(&long_line);
1651
1652 let view = ta.view();
1654 assert!(!view.is_empty(), "View should render long line");
1655 }
1656
1657 #[test]
1658 fn test_textarea_max_height_enforced() {
1659 let mut ta = TextArea::new();
1660 ta.max_height = 3;
1661
1662 ta.set_value("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1664
1665 assert!(ta.line_count() >= 3, "Content should be stored");
1668 }
1669
1670 #[test]
1671 fn test_textarea_width_set_propagates() {
1672 let mut ta = TextArea::new();
1673 ta.set_width(80);
1674
1675 let view = ta.view();
1677 assert!(!view.is_empty(), "View should work after width set");
1678 }
1679
1680 #[test]
1681 fn test_textarea_width_uses_prompt_display_width() {
1682 let mut ta = TextArea::new();
1683 ta.show_line_numbers = false;
1684 ta.prompt = "η ".to_string(); ta.set_width(6);
1686
1687 assert_eq!(ta.width(), 3);
1690 }
1691
1692 #[test]
1693 fn test_model_init_returns_blink_when_focused() {
1694 let mut ta = TextArea::new();
1695 ta.focus();
1696
1697 let cmd = Model::init(&ta);
1698 assert!(
1699 cmd.is_some(),
1700 "Focused textarea init should return blink command"
1701 );
1702 }
1703
1704 #[test]
1705 fn test_model_init_returns_none_when_unfocused() {
1706 let ta = TextArea::new();
1707
1708 let cmd = Model::init(&ta);
1709 assert!(cmd.is_none(), "Unfocused textarea init should return None");
1710 }
1711
1712 #[test]
1717 fn test_bracketed_paste_basic() {
1718 use bubbletea::{KeyMsg, KeyType, Message};
1719
1720 let mut ta = TextArea::new();
1721 ta.focus();
1722
1723 let key_msg = Message::new(KeyMsg {
1724 key_type: KeyType::Runes,
1725 runes: vec!['h', 'e', 'l', 'l', 'o'],
1726 alt: false,
1727 paste: true,
1728 });
1729 let _ = Model::update(&mut ta, key_msg);
1730
1731 assert_eq!(ta.value(), "hello");
1732 }
1733
1734 #[test]
1735 fn test_bracketed_paste_multiline_preserves_newlines() {
1736 use bubbletea::{KeyMsg, KeyType, Message};
1737
1738 let mut ta = TextArea::new();
1739 ta.focus();
1740
1741 let key_msg = Message::new(KeyMsg {
1743 key_type: KeyType::Runes,
1744 runes: "line1\nline2\nline3".chars().collect(),
1745 alt: false,
1746 paste: true,
1747 });
1748 let _ = Model::update(&mut ta, key_msg);
1749
1750 assert_eq!(
1751 ta.value(),
1752 "line1\nline2\nline3",
1753 "TextArea should preserve newlines in paste"
1754 );
1755 assert_eq!(ta.line_count(), 3, "Should have 3 lines after paste");
1756 }
1757
1758 #[test]
1759 fn test_bracketed_paste_crlf_normalized() {
1760 use bubbletea::{KeyMsg, KeyType, Message};
1761
1762 let mut ta = TextArea::new();
1763 ta.focus();
1764
1765 let key_msg = Message::new(KeyMsg {
1767 key_type: KeyType::Runes,
1768 runes: "line1\r\nline2".chars().collect(),
1769 alt: false,
1770 paste: true,
1771 });
1772 let _ = Model::update(&mut ta, key_msg);
1773
1774 assert_eq!(
1775 ta.value(),
1776 "line1\nline2",
1777 "CRLF should be normalized to LF"
1778 );
1779 assert_eq!(ta.line_count(), 2);
1780 }
1781
1782 #[test]
1783 fn test_bracketed_paste_respects_char_limit() {
1784 use bubbletea::{KeyMsg, KeyType, Message};
1785
1786 let mut ta = TextArea::new();
1787 ta.focus();
1788 ta.char_limit = 10;
1789
1790 let key_msg = Message::new(KeyMsg {
1791 key_type: KeyType::Runes,
1792 runes: "this is a very long paste".chars().collect(),
1793 alt: false,
1794 paste: true,
1795 });
1796 let _ = Model::update(&mut ta, key_msg);
1797
1798 assert_eq!(ta.length(), 10, "Paste should respect char_limit");
1799 }
1800
1801 #[test]
1802 fn test_bracketed_paste_unfocused_ignored() {
1803 use bubbletea::{KeyMsg, KeyType, Message};
1804
1805 let mut ta = TextArea::new();
1806 let key_msg = Message::new(KeyMsg {
1809 key_type: KeyType::Runes,
1810 runes: "ignored".chars().collect(),
1811 alt: false,
1812 paste: true,
1813 });
1814 let _ = Model::update(&mut ta, key_msg);
1815
1816 assert_eq!(ta.value(), "", "Unfocused textarea should ignore paste");
1817 }
1818
1819 #[test]
1820 fn test_bracketed_paste_inserts_at_cursor() {
1821 use bubbletea::{KeyMsg, KeyType, Message};
1822
1823 let mut ta = TextArea::new();
1824 ta.focus();
1825 ta.set_value("helloworld");
1826 ta.move_to_begin();
1827 ta.col = 5; let key_msg = Message::new(KeyMsg {
1830 key_type: KeyType::Runes,
1831 runes: " ".chars().collect(),
1832 alt: false,
1833 paste: true,
1834 });
1835 let _ = Model::update(&mut ta, key_msg);
1836
1837 assert_eq!(ta.value(), "hello world");
1838 }
1839
1840 #[test]
1841 fn test_bracketed_paste_unicode() {
1842 use bubbletea::{KeyMsg, KeyType, Message};
1843
1844 let mut ta = TextArea::new();
1845 ta.focus();
1846
1847 let key_msg = Message::new(KeyMsg {
1848 key_type: KeyType::Runes,
1849 runes: "hello δΈη π".chars().collect(),
1850 alt: false,
1851 paste: true,
1852 });
1853 let _ = Model::update(&mut ta, key_msg);
1854
1855 assert_eq!(ta.value(), "hello δΈη π");
1856 }
1857
1858 #[test]
1859 fn test_bracketed_paste_large_content() {
1860 use bubbletea::{KeyMsg, KeyType, Message};
1861
1862 let mut ta = TextArea::new();
1863 ta.focus();
1864
1865 let large_text: String = "a".repeat(1000);
1867 let key_msg = Message::new(KeyMsg {
1868 key_type: KeyType::Runes,
1869 runes: large_text.chars().collect(),
1870 alt: false,
1871 paste: true,
1872 });
1873 let _ = Model::update(&mut ta, key_msg);
1874
1875 assert_eq!(
1876 ta.value().len(),
1877 1000,
1878 "Large paste should work without issues"
1879 );
1880 }
1881
1882 #[test]
1883 fn test_bracketed_paste_multiline_cursor_position() {
1884 use bubbletea::{KeyMsg, KeyType, Message};
1885
1886 let mut ta = TextArea::new();
1887 ta.focus();
1888
1889 let key_msg = Message::new(KeyMsg {
1890 key_type: KeyType::Runes,
1891 runes: "line1\nline2\nline3".chars().collect(),
1892 alt: false,
1893 paste: true,
1894 });
1895 let _ = Model::update(&mut ta, key_msg);
1896
1897 assert_eq!(ta.row, 2, "Cursor should be on line 3 (index 2)");
1899 assert_eq!(ta.col, 5, "Cursor should be at end of 'line3'");
1900 }
1901}