1use crate::buffer::ScreenBuffer;
5use crate::cell::Cell;
6use crate::cursor::{CursorPosition, CursorState, Selection};
7use crate::event::{Event, KeyCode, KeyEvent, Modifiers};
8use crate::geometry::Rect;
9use crate::highlight::{Highlighter, NoHighlighter};
10use crate::style::Style;
11use crate::text_buffer::TextBuffer;
12use crate::undo::{EditOperation, UndoStack};
13use crate::wrap::wrap_line;
14use unicode_width::UnicodeWidthChar;
15
16use super::{EventResult, InteractiveWidget, Widget};
17
18pub struct TextArea {
23 pub buffer: TextBuffer,
25 pub cursor: CursorState,
27 pub undo_stack: UndoStack,
29 highlighter: Box<dyn Highlighter>,
30 pub scroll_offset: usize,
32 pub show_line_numbers: bool,
34 pub style: Style,
36 pub cursor_style: Style,
38 pub selection_style: Style,
40 pub line_number_style: Style,
42}
43
44impl TextArea {
45 pub fn new() -> Self {
47 Self {
48 buffer: TextBuffer::new(),
49 cursor: CursorState::new(0, 0),
50 undo_stack: UndoStack::new(1000),
51 highlighter: Box::new(NoHighlighter),
52 scroll_offset: 0,
53 show_line_numbers: false,
54 style: Style::default(),
55 cursor_style: Style::new().reverse(true),
56 selection_style: Style::new().reverse(true),
57 line_number_style: Style::new().dim(true),
58 }
59 }
60
61 pub fn from_text(text: &str) -> Self {
63 let mut ta = Self::new();
64 ta.buffer = TextBuffer::from_text(text);
65 ta
66 }
67
68 #[must_use]
70 pub fn with_highlighter(mut self, h: Box<dyn Highlighter>) -> Self {
71 self.highlighter = h;
72 self
73 }
74
75 #[must_use]
77 pub fn with_style(mut self, s: Style) -> Self {
78 self.style = s;
79 self
80 }
81
82 #[must_use]
84 pub fn with_line_numbers(mut self, show: bool) -> Self {
85 self.show_line_numbers = show;
86 self
87 }
88
89 #[must_use]
91 pub fn with_cursor_style(mut self, s: Style) -> Self {
92 self.cursor_style = s;
93 self
94 }
95
96 #[must_use]
98 pub fn with_selection_style(mut self, s: Style) -> Self {
99 self.selection_style = s;
100 self
101 }
102
103 pub fn text(&self) -> String {
105 self.buffer.to_string()
106 }
107
108 pub fn insert_char(&mut self, ch: char) {
112 self.delete_selection_if_active();
113 let pos = self.cursor.position;
114 self.buffer.insert_char(pos.line, pos.col, ch);
115 self.undo_stack.push(EditOperation::Insert {
116 pos,
117 text: ch.to_string(),
118 });
119 self.highlighter.on_edit(pos.line);
120 if ch == '\n' {
122 self.cursor.position.line += 1;
123 self.cursor.position.col = 0;
124 } else {
125 self.cursor.position.col += 1;
126 }
127 self.cursor.preferred_col = None;
128 }
129
130 pub fn insert_str(&mut self, text: &str) {
132 self.delete_selection_if_active();
133 let pos = self.cursor.position;
134 self.buffer.insert_str(pos.line, pos.col, text);
135 self.undo_stack.push(EditOperation::Insert {
136 pos,
137 text: text.to_string(),
138 });
139 self.highlighter.on_edit(pos.line);
140
141 for ch in text.chars() {
143 if ch == '\n' {
144 self.cursor.position.line += 1;
145 self.cursor.position.col = 0;
146 } else {
147 self.cursor.position.col += 1;
148 }
149 }
150 self.cursor.preferred_col = None;
151 }
152
153 pub fn delete_backward(&mut self) {
155 if self.delete_selection_if_active() {
156 return;
157 }
158 let pos = self.cursor.position;
159 if pos.col > 0 {
160 let del_col = pos.col - 1;
162 if let Some(line_text) = self.buffer.line(pos.line) {
163 let deleted: String = line_text
164 .chars()
165 .nth(del_col)
166 .map(String::from)
167 .unwrap_or_default();
168 self.buffer.delete_char(pos.line, del_col);
169 self.undo_stack.push(EditOperation::Delete {
170 pos: CursorPosition::new(pos.line, del_col),
171 text: deleted,
172 });
173 self.highlighter.on_edit(pos.line);
174 self.cursor.position.col -= 1;
175 }
176 } else if pos.line > 0 {
177 let prev_line_len = self.buffer.line_len(pos.line - 1).unwrap_or(0);
179 self.buffer.delete_char(pos.line - 1, prev_line_len);
180 self.undo_stack.push(EditOperation::Delete {
181 pos: CursorPosition::new(pos.line - 1, prev_line_len),
182 text: "\n".to_string(),
183 });
184 self.highlighter.on_edit(pos.line - 1);
185 self.cursor.position.line -= 1;
186 self.cursor.position.col = prev_line_len;
187 }
188 self.cursor.preferred_col = None;
189 }
190
191 pub fn delete_forward(&mut self) {
193 if self.delete_selection_if_active() {
194 return;
195 }
196 let pos = self.cursor.position;
197 let line_len = self.buffer.line_len(pos.line).unwrap_or(0);
198 if pos.col < line_len {
199 if let Some(line_text) = self.buffer.line(pos.line) {
200 let deleted: String = line_text
201 .chars()
202 .nth(pos.col)
203 .map(String::from)
204 .unwrap_or_default();
205 self.buffer.delete_char(pos.line, pos.col);
206 self.undo_stack
207 .push(EditOperation::Delete { pos, text: deleted });
208 self.highlighter.on_edit(pos.line);
209 }
210 } else if pos.line + 1 < self.buffer.line_count() {
211 self.buffer.delete_char(pos.line, pos.col);
213 self.undo_stack.push(EditOperation::Delete {
214 pos,
215 text: "\n".to_string(),
216 });
217 self.highlighter.on_edit(pos.line);
218 }
219 }
220
221 pub fn delete_selection(&mut self) -> bool {
225 self.delete_selection_if_active()
226 }
227
228 pub fn new_line(&mut self) {
230 self.insert_char('\n');
231 }
232
233 pub fn undo(&mut self) {
235 if let Some(op) = self.undo_stack.undo() {
236 self.apply_operation(&op);
237 }
238 }
239
240 pub fn redo(&mut self) {
242 if let Some(op) = self.undo_stack.redo() {
243 self.apply_operation(&op);
244 }
245 }
246
247 pub fn ensure_cursor_visible(&mut self, area_height: u16) {
249 let height = area_height as usize;
250 if height == 0 {
251 return;
252 }
253 let line = self.cursor.position.line;
254 if line < self.scroll_offset {
255 self.scroll_offset = line;
256 } else if line >= self.scroll_offset + height {
257 self.scroll_offset = line.saturating_sub(height - 1);
258 }
259 }
260
261 fn delete_selection_if_active(&mut self) -> bool {
265 let sel = match self.cursor.selection.take() {
266 Some(s) if !s.is_empty() => s,
267 other => {
268 self.cursor.selection = other;
269 return false;
270 }
271 };
272
273 let (start, end) = sel.ordered();
274 if let Some(selected) = self.selected_text_for(&sel) {
275 self.buffer
276 .delete_range(start.line, start.col, end.line, end.col);
277 self.undo_stack.push(EditOperation::Delete {
278 pos: start,
279 text: selected,
280 });
281 self.highlighter.on_edit(start.line);
282 self.cursor.position = start;
283 self.cursor.preferred_col = None;
284 }
285 true
286 }
287
288 fn selected_text_for(&self, sel: &Selection) -> Option<String> {
290 if sel.is_empty() {
291 return None;
292 }
293 let (start, end) = sel.ordered();
294 let mut result = String::new();
295 for line_idx in start.line..=end.line {
296 if let Some(line_text) = self.buffer.line(line_idx) {
297 let ls = if line_idx == start.line { start.col } else { 0 };
298 let le = if line_idx == end.line {
299 end.col.min(line_text.chars().count())
300 } else {
301 line_text.chars().count()
302 };
303 let chars: String = line_text
304 .chars()
305 .skip(ls)
306 .take(le.saturating_sub(ls))
307 .collect();
308 result.push_str(&chars);
309 if line_idx < end.line {
310 result.push('\n');
311 }
312 }
313 }
314 if result.is_empty() {
315 None
316 } else {
317 Some(result)
318 }
319 }
320
321 fn apply_operation(&mut self, op: &EditOperation) {
323 match op {
324 EditOperation::Insert { pos, text } => {
325 self.buffer.insert_str(pos.line, pos.col, text);
326 let mut line = pos.line;
328 let mut col = pos.col;
329 for ch in text.chars() {
330 if ch == '\n' {
331 line += 1;
332 col = 0;
333 } else {
334 col += 1;
335 }
336 }
337 self.cursor.position = CursorPosition::new(line, col);
338 }
339 EditOperation::Delete { pos, text } => {
340 let mut end_line = pos.line;
342 let mut end_col = pos.col;
343 for ch in text.chars() {
344 if ch == '\n' {
345 end_line += 1;
346 end_col = 0;
347 } else {
348 end_col += 1;
349 }
350 }
351 self.buffer
352 .delete_range(pos.line, pos.col, end_line, end_col);
353 self.cursor.position = *pos;
354 }
355 EditOperation::Replace {
356 pos,
357 old_text,
358 new_text,
359 } => {
360 let mut end_line = pos.line;
362 let mut end_col = pos.col;
363 for ch in old_text.chars() {
364 if ch == '\n' {
365 end_line += 1;
366 end_col = 0;
367 } else {
368 end_col += 1;
369 }
370 }
371 self.buffer
372 .delete_range(pos.line, pos.col, end_line, end_col);
373 self.buffer.insert_str(pos.line, pos.col, new_text);
374 self.cursor.position = *pos;
375 }
376 }
377 self.cursor.preferred_col = None;
378 }
379}
380
381impl Default for TextArea {
382 fn default() -> Self {
383 Self::new()
384 }
385}
386
387impl Widget for TextArea {
388 fn render(&self, area: Rect, buf: &mut ScreenBuffer) {
389 if area.size.width == 0 || area.size.height == 0 {
390 return;
391 }
392
393 let height = area.size.height as usize;
394 let total_width = area.size.width as usize;
395
396 let gutter_width = if self.show_line_numbers {
398 let digits = crate::wrap::line_number_width(self.buffer.line_count()) as usize;
399 digits + 1 } else {
401 0
402 };
403
404 let text_width = total_width.saturating_sub(gutter_width);
405 if text_width == 0 {
406 return;
407 }
408
409 let mut row: usize = 0;
411 let mut logical_line = self.scroll_offset;
412
413 while row < height && logical_line < self.buffer.line_count() {
414 let line_text = self.buffer.line(logical_line).unwrap_or_default();
415
416 let spans = self.highlighter.highlight_line(logical_line, &line_text);
418
419 let wrapped = wrap_line(&line_text, text_width);
421
422 for (wrap_idx, (visual_text, start_col)) in wrapped.iter().enumerate() {
423 if row >= height {
424 break;
425 }
426
427 let y = area.position.y + row as u16;
428
429 if self.show_line_numbers {
431 if wrap_idx == 0 {
432 let num_str = format!("{}", logical_line + 1);
433 let padded = format!("{:>width$} ", num_str, width = gutter_width - 1);
434 for (i, ch) in padded.chars().enumerate() {
435 let x = area.position.x + i as u16;
436 if x < area.position.x + area.size.width {
437 buf.set(
438 x,
439 y,
440 Cell::new(ch.to_string(), self.line_number_style.clone()),
441 );
442 }
443 }
444 } else {
445 for i in 0..gutter_width {
447 let x = area.position.x + i as u16;
448 if x < area.position.x + area.size.width {
449 buf.set(
450 x,
451 y,
452 Cell::new(" ".to_string(), self.line_number_style.clone()),
453 );
454 }
455 }
456 }
457 }
458
459 let gutter_x = area.position.x + gutter_width as u16;
461 let mut col_offset: usize = 0;
462
463 for (char_idx, ch) in visual_text.chars().enumerate() {
464 let buffer_col = start_col + char_idx;
465 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
466 let x = gutter_x + col_offset as u16;
467
468 if x >= area.position.x + area.size.width {
469 break;
470 }
471
472 let char_style = self.resolve_style(logical_line, buffer_col, &spans);
474
475 buf.set(x, y, Cell::new(ch.to_string(), char_style));
476 col_offset += ch_width;
477 }
478
479 if logical_line == self.cursor.position.line {
481 let col = self.cursor.position.col;
482 let end_col = start_col + visual_text.chars().count();
483 let is_last_wrap = wrap_idx == wrapped.len() - 1;
484 let cursor_in_wrap = col >= *start_col && (col < end_col || is_last_wrap);
485
486 if cursor_in_wrap {
487 let cursor_visual_col = self.cursor.position.col - start_col;
488 let cursor_x_offset: usize = visual_text
490 .chars()
491 .take(cursor_visual_col)
492 .map(|c| UnicodeWidthChar::width(c).unwrap_or(0))
493 .sum();
494 let cursor_x = gutter_x + cursor_x_offset as u16;
495 if cursor_x < area.position.x + area.size.width {
496 let cursor_ch = visual_text
497 .chars()
498 .nth(cursor_visual_col)
499 .map(|c| c.to_string())
500 .unwrap_or_else(|| " ".to_string());
501 buf.set(cursor_x, y, Cell::new(cursor_ch, self.cursor_style.clone()));
502 }
503 }
504 }
505
506 row += 1;
507 }
508
509 logical_line += 1;
510 }
511 }
512}
513
514impl TextArea {
515 fn resolve_style(
517 &self,
518 line: usize,
519 col: usize,
520 spans: &[crate::highlight::HighlightSpan],
521 ) -> Style {
522 let pos = CursorPosition::new(line, col);
523
524 if let Some(ref sel) = self.cursor.selection
526 && sel.contains(pos)
527 {
528 return self.selection_style.clone();
529 }
530
531 for span in spans {
533 if col >= span.start_col && col < span.end_col {
534 return span.style.clone();
535 }
536 }
537
538 self.style.clone()
540 }
541}
542
543impl InteractiveWidget for TextArea {
544 fn handle_event(&mut self, event: &Event) -> EventResult {
545 match event {
546 Event::Key(key_event) => self.handle_key(key_event),
547 _ => EventResult::Ignored,
548 }
549 }
550}
551
552impl TextArea {
553 fn handle_key(&mut self, key: &KeyEvent) -> EventResult {
555 let shift = key.modifiers.contains(Modifiers::SHIFT);
556 let ctrl = key.modifiers.contains(Modifiers::CTRL);
557
558 match key.code {
559 KeyCode::Left => {
560 if shift {
561 if self.cursor.selection.is_none() {
562 self.cursor.start_selection();
563 }
564 self.cursor.position = self.move_left_pos();
565 self.cursor.extend_selection();
566 } else {
567 self.cursor.move_left(&self.buffer);
568 }
569 EventResult::Consumed
570 }
571 KeyCode::Right => {
572 if shift {
573 if self.cursor.selection.is_none() {
574 self.cursor.start_selection();
575 }
576 self.cursor.position = self.move_right_pos();
577 self.cursor.extend_selection();
578 } else {
579 self.cursor.move_right(&self.buffer);
580 }
581 EventResult::Consumed
582 }
583 KeyCode::Up => {
584 if shift {
585 if self.cursor.selection.is_none() {
586 self.cursor.start_selection();
587 }
588 self.move_up_no_clear();
589 self.cursor.extend_selection();
590 } else {
591 self.cursor.move_up(&self.buffer);
592 }
593 EventResult::Consumed
594 }
595 KeyCode::Down => {
596 if shift {
597 if self.cursor.selection.is_none() {
598 self.cursor.start_selection();
599 }
600 self.move_down_no_clear();
601 self.cursor.extend_selection();
602 } else {
603 self.cursor.move_down(&self.buffer);
604 }
605 EventResult::Consumed
606 }
607 KeyCode::Home => {
608 if ctrl {
609 self.cursor.move_to_buffer_start();
610 } else {
611 self.cursor.move_to_line_start();
612 }
613 EventResult::Consumed
614 }
615 KeyCode::End => {
616 if ctrl {
617 self.cursor.move_to_buffer_end(&self.buffer);
618 } else {
619 self.cursor.move_to_line_end(&self.buffer);
620 }
621 EventResult::Consumed
622 }
623 KeyCode::Backspace => {
624 self.delete_backward();
625 EventResult::Consumed
626 }
627 KeyCode::Delete => {
628 self.delete_forward();
629 EventResult::Consumed
630 }
631 KeyCode::Enter => {
632 self.new_line();
633 EventResult::Consumed
634 }
635 KeyCode::Char(ch) => {
636 if ctrl && ch == 'z' {
637 self.undo();
638 } else if ctrl && ch == 'y' {
639 self.redo();
640 } else if !ctrl {
641 self.insert_char(ch);
642 } else {
643 return EventResult::Ignored;
644 }
645 EventResult::Consumed
646 }
647 _ => EventResult::Ignored,
648 }
649 }
650
651 fn move_left_pos(&self) -> CursorPosition {
653 let mut pos = self.cursor.position;
654 if pos.col > 0 {
655 pos.col -= 1;
656 } else if pos.line > 0 {
657 pos.line -= 1;
658 pos.col = self.buffer.line_len(pos.line).unwrap_or(0);
659 }
660 pos
661 }
662
663 fn move_right_pos(&self) -> CursorPosition {
665 let mut pos = self.cursor.position;
666 let line_len = self.buffer.line_len(pos.line).unwrap_or(0);
667 if pos.col < line_len {
668 pos.col += 1;
669 } else if pos.line + 1 < self.buffer.line_count() {
670 pos.line += 1;
671 pos.col = 0;
672 }
673 pos
674 }
675
676 fn move_up_no_clear(&mut self) {
678 if self.cursor.position.line > 0 {
679 let target_col = self
680 .cursor
681 .preferred_col
682 .unwrap_or(self.cursor.position.col);
683 self.cursor.preferred_col = Some(target_col);
684 self.cursor.position.line -= 1;
685 let line_len = self.buffer.line_len(self.cursor.position.line).unwrap_or(0);
686 self.cursor.position.col = target_col.min(line_len);
687 }
688 }
689
690 fn move_down_no_clear(&mut self) {
692 if self.cursor.position.line + 1 < self.buffer.line_count() {
693 let target_col = self
694 .cursor
695 .preferred_col
696 .unwrap_or(self.cursor.position.col);
697 self.cursor.preferred_col = Some(target_col);
698 self.cursor.position.line += 1;
699 let line_len = self.buffer.line_len(self.cursor.position.line).unwrap_or(0);
700 self.cursor.position.col = target_col.min(line_len);
701 }
702 }
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708 use crate::geometry::Size;
709
710 #[test]
713 fn empty_textarea_renders() {
714 let ta = TextArea::new();
715 let mut buf = ScreenBuffer::new(Size::new(20, 5));
716 ta.render(Rect::new(0, 0, 20, 5), &mut buf);
717 }
719
720 #[test]
721 fn text_renders_correctly() {
722 let ta = TextArea::from_text("hello");
723 let mut buf = ScreenBuffer::new(Size::new(20, 5));
724 ta.render(Rect::new(0, 0, 20, 5), &mut buf);
725 assert!(buf.get(0, 0).map(|c| c.grapheme.as_str()) == Some("h"));
726 assert!(buf.get(4, 0).map(|c| c.grapheme.as_str()) == Some("o"));
727 }
728
729 #[test]
730 fn line_numbers_displayed() {
731 let ta = TextArea::from_text("line1\nline2\nline3").with_line_numbers(true);
732 let mut buf = ScreenBuffer::new(Size::new(20, 5));
733 ta.render(Rect::new(0, 0, 20, 5), &mut buf);
734 assert!(buf.get(0, 0).map(|c| c.grapheme.as_str()) == Some("1"));
736 }
737
738 #[test]
739 fn soft_wrap_splits_long_line() {
740 let ta = TextArea::from_text("abcdefghij");
741 let mut buf = ScreenBuffer::new(Size::new(5, 5));
742 ta.render(Rect::new(0, 0, 5, 5), &mut buf);
743 assert!(buf.get(0, 0).map(|c| c.grapheme.as_str()) == Some("a"));
745 assert!(buf.get(4, 0).map(|c| c.grapheme.as_str()) == Some("e"));
746 assert!(buf.get(0, 1).map(|c| c.grapheme.as_str()) == Some("f"));
748 }
749
750 #[test]
751 fn cursor_visible_at_position() {
752 let ta = TextArea::from_text("hello");
753 let mut buf = ScreenBuffer::new(Size::new(20, 5));
754 ta.render(Rect::new(0, 0, 20, 5), &mut buf);
755 let cell = buf.get(0, 0);
757 assert!(cell.is_some());
758 assert!(cell.map(|c| c.style.reverse) == Some(true));
759 }
760
761 #[test]
762 fn scroll_offset_hides_top_lines() {
763 let mut ta = TextArea::from_text("line1\nline2\nline3\nline4");
764 ta.scroll_offset = 2;
765 let mut buf = ScreenBuffer::new(Size::new(20, 2));
766 ta.render(Rect::new(0, 0, 20, 2), &mut buf);
767 assert!(buf.get(0, 0).map(|c| c.grapheme.as_str()) == Some("l"));
769 assert!(buf.get(4, 0).map(|c| c.grapheme.as_str()) == Some("3"));
770 }
771
772 #[test]
775 fn insert_char_updates_buffer_and_cursor() {
776 let mut ta = TextArea::new();
777 ta.insert_char('a');
778 assert!(ta.text() == "a");
779 assert!(ta.cursor.position.col == 1);
780 }
781
782 #[test]
783 fn insert_at_middle_of_line() {
784 let mut ta = TextArea::from_text("ac");
785 ta.cursor.position.col = 1;
786 ta.insert_char('b');
787 assert!(ta.text() == "abc");
788 }
789
790 #[test]
791 fn backspace_at_start_joins_lines() {
792 let mut ta = TextArea::from_text("ab\ncd");
793 ta.cursor.position = CursorPosition::new(1, 0);
794 ta.delete_backward();
795 assert!(ta.text() == "abcd");
796 assert!(ta.cursor.position.line == 0);
797 assert!(ta.cursor.position.col == 2);
798 }
799
800 #[test]
801 fn delete_at_end_joins_lines() {
802 let mut ta = TextArea::from_text("ab\ncd");
803 ta.cursor.position = CursorPosition::new(0, 2);
804 ta.delete_forward();
805 assert!(ta.text() == "abcd");
806 }
807
808 #[test]
809 fn undo_reverses_insert() {
810 let mut ta = TextArea::new();
811 ta.insert_char('x');
812 assert!(ta.text() == "x");
813 ta.undo();
814 assert!(ta.text().is_empty());
815 }
816
817 #[test]
818 fn redo_reapplies() {
819 let mut ta = TextArea::new();
820 ta.insert_char('x');
821 ta.undo();
822 ta.redo();
823 assert!(ta.text() == "x");
824 }
825
826 #[test]
827 fn selection_delete_removes_text() {
828 let mut ta = TextArea::from_text("hello world");
829 ta.cursor.selection = Some(Selection::new(
830 CursorPosition::new(0, 5),
831 CursorPosition::new(0, 11),
832 ));
833 ta.cursor.position = CursorPosition::new(0, 11);
834 let deleted = ta.delete_selection();
835 assert!(deleted);
836 assert!(ta.text() == "hello");
837 }
838
839 #[test]
840 fn enter_splits_line() {
841 let mut ta = TextArea::from_text("helloworld");
842 ta.cursor.position.col = 5;
843 ta.new_line();
844 assert!(ta.buffer.line_count() == 2);
845 match ta.buffer.line(0) {
846 Some(ref s) if s == "hello" => {}
847 other => unreachable!("expected 'hello', got {other:?}"),
848 }
849 }
850
851 #[test]
852 fn ensure_cursor_visible_scrolls_down() {
853 let mut ta = TextArea::from_text("a\nb\nc\nd\ne\nf");
854 ta.cursor.position = CursorPosition::new(5, 0);
855 ta.ensure_cursor_visible(3);
856 assert!(ta.scroll_offset == 3);
857 }
858
859 #[test]
860 fn ensure_cursor_visible_scrolls_up() {
861 let mut ta = TextArea::from_text("a\nb\nc\nd\ne\nf");
862 ta.scroll_offset = 4;
863 ta.cursor.position = CursorPosition::new(1, 0);
864 ta.ensure_cursor_visible(3);
865 assert!(ta.scroll_offset == 1);
866 }
867
868 #[test]
869 fn handle_event_char_input() {
870 let mut ta = TextArea::new();
871 let event = Event::Key(KeyEvent {
872 code: KeyCode::Char('a'),
873 modifiers: Modifiers::NONE,
874 });
875 let result = ta.handle_event(&event);
876 assert!(result == EventResult::Consumed);
877 assert!(ta.text() == "a");
878 }
879
880 #[test]
881 fn handle_event_ctrl_z_undoes() {
882 let mut ta = TextArea::new();
883 ta.insert_char('x');
884 let event = Event::Key(KeyEvent {
885 code: KeyCode::Char('z'),
886 modifiers: Modifiers::CTRL,
887 });
888 ta.handle_event(&event);
889 assert!(ta.text().is_empty());
890 }
891}