Skip to main content

bubbles/
textarea.rs

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