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::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/// Key bindings for textarea navigation.
34#[derive(Debug, Clone)]
35pub struct KeyMap {
36    /// Move character forward.
37    pub character_forward: Binding,
38    /// Move character backward.
39    pub character_backward: Binding,
40    /// Delete text after cursor.
41    pub delete_after_cursor: Binding,
42    /// Delete text before cursor.
43    pub delete_before_cursor: Binding,
44    /// Delete character backward.
45    pub delete_character_backward: Binding,
46    /// Delete character forward.
47    pub delete_character_forward: Binding,
48    /// Delete word backward.
49    pub delete_word_backward: Binding,
50    /// Delete word forward.
51    pub delete_word_forward: Binding,
52    /// Insert newline.
53    pub insert_newline: Binding,
54    /// Move to line end.
55    pub line_end: Binding,
56    /// Move to next line.
57    pub line_next: Binding,
58    /// Move to previous line.
59    pub line_previous: Binding,
60    /// Move to line start.
61    pub line_start: Binding,
62    /// Paste from clipboard.
63    pub paste: Binding,
64    /// Move word backward.
65    pub word_backward: Binding,
66    /// Move word forward.
67    pub word_forward: Binding,
68    /// Move to input begin.
69    pub input_begin: Binding,
70    /// Move to input end.
71    pub input_end: Binding,
72    /// Uppercase word forward.
73    pub uppercase_word_forward: Binding,
74    /// Lowercase word forward.
75    pub lowercase_word_forward: Binding,
76    /// Capitalize word forward.
77    pub capitalize_word_forward: Binding,
78    /// Transpose character backward.
79    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/// Styles for the textarea in different states.
154#[derive(Debug, Clone)]
155pub struct Styles {
156    /// Base style.
157    pub base: Style,
158    /// Cursor line style.
159    pub cursor_line: Style,
160    /// Cursor line number style.
161    pub cursor_line_number: Style,
162    /// End of buffer style.
163    pub end_of_buffer: Style,
164    /// Line number style.
165    pub line_number: Style,
166    /// Placeholder style.
167    pub placeholder: Style,
168    /// Prompt style.
169    pub prompt: Style,
170    /// Text style.
171    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            // Match Go bubbles textarea defaults: no color styling unless explicitly configured.
180            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/// Message for paste operations.
191#[derive(Debug, Clone)]
192pub struct PasteMsg(pub String);
193
194/// Message for paste errors.
195#[derive(Debug, Clone)]
196pub struct PasteErrMsg(pub String);
197
198/// Multi-line text area model.
199#[derive(Debug, Clone)]
200pub struct TextArea {
201    /// Current error.
202    pub err: Option<String>,
203    /// Prompt string (displayed at start of each line).
204    pub prompt: String,
205    /// Placeholder text.
206    pub placeholder: String,
207    /// Whether to show line numbers.
208    pub show_line_numbers: bool,
209    /// End of buffer character.
210    pub end_of_buffer_character: char,
211    /// Key bindings.
212    pub key_map: KeyMap,
213    /// Style for focused state.
214    pub focused_style: Styles,
215    /// Style for blurred state.
216    pub blurred_style: Styles,
217    /// Cursor model.
218    pub cursor: Cursor,
219    /// Character limit (0 = no limit).
220    pub char_limit: usize,
221    /// Maximum height in rows.
222    pub max_height: usize,
223    /// Maximum width in columns.
224    pub max_width: usize,
225    /// Current style (points to focused or blurred).
226    use_focused_style: bool,
227    /// Prompt width.
228    prompt_width: usize,
229    /// Display width.
230    width: usize,
231    /// Display height.
232    height: usize,
233    /// Text value (lines of characters).
234    value: Vec<Vec<char>>,
235    /// Focus state.
236    focus: bool,
237    /// Cursor column.
238    col: usize,
239    /// Cursor row.
240    row: usize,
241    /// Last character offset for vertical navigation.
242    last_char_offset: usize,
243    /// Viewport for scrolling.
244    viewport: Viewport,
245    /// Rune sanitizer.
246    sanitizer: Sanitizer,
247}
248
249impl Default for TextArea {
250    fn default() -> Self {
251        Self::new()
252    }
253}
254
255impl TextArea {
256    /// Creates a new textarea with default settings.
257    #[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, // "┃ " is 2 chars
276            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    /// Sets the value of the textarea.
293    pub fn set_value(&mut self, s: &str) {
294        self.reset();
295        self.insert_string(s);
296    }
297
298    /// Inserts a string at the cursor position.
299    pub fn insert_string(&mut self, s: &str) {
300        self.insert_runes_from_user_input(&s.chars().collect::<Vec<_>>());
301    }
302
303    /// Inserts a single character at the cursor position.
304    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        // Split input into lines
327        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        // Obey max lines
341        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        // Save tail of current line
351        let tail: Vec<char> = self.value[self.row][self.col..].to_vec();
352
353        // Paste first line at cursor
354        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        // Handle additional lines
359        if lines.len() > 1 {
360            // Insert new lines
361            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        // Add tail at end
369        self.value[self.row].extend_from_slice(&tail);
370        self.set_cursor_col(self.col);
371    }
372
373    /// Returns the current value as a string.
374    #[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    /// Returns the total length in characters.
384    #[must_use]
385    pub fn length(&self) -> usize {
386        let char_count: usize = self.value.iter().map(|line| line.len()).sum();
387        // Add newlines between lines
388        char_count + self.value.len().saturating_sub(1)
389    }
390
391    /// Returns the number of lines.
392    #[must_use]
393    pub fn line_count(&self) -> usize {
394        self.value.len()
395    }
396
397    /// Returns the current line number (0-indexed).
398    #[must_use]
399    pub fn line(&self) -> usize {
400        self.row
401    }
402
403    /// Returns the current cursor column (0-indexed, in characters).
404    #[must_use]
405    pub fn cursor_col(&self) -> usize {
406        self.col
407    }
408
409    /// Returns the current cursor position (row, col) in character indices.
410    #[must_use]
411    pub fn cursor_pos(&self) -> (usize, usize) {
412        (self.row, self.col)
413    }
414
415    /// Returns the cursor position as a byte offset into the string returned by [`Self::value`].
416    #[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); // '\n'
430        }
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    /// Sets the cursor position based on a byte offset into the string returned by [`Self::value`].
441    ///
442    /// If the offset points to the newline separator between lines, the cursor is placed at the end
443    /// of the preceding line. Offsets beyond the end of the buffer clamp to the end.
444    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                // Consume the '\n' separator between lines.
481                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        // Clamp to end.
497        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    /// Moves cursor down one line.
503    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    /// Moves cursor up one line.
511    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    /// Sets cursor column position.
519    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    /// Moves cursor to start of line.
525    pub fn cursor_start(&mut self) {
526        self.set_cursor_col(0);
527    }
528
529    /// Moves cursor to end of line.
530    pub fn cursor_end(&mut self) {
531        self.set_cursor_col(self.value[self.row].len());
532    }
533
534    /// Moves cursor left one character.
535    pub fn cursor_left(&mut self) {
536        self.character_left(false);
537    }
538
539    /// Moves cursor right one character.
540    pub fn cursor_right(&mut self) {
541        self.character_right();
542    }
543
544    /// Returns whether the textarea is focused.
545    #[must_use]
546    pub fn focused(&self) -> bool {
547        self.focus
548    }
549
550    /// Focuses the textarea.
551    pub fn focus(&mut self) -> Option<Cmd> {
552        self.focus = true;
553        self.use_focused_style = true;
554        self.cursor.focus()
555    }
556
557    /// Blurs the textarea.
558    pub fn blur(&mut self) {
559        self.focus = false;
560        self.use_focused_style = false;
561        self.cursor.blur();
562    }
563
564    /// Resets the textarea to empty.
565    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 cursor is at or past end of line, move to last valid position for transpose
597        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        // Skip whitespace
615        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        // Skip non-whitespace
624        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        // Skip whitespace
653        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        // Skip non-whitespace
662        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        // Skip whitespace
731        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        // Skip non-whitespace
743        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        // Skip whitespace
781        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    /// Sets the width of the textarea.
815    pub fn set_width(&mut self, w: usize) {
816        self.prompt_width = UnicodeWidthStr::width(self.prompt.as_str());
817
818        let reserved_outer = 0; // No frame in base style
819        let mut reserved_inner = self.prompt_width;
820
821        if self.show_line_numbers {
822            reserved_inner += 4; // Line numbers
823        }
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    /// Returns the width.
839    #[must_use]
840    pub fn width(&self) -> usize {
841        self.width
842    }
843
844    /// Sets the height of the textarea.
845    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    /// Returns the height.
856    #[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    /// Updates the textarea based on messages.
901    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        // Handle paste message
911        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                // Insert regular characters
999                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            // Reset blink state when cursor moves - trigger blink cycle
1016            cmds.push(Some(blink_cmd()));
1017        }
1018
1019        self.reposition_view();
1020
1021        bubbletea::batch(cmds)
1022    }
1023
1024    /// Renders the textarea.
1025    #[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            // Prompt
1045            s.push_str(&style.prompt.render(&self.prompt));
1046
1047            // Line numbers
1048            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            // Line content
1058            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            // Padding
1081            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; // Cursor at end adds a space
1087            }
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        // Pad to height with empty lines
1098        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        // Apply viewport
1115        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            // Prompt
1134            s.push_str(&style.prompt.render(&self.prompt));
1135
1136            // Line numbers
1137            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                    // First char as cursor
1154                    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            // Pad each rendered line to the same visible width as Go bubbles.
1174            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    /// Initialize the textarea and return a blink command if focused.
1188    fn init(&self) -> Option<Cmd> {
1189        if self.focus { Some(blink_cmd()) } else { None }
1190    }
1191
1192    /// Update the textarea state based on incoming messages.
1193    fn update(&mut self, msg: Message) -> Option<Cmd> {
1194        TextArea::update(self, msg)
1195    }
1196
1197    /// Render the textarea.
1198    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); // After set_value, cursor at end
1229        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); // After "He"
1267        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        // The placeholder is split by cursor rendering: "E" (cursor) + "nter text..." (styled)
1319        // So check for both parts - the cursor char and the rest
1320        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    // Model trait implementation tests
1335    #[test]
1336    fn test_model_init_unfocused() {
1337        let ta = TextArea::new();
1338        // Unfocused textarea should not return init command
1339        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        // Focused textarea should return blink command
1348        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        // Model::view should return same result as TextArea::view
1357        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        // Not focused
1386        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        // Press Down arrow
1425        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    // ========================================================================
1438    // Additional Model trait tests for bead charmed_rust-29c
1439    // ========================================================================
1440
1441    #[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; // At end of "Hello"
1450
1451        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        // Line numbers should appear in view
1573        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        // Content should be present but line numbers formatting may differ
1588        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        // Navigation on empty should not panic
1601        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        // Move right through unicode chars
1639        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        // Should not panic, view should work
1653        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        // Try to insert many lines
1663        ta.set_value("1\n2\n3\n4\n5\n6\n7\n8\n9\n10");
1664
1665        // max_height limits visible height, not content
1666        // Verify the content is still there
1667        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        // Width should be accessible
1676        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(); // display width 3, char count 2
1685        ta.set_width(6);
1686
1687        // Total width budget is 6, so content width must be 3 when prompt width is
1688        // measured as display width (not char count).
1689        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    // === Bracketed Paste Tests ===
1713    // These tests verify paste behavior when receiving KeyMsg with paste=true,
1714    // which is how terminals deliver bracketed paste sequences.
1715
1716    #[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        // Multi-line paste should preserve newlines in TextArea
1742        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        // Windows-style CRLF should be normalized to LF
1766        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        // Not focused!
1807
1808        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; // After "hello"
1828
1829        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        // Simulate a large paste (1000 characters)
1866        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        // Cursor should be at end of last pasted line
1898        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}