tui_textarea/textarea.rs
1use crate::cursor::CursorMove;
2use crate::highlight::LineHighlighter;
3use crate::history::{Edit, EditKind, History};
4use crate::input::{Input, Key};
5use crate::ratatui::layout::Alignment;
6use crate::ratatui::style::{Color, Modifier, Style};
7use crate::ratatui::widgets::{Block, Widget};
8use crate::scroll::Scrolling;
9#[cfg(feature = "search")]
10use crate::search::Search;
11use crate::util::{spaces, Pos};
12use crate::widget::Viewport;
13use crate::word::{find_word_exclusive_end_forward, find_word_start_backward};
14#[cfg(feature = "ratatui")]
15use ratatui::text::Line;
16use std::cmp::Ordering;
17use std::fmt;
18#[cfg(feature = "tuirs")]
19use tui::text::Spans as Line;
20use unicode_width::UnicodeWidthChar as _;
21
22#[derive(Debug, Clone)]
23enum YankText {
24 Piece(String),
25 Chunk(Vec<String>),
26}
27
28impl Default for YankText {
29 fn default() -> Self {
30 Self::Piece(String::new())
31 }
32}
33
34impl From<String> for YankText {
35 fn from(s: String) -> Self {
36 Self::Piece(s)
37 }
38}
39impl From<Vec<String>> for YankText {
40 fn from(mut c: Vec<String>) -> Self {
41 match c.len() {
42 0 => Self::default(),
43 1 => Self::Piece(c.remove(0)),
44 _ => Self::Chunk(c),
45 }
46 }
47}
48
49impl fmt::Display for YankText {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::Piece(s) => write!(f, "{}", s),
53 Self::Chunk(ss) => write!(f, "{}", ss.join("\n")),
54 }
55 }
56}
57
58/// A type to manage state of textarea. These are some important methods:
59///
60/// - [`TextArea::default`] creates an empty textarea.
61/// - [`TextArea::new`] creates a textarea with given text lines.
62/// - [`TextArea::from`] creates a textarea from an iterator of lines.
63/// - [`TextArea::input`] handles key input.
64/// - [`TextArea::lines`] returns line texts.
65/// ```
66/// use tui_textarea::{TextArea, Input, Key};
67/// use ratatui::backend::CrosstermBackend;
68/// use ratatui::layout::{Constraint, Direction, Layout};
69/// use ratatui::Terminal;
70///
71/// let mut textarea = TextArea::default();
72///
73/// // Input 'a'
74/// let input = Input { key: Key::Char('a'), ctrl: false, alt: false, shift: false };
75/// textarea.input(input);
76///
77/// // Get lines as String.
78/// println!("Lines: {:?}", textarea.lines());
79/// ```
80///
81/// It implements [`ratatui::widgets::Widget`] trait so it can be rendered to a terminal screen via
82/// [`ratatui::Frame::render_widget`] method.
83/// ```no_run
84/// use ratatui::backend::CrosstermBackend;
85/// use ratatui::layout::{Constraint, Direction, Layout};
86/// use ratatui::Terminal;
87/// use tui_textarea::TextArea;
88///
89/// let mut textarea = TextArea::default();
90///
91/// let layout = Layout::default()
92/// .direction(Direction::Vertical)
93/// .constraints([Constraint::Min(1)].as_ref());
94/// let backend = CrosstermBackend::new(std::io::stdout());
95/// let mut term = Terminal::new(backend).unwrap();
96///
97/// loop {
98/// term.draw(|f| {
99/// let chunks = layout.split(f.area());
100/// f.render_widget(&textarea, chunks[0]);
101/// }).unwrap();
102///
103/// // ...
104/// }
105/// ```
106#[derive(Clone, Debug)]
107pub struct TextArea<'a> {
108 lines: Vec<String>,
109 block: Option<Block<'a>>,
110 style: Style,
111 cursor: (usize, usize), // 0-base
112 tab_len: u8,
113 hard_tab_indent: bool,
114 history: History,
115 cursor_line_style: Style,
116 line_number_style: Option<Style>,
117 pub(crate) viewport: Viewport,
118 pub(crate) cursor_style: Style,
119 yank: YankText,
120 #[cfg(feature = "search")]
121 search: Search,
122 alignment: Alignment,
123 pub(crate) placeholder: String,
124 pub(crate) placeholder_style: Style,
125 mask: Option<char>,
126 selection_start: Option<(usize, usize)>,
127 select_style: Style,
128}
129
130/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
131/// handled as line. Ensure that the strings don't contain any newlines. This method is useful to create [`TextArea`]
132/// from [`std::str::Lines`].
133/// ```
134/// use tui_textarea::TextArea;
135///
136/// // From `String`
137/// let text = "hello\nworld";
138/// let textarea = TextArea::from(text.lines());
139/// assert_eq!(textarea.lines(), ["hello", "world"]);
140///
141/// // From array of `&str`
142/// let textarea = TextArea::from(["hello", "world"]);
143/// assert_eq!(textarea.lines(), ["hello", "world"]);
144///
145/// // From slice of `&str`
146/// let slice = &["hello", "world"];
147/// let textarea = TextArea::from(slice.iter().copied());
148/// assert_eq!(textarea.lines(), ["hello", "world"]);
149/// ```
150impl<'a, I> From<I> for TextArea<'a>
151where
152 I: IntoIterator,
153 I::Item: Into<String>,
154{
155 fn from(i: I) -> Self {
156 Self::new(i.into_iter().map(|s| s.into()).collect::<Vec<String>>())
157 }
158}
159
160/// Collect line texts from iterator as [`TextArea`]. It is useful when creating a textarea with text read from a file.
161/// [`Iterator::collect`] handles errors which may happen on reading each lines. The following example reads text from
162/// a file efficiently line-by-line.
163/// ```
164/// use std::fs;
165/// use std::io::{self, BufRead};
166/// use std::path::Path;
167/// use tui_textarea::TextArea;
168///
169/// fn read_from_file<'a>(path: impl AsRef<Path>) -> io::Result<TextArea<'a>> {
170/// let file = fs::File::open(path)?;
171/// io::BufReader::new(file).lines().collect()
172/// }
173///
174/// let textarea = read_from_file("README.md").unwrap();
175/// assert!(!textarea.is_empty());
176/// ```
177impl<'a, S: Into<String>> FromIterator<S> for TextArea<'a> {
178 fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
179 iter.into()
180 }
181}
182
183/// Create [`TextArea`] instance with empty text content.
184/// ```
185/// use tui_textarea::TextArea;
186///
187/// let textarea = TextArea::default();
188/// assert_eq!(textarea.lines(), [""]);
189/// assert!(textarea.is_empty());
190/// ```
191impl<'a> Default for TextArea<'a> {
192 fn default() -> Self {
193 Self::new(vec![String::new()])
194 }
195}
196
197impl<'a> TextArea<'a> {
198 /// Create [`TextArea`] instance with given lines. If you have value other than `Vec<String>`, [`TextArea::from`]
199 /// may be more useful.
200 /// ```
201 /// use tui_textarea::TextArea;
202 ///
203 /// let lines = vec!["hello".to_string(), "...".to_string(), "goodbye".to_string()];
204 /// let textarea = TextArea::new(lines);
205 /// assert_eq!(textarea.lines(), ["hello", "...", "goodbye"]);
206 /// ```
207 pub fn new(mut lines: Vec<String>) -> Self {
208 if lines.is_empty() {
209 lines.push(String::new());
210 }
211
212 Self {
213 lines,
214 block: None,
215 style: Style::default(),
216 cursor: (0, 0),
217 tab_len: 4,
218 hard_tab_indent: false,
219 history: History::new(50),
220 cursor_line_style: Style::default().add_modifier(Modifier::UNDERLINED),
221 line_number_style: None,
222 viewport: Viewport::default(),
223 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
224 yank: YankText::default(),
225 #[cfg(feature = "search")]
226 search: Search::default(),
227 alignment: Alignment::Left,
228 placeholder: String::new(),
229 placeholder_style: Style::default().fg(Color::DarkGray),
230 mask: None,
231 selection_start: None,
232 select_style: Style::default().bg(Color::LightBlue),
233 }
234 }
235
236 /// Handle a key input with default key mappings. For default key mappings, see the table in
237 /// [the module document](./index.html).
238 /// `crossterm`, `termion`, and `termwiz` features enable conversion from their own key event types into
239 /// [`Input`] so this method can take the event values directly.
240 /// This method returns if the input modified text contents or not in the textarea.
241 /// ```ignore
242 /// use tui_textarea::{TextArea, Key, Input};
243 ///
244 /// let mut textarea = TextArea::default();
245 ///
246 /// // Handle crossterm key events
247 /// let event: crossterm::event::Event = ...;
248 /// textarea.input(event);
249 /// if let crossterm::event::Event::Key(key) = event {
250 /// textarea.input(key);
251 /// }
252 ///
253 /// // Handle termion key events
254 /// let event: termion::event::Event = ...;
255 /// textarea.input(event);
256 /// if let termion::event::Event::Key(key) = event {
257 /// textarea.input(key);
258 /// }
259 ///
260 /// // Handle termwiz key events
261 /// let event: termwiz::input::InputEvent = ...;
262 /// textarea.input(event);
263 /// if let termwiz::input::InputEvent::Key(key) = event {
264 /// textarea.input(key);
265 /// }
266 ///
267 /// // Handle backend-agnostic key input
268 /// let input = Input { key: Key::Char('a'), ctrl: false, alt: false, shift: false };
269 /// let modified = textarea.input(input);
270 /// assert!(modified);
271 /// ```
272 pub fn input(&mut self, input: impl Into<Input>) -> bool {
273 let input = input.into();
274 let modified = match input {
275 Input {
276 key: Key::Char('m'),
277 ctrl: true,
278 alt: false,
279 ..
280 }
281 | Input {
282 key: Key::Char('\n' | '\r'),
283 ctrl: false,
284 alt: false,
285 ..
286 }
287 | Input {
288 key: Key::Enter, ..
289 } => {
290 self.insert_newline();
291 true
292 }
293 Input {
294 key: Key::Char(c),
295 ctrl: false,
296 alt: false,
297 ..
298 } => {
299 self.insert_char(c);
300 true
301 }
302 Input {
303 key: Key::Tab,
304 ctrl: false,
305 alt: false,
306 ..
307 } => self.insert_tab(),
308 Input {
309 key: Key::Char('h'),
310 ctrl: true,
311 alt: false,
312 ..
313 }
314 | Input {
315 key: Key::Backspace,
316 ctrl: false,
317 alt: false,
318 ..
319 } => self.delete_char(),
320 Input {
321 key: Key::Char('d'),
322 ctrl: true,
323 alt: false,
324 ..
325 }
326 | Input {
327 key: Key::Delete,
328 ctrl: false,
329 alt: false,
330 ..
331 } => self.delete_next_char(),
332 Input {
333 key: Key::Char('k'),
334 ctrl: true,
335 alt: false,
336 ..
337 } => self.delete_line_by_end(),
338 Input {
339 key: Key::Char('j'),
340 ctrl: true,
341 alt: false,
342 ..
343 } => self.delete_line_by_head(),
344 Input {
345 key: Key::Char('w'),
346 ctrl: true,
347 alt: false,
348 ..
349 }
350 | Input {
351 key: Key::Char('h'),
352 ctrl: false,
353 alt: true,
354 ..
355 }
356 | Input {
357 key: Key::Backspace,
358 ctrl: false,
359 alt: true,
360 ..
361 } => self.delete_word(),
362 Input {
363 key: Key::Delete,
364 ctrl: false,
365 alt: true,
366 ..
367 }
368 | Input {
369 key: Key::Char('d'),
370 ctrl: false,
371 alt: true,
372 ..
373 } => self.delete_next_word(),
374 Input {
375 key: Key::Char('n'),
376 ctrl: true,
377 alt: false,
378 shift,
379 }
380 | Input {
381 key: Key::Down,
382 ctrl: false,
383 alt: false,
384 shift,
385 } => {
386 self.move_cursor_with_shift(CursorMove::Down, shift);
387 false
388 }
389 Input {
390 key: Key::Char('p'),
391 ctrl: true,
392 alt: false,
393 shift,
394 }
395 | Input {
396 key: Key::Up,
397 ctrl: false,
398 alt: false,
399 shift,
400 } => {
401 self.move_cursor_with_shift(CursorMove::Up, shift);
402 false
403 }
404 Input {
405 key: Key::Char('f'),
406 ctrl: true,
407 alt: false,
408 shift,
409 }
410 | Input {
411 key: Key::Right,
412 ctrl: false,
413 alt: false,
414 shift,
415 } => {
416 self.move_cursor_with_shift(CursorMove::Forward, shift);
417 false
418 }
419 Input {
420 key: Key::Char('b'),
421 ctrl: true,
422 alt: false,
423 shift,
424 }
425 | Input {
426 key: Key::Left,
427 ctrl: false,
428 alt: false,
429 shift,
430 } => {
431 self.move_cursor_with_shift(CursorMove::Back, shift);
432 false
433 }
434 Input {
435 key: Key::Char('a'),
436 ctrl: true,
437 alt: false,
438 shift,
439 }
440 | Input {
441 key: Key::Home,
442 shift,
443 ..
444 }
445 | Input {
446 key: Key::Left | Key::Char('b'),
447 ctrl: true,
448 alt: true,
449 shift,
450 } => {
451 self.move_cursor_with_shift(CursorMove::Head, shift);
452 false
453 }
454 Input {
455 key: Key::Char('e'),
456 ctrl: true,
457 alt: false,
458 shift,
459 }
460 | Input {
461 key: Key::End,
462 shift,
463 ..
464 }
465 | Input {
466 key: Key::Right | Key::Char('f'),
467 ctrl: true,
468 alt: true,
469 shift,
470 } => {
471 self.move_cursor_with_shift(CursorMove::End, shift);
472 false
473 }
474 Input {
475 key: Key::Char('<'),
476 ctrl: false,
477 alt: true,
478 shift,
479 }
480 | Input {
481 key: Key::Up | Key::Char('p'),
482 ctrl: true,
483 alt: true,
484 shift,
485 } => {
486 self.move_cursor_with_shift(CursorMove::Top, shift);
487 false
488 }
489 Input {
490 key: Key::Char('>'),
491 ctrl: false,
492 alt: true,
493 shift,
494 }
495 | Input {
496 key: Key::Down | Key::Char('n'),
497 ctrl: true,
498 alt: true,
499 shift,
500 } => {
501 self.move_cursor_with_shift(CursorMove::Bottom, shift);
502 false
503 }
504 Input {
505 key: Key::Char('f'),
506 ctrl: false,
507 alt: true,
508 shift,
509 }
510 | Input {
511 key: Key::Right,
512 ctrl: true,
513 alt: false,
514 shift,
515 } => {
516 self.move_cursor_with_shift(CursorMove::WordForward, shift);
517 false
518 }
519 Input {
520 key: Key::Char('b'),
521 ctrl: false,
522 alt: true,
523 shift,
524 }
525 | Input {
526 key: Key::Left,
527 ctrl: true,
528 alt: false,
529 shift,
530 } => {
531 self.move_cursor_with_shift(CursorMove::WordBack, shift);
532 false
533 }
534 Input {
535 key: Key::Char(']'),
536 ctrl: false,
537 alt: true,
538 shift,
539 }
540 | Input {
541 key: Key::Char('n'),
542 ctrl: false,
543 alt: true,
544 shift,
545 }
546 | Input {
547 key: Key::Down,
548 ctrl: true,
549 alt: false,
550 shift,
551 } => {
552 self.move_cursor_with_shift(CursorMove::ParagraphForward, shift);
553 false
554 }
555 Input {
556 key: Key::Char('['),
557 ctrl: false,
558 alt: true,
559 shift,
560 }
561 | Input {
562 key: Key::Char('p'),
563 ctrl: false,
564 alt: true,
565 shift,
566 }
567 | Input {
568 key: Key::Up,
569 ctrl: true,
570 alt: false,
571 shift,
572 } => {
573 self.move_cursor_with_shift(CursorMove::ParagraphBack, shift);
574 false
575 }
576 Input {
577 key: Key::Char('u'),
578 ctrl: true,
579 alt: false,
580 ..
581 } => self.undo(),
582 Input {
583 key: Key::Char('r'),
584 ctrl: true,
585 alt: false,
586 ..
587 } => self.redo(),
588 Input {
589 key: Key::Char('y'),
590 ctrl: true,
591 alt: false,
592 ..
593 }
594 | Input {
595 key: Key::Paste, ..
596 } => self.paste(),
597 Input {
598 key: Key::Char('x'),
599 ctrl: true,
600 alt: false,
601 ..
602 }
603 | Input { key: Key::Cut, .. } => self.cut(),
604 Input {
605 key: Key::Char('c'),
606 ctrl: true,
607 alt: false,
608 ..
609 }
610 | Input { key: Key::Copy, .. } => {
611 self.copy();
612 false
613 }
614 Input {
615 key: Key::Char('v'),
616 ctrl: true,
617 alt: false,
618 shift,
619 }
620 | Input {
621 key: Key::PageDown,
622 shift,
623 ..
624 } => {
625 self.scroll_with_shift(Scrolling::PageDown, shift);
626 false
627 }
628 Input {
629 key: Key::Char('v'),
630 ctrl: false,
631 alt: true,
632 shift,
633 }
634 | Input {
635 key: Key::PageUp,
636 shift,
637 ..
638 } => {
639 self.scroll_with_shift(Scrolling::PageUp, shift);
640 false
641 }
642 Input {
643 key: Key::MouseScrollDown,
644 shift,
645 ..
646 } => {
647 self.scroll_with_shift((1, 0).into(), shift);
648 false
649 }
650 Input {
651 key: Key::MouseScrollUp,
652 shift,
653 ..
654 } => {
655 self.scroll_with_shift((-1, 0).into(), shift);
656 false
657 }
658 _ => false,
659 };
660
661 // Check invariants
662 debug_assert!(!self.lines.is_empty(), "no line after {:?}", input);
663 let (r, c) = self.cursor;
664 debug_assert!(
665 self.lines.len() > r,
666 "cursor {:?} exceeds max lines {} after {:?}",
667 self.cursor,
668 self.lines.len(),
669 input,
670 );
671 debug_assert!(
672 self.lines[r].chars().count() >= c,
673 "cursor {:?} exceeds max col {} at line {:?} after {:?}",
674 self.cursor,
675 self.lines[r].chars().count(),
676 self.lines[r],
677 input,
678 );
679
680 modified
681 }
682
683 /// Handle a key input without default key mappings. This method handles only
684 ///
685 /// - Single character input without modifier keys
686 /// - Tab
687 /// - Enter
688 /// - Backspace
689 /// - Delete
690 ///
691 /// This method returns if the input modified text contents or not in the textarea.
692 ///
693 /// This method is useful when you want to define your own key mappings and don't want default key mappings.
694 /// See 'Define your own key mappings' section in [the module document](./index.html).
695 pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool {
696 match input.into() {
697 Input {
698 key: Key::Char(c),
699 ctrl: false,
700 alt: false,
701 ..
702 } => {
703 self.insert_char(c);
704 true
705 }
706 Input {
707 key: Key::Tab,
708 ctrl: false,
709 alt: false,
710 ..
711 } => self.insert_tab(),
712 Input {
713 key: Key::Backspace,
714 ..
715 } => self.delete_char(),
716 Input {
717 key: Key::Delete, ..
718 } => self.delete_next_char(),
719 Input {
720 key: Key::Enter, ..
721 } => {
722 self.insert_newline();
723 true
724 }
725 Input {
726 key: Key::MouseScrollDown,
727 ..
728 } => {
729 self.scroll((1, 0));
730 false
731 }
732 Input {
733 key: Key::MouseScrollUp,
734 ..
735 } => {
736 self.scroll((-1, 0));
737 false
738 }
739 _ => false,
740 }
741 }
742
743 fn push_history(&mut self, kind: EditKind, before: Pos, after_offset: usize) {
744 let (row, col) = self.cursor;
745 let after = Pos::new(row, col, after_offset);
746 let edit = Edit::new(kind, before, after);
747 self.history.push(edit);
748 }
749
750 /// Insert a single character at current cursor position.
751 /// ```
752 /// use tui_textarea::TextArea;
753 ///
754 /// let mut textarea = TextArea::default();
755 ///
756 /// textarea.insert_char('a');
757 /// assert_eq!(textarea.lines(), ["a"]);
758 /// ```
759 pub fn insert_char(&mut self, c: char) {
760 if c == '\n' || c == '\r' {
761 self.insert_newline();
762 return;
763 }
764
765 self.delete_selection(false);
766 let (row, col) = self.cursor;
767 let line = &mut self.lines[row];
768 let i = line
769 .char_indices()
770 .nth(col)
771 .map(|(i, _)| i)
772 .unwrap_or(line.len());
773 line.insert(i, c);
774 self.cursor.1 += 1;
775 self.push_history(
776 EditKind::InsertChar(c),
777 Pos::new(row, col, i),
778 i + c.len_utf8(),
779 );
780 }
781
782 /// Insert a string at current cursor position. This method returns if some text was inserted or not in the textarea.
783 /// Both `\n` and `\r\n` are recognized as newlines but `\r` isn't.
784 /// ```
785 /// use tui_textarea::TextArea;
786 ///
787 /// let mut textarea = TextArea::default();
788 ///
789 /// textarea.insert_str("hello");
790 /// assert_eq!(textarea.lines(), ["hello"]);
791 ///
792 /// textarea.insert_str(", world\ngoodbye, world");
793 /// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]);
794 /// ```
795 pub fn insert_str<S: AsRef<str>>(&mut self, s: S) -> bool {
796 let modified = self.delete_selection(false);
797 let mut lines: Vec<_> = s
798 .as_ref()
799 .split('\n')
800 .map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
801 .collect();
802 match lines.len() {
803 0 => modified,
804 1 => self.insert_piece(lines.remove(0)),
805 _ => self.insert_chunk(lines),
806 }
807 }
808
809 fn insert_chunk(&mut self, chunk: Vec<String>) -> bool {
810 debug_assert!(chunk.len() > 1, "Chunk size must be > 1: {:?}", chunk);
811
812 let (row, col) = self.cursor;
813 let line = &mut self.lines[row];
814 let i = line
815 .char_indices()
816 .nth(col)
817 .map(|(i, _)| i)
818 .unwrap_or(line.len());
819 let before = Pos::new(row, col, i);
820
821 let (row, col) = (
822 row + chunk.len() - 1,
823 chunk[chunk.len() - 1].chars().count(),
824 );
825 self.cursor = (row, col);
826
827 let end_offset = chunk.last().unwrap().len();
828
829 let edit = EditKind::InsertChunk(chunk);
830 edit.apply(&mut self.lines, &before, &Pos::new(row, col, end_offset));
831
832 self.push_history(edit, before, end_offset);
833 true
834 }
835
836 fn insert_piece(&mut self, s: String) -> bool {
837 if s.is_empty() {
838 return false;
839 }
840
841 let (row, col) = self.cursor;
842 let line = &mut self.lines[row];
843 debug_assert!(
844 !s.contains('\n'),
845 "string given to TextArea::insert_piece must not contain newline: {:?}",
846 line,
847 );
848
849 let i = line
850 .char_indices()
851 .nth(col)
852 .map(|(i, _)| i)
853 .unwrap_or(line.len());
854 line.insert_str(i, &s);
855 let end_offset = i + s.len();
856
857 self.cursor.1 += s.chars().count();
858 self.push_history(EditKind::InsertStr(s), Pos::new(row, col, i), end_offset);
859 true
860 }
861
862 fn delete_range(&mut self, start: Pos, end: Pos, should_yank: bool) {
863 self.cursor = (start.row, start.col);
864
865 if start.row == end.row {
866 let removed = self.lines[start.row]
867 .drain(start.offset..end.offset)
868 .as_str()
869 .to_string();
870 if should_yank {
871 self.yank = removed.clone().into();
872 }
873 self.push_history(EditKind::DeleteStr(removed), end, start.offset);
874 return;
875 }
876
877 let mut deleted = vec![self.lines[start.row]
878 .drain(start.offset..)
879 .as_str()
880 .to_string()];
881 deleted.extend(self.lines.drain(start.row + 1..end.row));
882 if start.row + 1 < self.lines.len() {
883 let mut last_line = self.lines.remove(start.row + 1);
884 self.lines[start.row].push_str(&last_line[end.offset..]);
885 last_line.truncate(end.offset);
886 deleted.push(last_line);
887 }
888
889 if should_yank {
890 self.yank = YankText::Chunk(deleted.clone());
891 }
892
893 let edit = if deleted.len() == 1 {
894 EditKind::DeleteStr(deleted.remove(0))
895 } else {
896 EditKind::DeleteChunk(deleted)
897 };
898 self.push_history(edit, end, start.offset);
899 }
900
901 /// Delete a string from the current cursor position. The `chars` parameter means number of characters, not a byte
902 /// length of the string. Newlines at the end of lines are counted in the number. This method returns if some text
903 /// was deleted or not.
904 /// ```
905 /// use tui_textarea::{TextArea, CursorMove};
906 ///
907 /// let mut textarea = TextArea::from(["🐱🐶🐰🐮"]);
908 /// textarea.move_cursor(CursorMove::Forward);
909 ///
910 /// textarea.delete_str(2);
911 /// assert_eq!(textarea.lines(), ["🐱🐮"]);
912 ///
913 /// let mut textarea = TextArea::from(["🐱", "🐶", "🐰", "🐮"]);
914 /// textarea.move_cursor(CursorMove::Down);
915 ///
916 /// textarea.delete_str(4); // Deletes 🐶, \n, 🐰, \n
917 /// assert_eq!(textarea.lines(), ["🐱", "🐮"]);
918 /// ```
919 pub fn delete_str(&mut self, chars: usize) -> bool {
920 if self.delete_selection(false) {
921 return true;
922 }
923 if chars == 0 {
924 return false;
925 }
926
927 let (start_row, start_col) = self.cursor;
928
929 let mut remaining = chars;
930 let mut find_end = move |line: &str| {
931 let mut col = 0usize;
932 for (i, _) in line.char_indices() {
933 if remaining == 0 {
934 return Some((i, col));
935 }
936 col += 1;
937 remaining -= 1;
938 }
939 if remaining == 0 {
940 Some((line.len(), col))
941 } else {
942 remaining -= 1;
943 None
944 }
945 };
946
947 let line = &self.lines[start_row];
948 let start_offset = {
949 line.char_indices()
950 .nth(start_col)
951 .map(|(i, _)| i)
952 .unwrap_or(line.len())
953 };
954
955 // First line
956 if let Some((offset_delta, col_delta)) = find_end(&line[start_offset..]) {
957 let end_offset = start_offset + offset_delta;
958 let end_col = start_col + col_delta;
959 let removed = self.lines[start_row]
960 .drain(start_offset..end_offset)
961 .as_str()
962 .to_string();
963 self.yank = removed.clone().into();
964 self.push_history(
965 EditKind::DeleteStr(removed),
966 Pos::new(start_row, end_col, end_offset),
967 start_offset,
968 );
969 return true;
970 }
971
972 let mut r = start_row + 1;
973 let mut offset = 0;
974 let mut col = 0;
975
976 while r < self.lines.len() {
977 let line = &self.lines[r];
978 if let Some((o, c)) = find_end(line) {
979 offset = o;
980 col = c;
981 break;
982 }
983 r += 1;
984 }
985
986 let start = Pos::new(start_row, start_col, start_offset);
987 let end = Pos::new(r, col, offset);
988 self.delete_range(start, end, true);
989 true
990 }
991
992 fn delete_piece(&mut self, col: usize, chars: usize) -> bool {
993 if chars == 0 {
994 return false;
995 }
996
997 #[inline]
998 fn bytes_and_chars(claimed: usize, s: &str) -> (usize, usize) {
999 // Note: `claimed` may be larger than characters in `s` (e.g. usize::MAX)
1000 let mut last_col = 0;
1001 for (col, (bytes, _)) in s.char_indices().enumerate() {
1002 if col == claimed {
1003 return (bytes, claimed);
1004 }
1005 last_col = col;
1006 }
1007 (s.len(), last_col + 1)
1008 }
1009
1010 let (row, _) = self.cursor;
1011 let line = &mut self.lines[row];
1012 if let Some((i, _)) = line.char_indices().nth(col) {
1013 let (bytes, chars) = bytes_and_chars(chars, &line[i..]);
1014 let removed = line.drain(i..i + bytes).as_str().to_string();
1015
1016 self.cursor = (row, col);
1017 self.push_history(
1018 EditKind::DeleteStr(removed.clone()),
1019 Pos::new(row, col + chars, i + bytes),
1020 i,
1021 );
1022 self.yank = removed.into();
1023 true
1024 } else {
1025 false
1026 }
1027 }
1028
1029 /// Insert a tab at current cursor position. Note that this method does nothing when the tab length is 0. This
1030 /// method returns if a tab string was inserted or not in the textarea.
1031 /// ```
1032 /// use tui_textarea::{TextArea, CursorMove};
1033 ///
1034 /// let mut textarea = TextArea::from(["hi"]);
1035 ///
1036 /// textarea.move_cursor(CursorMove::End); // Move to the end of line
1037 ///
1038 /// textarea.insert_tab();
1039 /// assert_eq!(textarea.lines(), ["hi "]);
1040 /// textarea.insert_tab();
1041 /// assert_eq!(textarea.lines(), ["hi "]);
1042 /// ```
1043 pub fn insert_tab(&mut self) -> bool {
1044 let modified = self.delete_selection(false);
1045 if self.tab_len == 0 {
1046 return modified;
1047 }
1048
1049 if self.hard_tab_indent {
1050 self.insert_char('\t');
1051 return true;
1052 }
1053
1054 let (row, col) = self.cursor;
1055 let width: usize = self.lines[row]
1056 .chars()
1057 .take(col)
1058 .map(|c| c.width().unwrap_or(0))
1059 .sum();
1060 let len = self.tab_len - (width % self.tab_len as usize) as u8;
1061 self.insert_piece(spaces(len).to_string())
1062 }
1063
1064 /// Insert a newline at current cursor position.
1065 /// ```
1066 /// use tui_textarea::{TextArea, CursorMove};
1067 ///
1068 /// let mut textarea = TextArea::from(["hi"]);
1069 ///
1070 /// textarea.move_cursor(CursorMove::Forward);
1071 /// textarea.insert_newline();
1072 /// assert_eq!(textarea.lines(), ["h", "i"]);
1073 /// ```
1074 pub fn insert_newline(&mut self) {
1075 self.delete_selection(false);
1076
1077 let (row, col) = self.cursor;
1078 let line = &mut self.lines[row];
1079 let offset = line
1080 .char_indices()
1081 .nth(col)
1082 .map(|(i, _)| i)
1083 .unwrap_or(line.len());
1084 let next_line = line[offset..].to_string();
1085 line.truncate(offset);
1086
1087 self.lines.insert(row + 1, next_line);
1088 self.cursor = (row + 1, 0);
1089 self.push_history(EditKind::InsertNewline, Pos::new(row, col, offset), 0);
1090 }
1091
1092 /// Delete a newline from **head** of current cursor line. This method returns if a newline was deleted or not in
1093 /// the textarea. When some text is selected, it is deleted instead.
1094 /// ```
1095 /// use tui_textarea::{TextArea, CursorMove};
1096 ///
1097 /// let mut textarea = TextArea::from(["hello", "world"]);
1098 ///
1099 /// textarea.move_cursor(CursorMove::Down);
1100 /// textarea.delete_newline();
1101 /// assert_eq!(textarea.lines(), ["helloworld"]);
1102 /// ```
1103 pub fn delete_newline(&mut self) -> bool {
1104 if self.delete_selection(false) {
1105 return true;
1106 }
1107
1108 let (row, _) = self.cursor;
1109 if row == 0 {
1110 return false;
1111 }
1112
1113 let line = self.lines.remove(row);
1114 let prev_line = &mut self.lines[row - 1];
1115 let prev_line_end = prev_line.len();
1116
1117 self.cursor = (row - 1, prev_line.chars().count());
1118 prev_line.push_str(&line);
1119 self.push_history(EditKind::DeleteNewline, Pos::new(row, 0, 0), prev_line_end);
1120 true
1121 }
1122
1123 /// Delete one character before cursor. When the cursor is at head of line, the newline before the cursor will be
1124 /// removed. This method returns if some text was deleted or not in the textarea. When some text is selected, it is
1125 /// deleted instead.
1126 /// ```
1127 /// use tui_textarea::{TextArea, CursorMove};
1128 ///
1129 /// let mut textarea = TextArea::from(["abc"]);
1130 ///
1131 /// textarea.move_cursor(CursorMove::Forward);
1132 /// textarea.delete_char();
1133 /// assert_eq!(textarea.lines(), ["bc"]);
1134 /// ```
1135 pub fn delete_char(&mut self) -> bool {
1136 if self.delete_selection(false) {
1137 return true;
1138 }
1139
1140 let (row, col) = self.cursor;
1141 if col == 0 {
1142 return self.delete_newline();
1143 }
1144
1145 let line = &mut self.lines[row];
1146 if let Some((offset, c)) = line.char_indices().nth(col - 1) {
1147 line.remove(offset);
1148 self.cursor.1 -= 1;
1149 self.push_history(
1150 EditKind::DeleteChar(c),
1151 Pos::new(row, col, offset + c.len_utf8()),
1152 offset,
1153 );
1154 true
1155 } else {
1156 false
1157 }
1158 }
1159
1160 /// Delete one character next to cursor. When the cursor is at end of line, the newline next to the cursor will be
1161 /// removed. This method returns if a character was deleted or not in the textarea.
1162 /// ```
1163 /// use tui_textarea::{TextArea, CursorMove};
1164 ///
1165 /// let mut textarea = TextArea::from(["abc"]);
1166 ///
1167 /// textarea.move_cursor(CursorMove::Forward);
1168 /// textarea.delete_next_char();
1169 /// assert_eq!(textarea.lines(), ["ac"]);
1170 /// ```
1171 pub fn delete_next_char(&mut self) -> bool {
1172 if self.delete_selection(false) {
1173 return true;
1174 }
1175
1176 let before = self.cursor;
1177 self.move_cursor_with_shift(CursorMove::Forward, false);
1178 if before == self.cursor {
1179 return false; // Cursor didn't move, meant no character at next of cursor.
1180 }
1181
1182 self.delete_char()
1183 }
1184
1185 /// Delete string from cursor to end of the line. When the cursor is at end of line, the newline next to the cursor
1186 /// is removed. This method returns if some text was deleted or not in the textarea.
1187 /// ```
1188 /// use tui_textarea::{TextArea, CursorMove};
1189 ///
1190 /// let mut textarea = TextArea::from(["abcde"]);
1191 ///
1192 /// // Move to 'c'
1193 /// textarea.move_cursor(CursorMove::Forward);
1194 /// textarea.move_cursor(CursorMove::Forward);
1195 ///
1196 /// textarea.delete_line_by_end();
1197 /// assert_eq!(textarea.lines(), ["ab"]);
1198 /// ```
1199 pub fn delete_line_by_end(&mut self) -> bool {
1200 if self.delete_selection(false) {
1201 return true;
1202 }
1203 if self.delete_piece(self.cursor.1, usize::MAX) {
1204 return true;
1205 }
1206 self.delete_next_char() // At the end of the line. Try to delete next line
1207 }
1208
1209 /// Delete string from cursor to head of the line. When the cursor is at head of line, the newline before the cursor
1210 /// will be removed. This method returns if some text was deleted or not in the textarea.
1211 /// ```
1212 /// use tui_textarea::{TextArea, CursorMove};
1213 ///
1214 /// let mut textarea = TextArea::from(["abcde"]);
1215 ///
1216 /// // Move to 'c'
1217 /// textarea.move_cursor(CursorMove::Forward);
1218 /// textarea.move_cursor(CursorMove::Forward);
1219 ///
1220 /// textarea.delete_line_by_head();
1221 /// assert_eq!(textarea.lines(), ["cde"]);
1222 /// ```
1223 pub fn delete_line_by_head(&mut self) -> bool {
1224 if self.delete_selection(false) {
1225 return true;
1226 }
1227 if self.delete_piece(0, self.cursor.1) {
1228 return true;
1229 }
1230 self.delete_newline()
1231 }
1232
1233 /// Delete a word before cursor. Word boundary appears at spaces, punctuations, and others. For example `fn foo(a)`
1234 /// consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at head of line, the newline before the cursor
1235 /// will be removed.
1236 ///
1237 /// This method returns if some text was deleted or not in the textarea.
1238 ///
1239 /// ```
1240 /// use tui_textarea::{TextArea, CursorMove};
1241 ///
1242 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
1243 ///
1244 /// textarea.move_cursor(CursorMove::End);
1245 ///
1246 /// textarea.delete_word();
1247 /// assert_eq!(textarea.lines(), ["aaa bbb "]);
1248 /// textarea.delete_word();
1249 /// assert_eq!(textarea.lines(), ["aaa "]);
1250 /// ```
1251 pub fn delete_word(&mut self) -> bool {
1252 if self.delete_selection(false) {
1253 return true;
1254 }
1255 let (r, c) = self.cursor;
1256 if let Some(col) = find_word_start_backward(&self.lines[r], c) {
1257 self.delete_piece(col, c - col)
1258 } else if c > 0 {
1259 self.delete_piece(0, c)
1260 } else {
1261 self.delete_newline()
1262 }
1263 }
1264
1265 /// Delete a word next to cursor. Word boundary appears at spaces, punctuations, and others. For example `fn foo(a)`
1266 /// consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at end of line, the newline next to the cursor
1267 /// will be removed.
1268 ///
1269 /// This method returns if some text was deleted or not in the textarea.
1270 ///
1271 /// ```
1272 /// use tui_textarea::TextArea;
1273 ///
1274 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
1275 ///
1276 /// textarea.delete_next_word();
1277 /// assert_eq!(textarea.lines(), [" bbb ccc"]);
1278 /// textarea.delete_next_word();
1279 /// assert_eq!(textarea.lines(), [" ccc"]);
1280 /// ```
1281 pub fn delete_next_word(&mut self) -> bool {
1282 if self.delete_selection(false) {
1283 return true;
1284 }
1285 let (r, c) = self.cursor;
1286 let line = &self.lines[r];
1287 if let Some(col) = find_word_exclusive_end_forward(line, c) {
1288 self.delete_piece(c, col - c)
1289 } else {
1290 let end_col = line.chars().count();
1291 if c < end_col {
1292 self.delete_piece(c, end_col - c)
1293 } else if r + 1 < self.lines.len() {
1294 self.cursor = (r + 1, 0);
1295 self.delete_newline()
1296 } else {
1297 false
1298 }
1299 }
1300 }
1301
1302 /// Paste a string previously deleted by [`TextArea::delete_line_by_head`], [`TextArea::delete_line_by_end`],
1303 /// [`TextArea::delete_word`], [`TextArea::delete_next_word`]. This method returns if some text was inserted or not
1304 /// in the textarea.
1305 /// ```
1306 /// use tui_textarea::{TextArea, CursorMove};
1307 ///
1308 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
1309 ///
1310 /// textarea.delete_next_word();
1311 /// textarea.move_cursor(CursorMove::End);
1312 /// textarea.paste();
1313 /// assert_eq!(textarea.lines(), [" bbb cccaaa"]);
1314 /// ```
1315 pub fn paste(&mut self) -> bool {
1316 self.delete_selection(false);
1317 match self.yank.clone() {
1318 YankText::Piece(s) => self.insert_piece(s),
1319 YankText::Chunk(c) => self.insert_chunk(c),
1320 }
1321 }
1322
1323 /// Start text selection at the cursor position. If text selection is already ongoing, the start position is reset.
1324 /// ```
1325 /// use tui_textarea::{TextArea, CursorMove};
1326 ///
1327 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
1328 ///
1329 /// textarea.start_selection();
1330 /// textarea.move_cursor(CursorMove::WordForward);
1331 /// textarea.copy();
1332 /// assert_eq!(textarea.yank_text(), "aaa ");
1333 /// ```
1334 pub fn start_selection(&mut self) {
1335 self.selection_start = Some(self.cursor);
1336 }
1337
1338 /// Stop the current text selection. This method does nothing if text selection is not ongoing.
1339 /// ```
1340 /// use tui_textarea::{TextArea, CursorMove};
1341 ///
1342 /// let mut textarea = TextArea::from(["aaa bbb ccc"]);
1343 ///
1344 /// textarea.start_selection();
1345 /// textarea.move_cursor(CursorMove::WordForward);
1346 ///
1347 /// // Cancel the ongoing text selection
1348 /// textarea.cancel_selection();
1349 ///
1350 /// // As the result, this `copy` call does nothing
1351 /// textarea.copy();
1352 /// assert_eq!(textarea.yank_text(), "");
1353 /// ```
1354 pub fn cancel_selection(&mut self) {
1355 self.selection_start = None;
1356 }
1357
1358 /// Select the entire text. Cursor moves to the end of the text buffer. When text selection is already ongoing,
1359 /// it is canceled.
1360 /// ```
1361 /// use tui_textarea::{TextArea, CursorMove};
1362 ///
1363 /// let mut textarea = TextArea::from(["aaa", "bbb", "ccc"]);
1364 ///
1365 /// textarea.select_all();
1366 ///
1367 /// // Cut the entire text;
1368 /// textarea.cut();
1369 ///
1370 /// assert_eq!(textarea.lines(), [""]); // Buffer is now empty
1371 /// assert_eq!(textarea.yank_text(), "aaa\nbbb\nccc");
1372 /// ```
1373 pub fn select_all(&mut self) {
1374 self.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
1375 self.selection_start = Some((0, 0));
1376 }
1377
1378 /// Return if text selection is ongoing or not.
1379 /// ```
1380 /// use tui_textarea::{TextArea};
1381 ///
1382 /// let mut textarea = TextArea::default();
1383 ///
1384 /// assert!(!textarea.is_selecting());
1385 /// textarea.start_selection();
1386 /// assert!(textarea.is_selecting());
1387 /// textarea.cancel_selection();
1388 /// assert!(!textarea.is_selecting());
1389 /// ```
1390 pub fn is_selecting(&self) -> bool {
1391 self.selection_start.is_some()
1392 }
1393
1394 fn line_offset(&self, row: usize, col: usize) -> usize {
1395 let line = self
1396 .lines
1397 .get(row)
1398 .unwrap_or(&self.lines[self.lines.len() - 1]);
1399 line.char_indices()
1400 .nth(col)
1401 .map(|(i, _)| i)
1402 .unwrap_or(line.len())
1403 }
1404
1405 /// Set the style used for text selection. The default style is light blue.
1406 /// ```
1407 /// use tui_textarea::TextArea;
1408 /// use ratatui::style::{Style, Color};
1409 ///
1410 /// let mut textarea = TextArea::default();
1411 ///
1412 /// // Change the selection color from the default to Red
1413 /// textarea.set_selection_style(Style::default().bg(Color::Red));
1414 /// assert_eq!(textarea.selection_style(), Style::default().bg(Color::Red));
1415 /// ```
1416 pub fn set_selection_style(&mut self, style: Style) {
1417 self.select_style = style;
1418 }
1419
1420 /// Get the style used for text selection.
1421 /// ```
1422 /// use tui_textarea::TextArea;
1423 /// use ratatui::style::{Style, Color};
1424 ///
1425 /// let mut textarea = TextArea::default();
1426 ///
1427 /// assert_eq!(textarea.selection_style(), Style::default().bg(Color::LightBlue));
1428 /// ```
1429 pub fn selection_style(&mut self) -> Style {
1430 self.select_style
1431 }
1432
1433 fn selection_positions(&self) -> Option<(Pos, Pos)> {
1434 let (sr, sc) = self.selection_start?;
1435 let (er, ec) = self.cursor;
1436 let (so, eo) = (self.line_offset(sr, sc), self.line_offset(er, ec));
1437 let s = Pos::new(sr, sc, so);
1438 let e = Pos::new(er, ec, eo);
1439 match (sr, so).cmp(&(er, eo)) {
1440 Ordering::Less => Some((s, e)),
1441 Ordering::Equal => None,
1442 Ordering::Greater => Some((e, s)),
1443 }
1444 }
1445
1446 fn take_selection_positions(&mut self) -> Option<(Pos, Pos)> {
1447 let range = self.selection_positions();
1448 self.cancel_selection();
1449 range
1450 }
1451
1452 /// Copy the selection text to the yank buffer. When nothing is selected, this method does nothing.
1453 /// To get the yanked text, use [`TextArea::yank_text`].
1454 /// ```
1455 /// use tui_textarea::{TextArea, Key, Input, CursorMove};
1456 ///
1457 /// let mut textarea = TextArea::from(["Hello World"]);
1458 ///
1459 /// // Start text selection at 'W'
1460 /// textarea.move_cursor(CursorMove::WordForward);
1461 /// textarea.start_selection();
1462 ///
1463 /// // Select the word "World" and copy the selected text
1464 /// textarea.move_cursor(CursorMove::End);
1465 /// textarea.copy();
1466 ///
1467 /// assert_eq!(textarea.yank_text(), "World");
1468 /// assert_eq!(textarea.lines(), ["Hello World"]); // Text does not change
1469 /// ```
1470 pub fn copy(&mut self) {
1471 if let Some((start, end)) = self.take_selection_positions() {
1472 if start.row == end.row {
1473 self.yank = self.lines[start.row][start.offset..end.offset]
1474 .to_string()
1475 .into();
1476 return;
1477 }
1478 let mut chunk = vec![self.lines[start.row][start.offset..].to_string()];
1479 chunk.extend(self.lines[start.row + 1..end.row].iter().cloned());
1480 chunk.push(self.lines[end.row][..end.offset].to_string());
1481 self.yank = YankText::Chunk(chunk);
1482 }
1483 }
1484
1485 /// Cut the selected text and place it in the yank buffer. This method returns whether the text was modified.
1486 /// The cursor will move to the start position of the text selection.
1487 /// To get the yanked text, use [`TextArea::yank_text`].
1488 /// ```
1489 /// use tui_textarea::{TextArea, Key, Input, CursorMove};
1490 ///
1491 /// let mut textarea = TextArea::from(["Hello World"]);
1492 ///
1493 /// // Start text selection at 'W'
1494 /// textarea.move_cursor(CursorMove::WordForward);
1495 /// textarea.start_selection();
1496 ///
1497 /// // Select the word "World" and copy the selected text
1498 /// textarea.move_cursor(CursorMove::End);
1499 /// textarea.cut();
1500 ///
1501 /// assert_eq!(textarea.yank_text(), "World");
1502 /// assert_eq!(textarea.lines(), ["Hello "]);
1503 /// ```
1504 pub fn cut(&mut self) -> bool {
1505 self.delete_selection(true)
1506 }
1507
1508 fn delete_selection(&mut self, should_yank: bool) -> bool {
1509 if let Some((s, e)) = self.take_selection_positions() {
1510 self.delete_range(s, e, should_yank);
1511 return true;
1512 }
1513 false
1514 }
1515
1516 /// Move the cursor to the position specified by the [`CursorMove`] parameter. For each kind of cursor moves, see
1517 /// the document of [`CursorMove`].
1518 /// ```
1519 /// use tui_textarea::{TextArea, CursorMove};
1520 ///
1521 /// let mut textarea = TextArea::from(["abc", "def"]);
1522 ///
1523 /// textarea.move_cursor(CursorMove::Forward);
1524 /// assert_eq!(textarea.cursor(), (0, 1));
1525 /// textarea.move_cursor(CursorMove::Down);
1526 /// assert_eq!(textarea.cursor(), (1, 1));
1527 /// ```
1528 pub fn move_cursor(&mut self, m: CursorMove) {
1529 self.move_cursor_with_shift(m, self.selection_start.is_some());
1530 }
1531
1532 fn move_cursor_with_shift(&mut self, m: CursorMove, shift: bool) {
1533 if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
1534 if shift {
1535 if self.selection_start.is_none() {
1536 self.start_selection();
1537 }
1538 } else {
1539 self.cancel_selection();
1540 }
1541 self.cursor = cursor;
1542 }
1543 }
1544
1545 /// Undo the last modification. This method returns if the undo modified text contents or not in the textarea.
1546 /// ```
1547 /// use tui_textarea::{TextArea, CursorMove};
1548 ///
1549 /// let mut textarea = TextArea::from(["abc def"]);
1550 ///
1551 /// textarea.delete_next_word();
1552 /// assert_eq!(textarea.lines(), [" def"]);
1553 /// textarea.undo();
1554 /// assert_eq!(textarea.lines(), ["abc def"]);
1555 /// ```
1556 pub fn undo(&mut self) -> bool {
1557 if let Some(cursor) = self.history.undo(&mut self.lines) {
1558 self.cancel_selection();
1559 self.cursor = cursor;
1560 true
1561 } else {
1562 false
1563 }
1564 }
1565
1566 /// Redo the last undo change. This method returns if the redo modified text contents or not in the textarea.
1567 /// ```
1568 /// use tui_textarea::{TextArea, CursorMove};
1569 ///
1570 /// let mut textarea = TextArea::from(["abc def"]);
1571 ///
1572 /// textarea.delete_next_word();
1573 /// assert_eq!(textarea.lines(), [" def"]);
1574 /// textarea.undo();
1575 /// assert_eq!(textarea.lines(), ["abc def"]);
1576 /// textarea.redo();
1577 /// assert_eq!(textarea.lines(), [" def"]);
1578 /// ```
1579 pub fn redo(&mut self) -> bool {
1580 if let Some(cursor) = self.history.redo(&mut self.lines) {
1581 self.cancel_selection();
1582 self.cursor = cursor;
1583 true
1584 } else {
1585 false
1586 }
1587 }
1588
1589 pub(crate) fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Line<'b> {
1590 let mut hl = LineHighlighter::new(
1591 line,
1592 self.cursor_style,
1593 self.tab_len,
1594 self.mask,
1595 self.select_style,
1596 );
1597
1598 if let Some(style) = self.line_number_style {
1599 hl.line_number(row, lnum_len, style);
1600 }
1601
1602 if row == self.cursor.0 {
1603 hl.cursor_line(self.cursor.1, self.cursor_line_style);
1604 }
1605
1606 #[cfg(feature = "search")]
1607 if let Some(matches) = self.search.matches(line) {
1608 hl.search(matches, self.search.style);
1609 }
1610
1611 if let Some((start, end)) = self.selection_positions() {
1612 hl.selection(row, start.row, start.offset, end.row, end.offset);
1613 }
1614
1615 hl.into_spans()
1616 }
1617
1618 /// Build a ratatui (or tui-rs) widget to render the current state of the textarea. The widget instance returned
1619 /// from this method can be rendered with [`ratatui::Frame::render_widget`].
1620 ///
1621 /// This method was deprecated at v0.5.3 and is no longer necessary. Instead you can directly pass `&TextArea`
1622 /// reference to the `Frame::render_widget` method call.
1623 /// ```no_run
1624 /// # use ratatui::layout::Rect;
1625 /// # use ratatui::Terminal;
1626 /// # use ratatui::widgets::Widget as _;
1627 /// # use ratatui::backend::CrosstermBackend;
1628 /// # use tui_textarea::TextArea;
1629 /// #
1630 /// # let backend = CrosstermBackend::new(std::io::stdout());
1631 /// # let mut term = Terminal::new(backend).unwrap();
1632 /// # let textarea = TextArea::default();
1633 /// #
1634 /// # #[allow(deprecated)]
1635 /// # term.draw(|f| {
1636 /// # let rect = Rect {
1637 /// # x: 0,
1638 /// # y: 0,
1639 /// # width: 24,
1640 /// # height: 8,
1641 /// # };
1642 /// // v0.5.2 or earlier
1643 /// f.render_widget(textarea.widget(), rect);
1644 ///
1645 /// // v0.5.3 or later
1646 /// f.render_widget(&textarea, rect);
1647 /// # }).unwrap();
1648 /// ```
1649 #[deprecated(
1650 since = "0.5.3",
1651 note = "calling this method is no longer necessary on rendering a textarea. pass &TextArea reference to `Frame::render_widget` method call directly"
1652 )]
1653 pub fn widget(&'a self) -> impl Widget + 'a {
1654 self
1655 }
1656
1657 /// Set the style of textarea. By default, textarea is not styled.
1658 /// ```
1659 /// use ratatui::style::{Style, Color};
1660 /// use tui_textarea::TextArea;
1661 ///
1662 /// let mut textarea = TextArea::default();
1663 /// let style = Style::default().fg(Color::Red);
1664 /// textarea.set_style(style);
1665 /// assert_eq!(textarea.style(), style);
1666 /// ```
1667 pub fn set_style(&mut self, style: Style) {
1668 self.style = style;
1669 }
1670
1671 /// Get the current style of textarea.
1672 pub fn style(&self) -> Style {
1673 self.style
1674 }
1675
1676 /// Set the block of textarea. By default, no block is set.
1677 /// ```
1678 /// use tui_textarea::TextArea;
1679 /// use ratatui::widgets::{Block, Borders};
1680 ///
1681 /// let mut textarea = TextArea::default();
1682 /// let block = Block::default().borders(Borders::ALL).title("Block Title");
1683 /// textarea.set_block(block);
1684 /// assert!(textarea.block().is_some());
1685 /// ```
1686 pub fn set_block(&mut self, block: Block<'a>) {
1687 self.block = Some(block);
1688 }
1689
1690 /// Remove the block of textarea which was set by [`TextArea::set_block`].
1691 /// ```
1692 /// use tui_textarea::TextArea;
1693 /// use ratatui::widgets::{Block, Borders};
1694 ///
1695 /// let mut textarea = TextArea::default();
1696 /// let block = Block::default().borders(Borders::ALL).title("Block Title");
1697 /// textarea.set_block(block);
1698 /// textarea.remove_block();
1699 /// assert!(textarea.block().is_none());
1700 /// ```
1701 pub fn remove_block(&mut self) {
1702 self.block = None;
1703 }
1704
1705 /// Get the block of textarea if exists.
1706 pub fn block<'s>(&'s self) -> Option<&'s Block<'a>> {
1707 self.block.as_ref()
1708 }
1709
1710 /// Set the length of tab character. Setting 0 disables tab inputs.
1711 /// ```
1712 /// use tui_textarea::{TextArea, Input, Key};
1713 ///
1714 /// let mut textarea = TextArea::default();
1715 /// let tab_input = Input { key: Key::Tab, ctrl: false, alt: false, shift: false };
1716 ///
1717 /// textarea.set_tab_length(8);
1718 /// textarea.input(tab_input.clone());
1719 /// assert_eq!(textarea.lines(), [" "]);
1720 ///
1721 /// textarea.set_tab_length(2);
1722 /// textarea.input(tab_input);
1723 /// assert_eq!(textarea.lines(), [" "]);
1724 /// ```
1725 pub fn set_tab_length(&mut self, len: u8) {
1726 self.tab_len = len;
1727 }
1728
1729 /// Get how many spaces are used for representing tab character. The default value is 4.
1730 pub fn tab_length(&self) -> u8 {
1731 self.tab_len
1732 }
1733
1734 /// Set if a hard tab is used or not for indent. When `true` is set, typing a tab key inserts a hard tab instead of
1735 /// spaces. By default, hard tab is disabled.
1736 /// ```
1737 /// use tui_textarea::TextArea;
1738 ///
1739 /// let mut textarea = TextArea::default();
1740 ///
1741 /// textarea.set_hard_tab_indent(true);
1742 /// textarea.insert_tab();
1743 /// assert_eq!(textarea.lines(), ["\t"]);
1744 /// ```
1745 pub fn set_hard_tab_indent(&mut self, enabled: bool) {
1746 self.hard_tab_indent = enabled;
1747 }
1748
1749 /// Get if a hard tab is used for indent or not.
1750 /// ```
1751 /// use tui_textarea::TextArea;
1752 ///
1753 /// let mut textarea = TextArea::default();
1754 ///
1755 /// assert!(!textarea.hard_tab_indent());
1756 /// textarea.set_hard_tab_indent(true);
1757 /// assert!(textarea.hard_tab_indent());
1758 /// ```
1759 pub fn hard_tab_indent(&self) -> bool {
1760 self.hard_tab_indent
1761 }
1762
1763 /// Get a string for indent. It consists of spaces by default. When hard tab is enabled, it is a tab character.
1764 /// ```
1765 /// use tui_textarea::TextArea;
1766 ///
1767 /// let mut textarea = TextArea::default();
1768 ///
1769 /// assert_eq!(textarea.indent(), " ");
1770 /// textarea.set_tab_length(2);
1771 /// assert_eq!(textarea.indent(), " ");
1772 /// textarea.set_hard_tab_indent(true);
1773 /// assert_eq!(textarea.indent(), "\t");
1774 /// ```
1775 pub fn indent(&self) -> &'static str {
1776 if self.hard_tab_indent {
1777 "\t"
1778 } else {
1779 spaces(self.tab_len)
1780 }
1781 }
1782
1783 /// Set how many modifications are remembered for undo/redo. Setting 0 disables undo/redo.
1784 pub fn set_max_histories(&mut self, max: usize) {
1785 self.history = History::new(max);
1786 }
1787
1788 /// Get how many modifications are remembered for undo/redo. The default value is 50.
1789 pub fn max_histories(&self) -> usize {
1790 self.history.max_items()
1791 }
1792
1793 /// Set the style of line at cursor. By default, the cursor line is styled with underline. To stop styling the
1794 /// cursor line, set the default style.
1795 /// ```
1796 /// use ratatui::style::{Style, Color};
1797 /// use tui_textarea::TextArea;
1798 ///
1799 /// let mut textarea = TextArea::default();
1800 ///
1801 /// let style = Style::default().fg(Color::Red);
1802 /// textarea.set_cursor_line_style(style);
1803 /// assert_eq!(textarea.cursor_line_style(), style);
1804 ///
1805 /// // Disable cursor line style
1806 /// textarea.set_cursor_line_style(Style::default());
1807 /// ```
1808 pub fn set_cursor_line_style(&mut self, style: Style) {
1809 self.cursor_line_style = style;
1810 }
1811
1812 /// Get the style of cursor line. By default it is styled with underline.
1813 pub fn cursor_line_style(&self) -> Style {
1814 self.cursor_line_style
1815 }
1816
1817 /// Set the style of line number. By setting the style with this method, line numbers are drawn in textarea, meant
1818 /// that line numbers are disabled by default. If you want to show line numbers but don't want to style them, set
1819 /// the default style.
1820 /// ```
1821 /// use ratatui::style::{Style, Color};
1822 /// use tui_textarea::TextArea;
1823 ///
1824 /// let mut textarea = TextArea::default();
1825 ///
1826 /// // Show line numbers in dark gray background
1827 /// let style = Style::default().bg(Color::DarkGray);
1828 /// textarea.set_line_number_style(style);
1829 /// assert_eq!(textarea.line_number_style(), Some(style));
1830 /// ```
1831 pub fn set_line_number_style(&mut self, style: Style) {
1832 self.line_number_style = Some(style);
1833 }
1834
1835 /// Remove the style of line number which was set by [`TextArea::set_line_number_style`]. After calling this
1836 /// method, Line numbers will no longer be shown.
1837 /// ```
1838 /// use ratatui::style::{Style, Color};
1839 /// use tui_textarea::TextArea;
1840 ///
1841 /// let mut textarea = TextArea::default();
1842 ///
1843 /// textarea.set_line_number_style(Style::default().bg(Color::DarkGray));
1844 /// textarea.remove_line_number();
1845 /// assert_eq!(textarea.line_number_style(), None);
1846 /// ```
1847 pub fn remove_line_number(&mut self) {
1848 self.line_number_style = None;
1849 }
1850
1851 /// Get the style of line number if set.
1852 pub fn line_number_style(&self) -> Option<Style> {
1853 self.line_number_style
1854 }
1855
1856 /// Set the placeholder text. The text is set in the textarea when no text is input. Setting a non-empty string `""`
1857 /// enables the placeholder. The default value is an empty string so the placeholder is disabled by default.
1858 /// To customize the text style, see [`TextArea::set_placeholder_style`].
1859 /// ```
1860 /// use tui_textarea::TextArea;
1861 ///
1862 /// let mut textarea = TextArea::default();
1863 /// assert_eq!(textarea.placeholder_text(), "");
1864 /// assert!(textarea.placeholder_style().is_none());
1865 ///
1866 /// textarea.set_placeholder_text("Hello");
1867 /// assert_eq!(textarea.placeholder_text(), "Hello");
1868 /// assert!(textarea.placeholder_style().is_some());
1869 /// ```
1870 pub fn set_placeholder_text(&mut self, placeholder: impl Into<String>) {
1871 self.placeholder = placeholder.into();
1872 }
1873
1874 /// Set the style of the placeholder text. The default style is a dark gray text.
1875 /// ```
1876 /// use ratatui::style::{Style, Color};
1877 /// use tui_textarea::TextArea;
1878 ///
1879 /// let mut textarea = TextArea::default();
1880 /// assert_eq!(textarea.placeholder_style(), None); // When the placeholder is disabled
1881 ///
1882 /// textarea.set_placeholder_text("Enter your message"); // Enable placeholder by setting non-empty text
1883 ///
1884 /// let style = Style::default().bg(Color::Blue);
1885 /// textarea.set_placeholder_style(style);
1886 /// assert_eq!(textarea.placeholder_style(), Some(style));
1887 /// ```
1888 pub fn set_placeholder_style(&mut self, style: Style) {
1889 self.placeholder_style = style;
1890 }
1891
1892 /// Get the placeholder text. An empty string means the placeholder is disabled. The default value is an empty string.
1893 /// ```
1894 /// use tui_textarea::TextArea;
1895 ///
1896 /// let textarea = TextArea::default();
1897 /// assert_eq!(textarea.placeholder_text(), "");
1898 /// ```
1899 pub fn placeholder_text(&self) -> &'_ str {
1900 self.placeholder.as_str()
1901 }
1902
1903 /// Get the placeholder style. When the placeholder text is empty, it returns `None` since the placeholder is disabled.
1904 /// The default style is a dark gray text.
1905 /// ```
1906 /// use tui_textarea::TextArea;
1907 ///
1908 /// let mut textarea = TextArea::default();
1909 /// assert_eq!(textarea.placeholder_style(), None);
1910 ///
1911 /// textarea.set_placeholder_text("hello");
1912 /// assert!(textarea.placeholder_style().is_some());
1913 /// ```
1914 pub fn placeholder_style(&self) -> Option<Style> {
1915 if self.placeholder.is_empty() {
1916 None
1917 } else {
1918 Some(self.placeholder_style)
1919 }
1920 }
1921
1922 /// Specify a character masking the text. All characters in the textarea will be replaced by this character.
1923 /// This API is useful for making a kind of credentials form such as a password input.
1924 /// ```
1925 /// use tui_textarea::TextArea;
1926 ///
1927 /// let mut textarea = TextArea::default();
1928 ///
1929 /// textarea.set_mask_char('*');
1930 /// assert_eq!(textarea.mask_char(), Some('*'));
1931 /// textarea.set_mask_char('●');
1932 /// assert_eq!(textarea.mask_char(), Some('●'));
1933 /// ```
1934 pub fn set_mask_char(&mut self, mask: char) {
1935 self.mask = Some(mask);
1936 }
1937
1938 /// Clear the masking character previously set by [`TextArea::set_mask_char`].
1939 /// ```
1940 /// use tui_textarea::TextArea;
1941 ///
1942 /// let mut textarea = TextArea::default();
1943 ///
1944 /// textarea.set_mask_char('*');
1945 /// assert_eq!(textarea.mask_char(), Some('*'));
1946 /// textarea.clear_mask_char();
1947 /// assert_eq!(textarea.mask_char(), None);
1948 /// ```
1949 pub fn clear_mask_char(&mut self) {
1950 self.mask = None;
1951 }
1952
1953 /// Get the character to mask text. When no character is set, `None` is returned.
1954 /// ```
1955 /// use tui_textarea::TextArea;
1956 ///
1957 /// let mut textarea = TextArea::default();
1958 ///
1959 /// assert_eq!(textarea.mask_char(), None);
1960 /// textarea.set_mask_char('*');
1961 /// assert_eq!(textarea.mask_char(), Some('*'));
1962 /// ```
1963 pub fn mask_char(&self) -> Option<char> {
1964 self.mask
1965 }
1966
1967 /// Set the style of cursor. By default, a cursor is rendered in the reversed color. Setting the same style as
1968 /// cursor line hides a cursor.
1969 /// ```
1970 /// use ratatui::style::{Style, Color};
1971 /// use tui_textarea::TextArea;
1972 ///
1973 /// let mut textarea = TextArea::default();
1974 ///
1975 /// let style = Style::default().bg(Color::Red);
1976 /// textarea.set_cursor_style(style);
1977 /// assert_eq!(textarea.cursor_style(), style);
1978 /// ```
1979 pub fn set_cursor_style(&mut self, style: Style) {
1980 self.cursor_style = style;
1981 }
1982
1983 /// Get the style of cursor.
1984 pub fn cursor_style(&self) -> Style {
1985 self.cursor_style
1986 }
1987
1988 /// Get slice of line texts. This method borrows the content, but not moves. Note that the returned slice will
1989 /// never be empty because an empty text means a slice containing one empty line. This is correct since any text
1990 /// file must end with a newline.
1991 /// ```
1992 /// use tui_textarea::TextArea;
1993 ///
1994 /// let mut textarea = TextArea::default();
1995 /// assert_eq!(textarea.lines(), [""]);
1996 ///
1997 /// textarea.insert_char('a');
1998 /// assert_eq!(textarea.lines(), ["a"]);
1999 ///
2000 /// textarea.insert_newline();
2001 /// assert_eq!(textarea.lines(), ["a", ""]);
2002 ///
2003 /// textarea.insert_char('b');
2004 /// assert_eq!(textarea.lines(), ["a", "b"]);
2005 /// ```
2006 pub fn lines(&'a self) -> &'a [String] {
2007 &self.lines
2008 }
2009
2010 /// Convert [`TextArea`] instance into line texts.
2011 /// ```
2012 /// use tui_textarea::TextArea;
2013 ///
2014 /// let mut textarea = TextArea::default();
2015 ///
2016 /// textarea.insert_char('a');
2017 /// textarea.insert_newline();
2018 /// textarea.insert_char('b');
2019 ///
2020 /// assert_eq!(textarea.into_lines(), ["a", "b"]);
2021 /// ```
2022 pub fn into_lines(self) -> Vec<String> {
2023 self.lines
2024 }
2025
2026 /// Get the current cursor position. 0-base character-wise (row, col) cursor position.
2027 /// ```
2028 /// use tui_textarea::TextArea;
2029 ///
2030 /// let mut textarea = TextArea::default();
2031 /// assert_eq!(textarea.cursor(), (0, 0));
2032 ///
2033 /// textarea.insert_char('a');
2034 /// textarea.insert_newline();
2035 /// textarea.insert_char('b');
2036 ///
2037 /// assert_eq!(textarea.cursor(), (1, 1));
2038 /// ```
2039 pub fn cursor(&self) -> (usize, usize) {
2040 self.cursor
2041 }
2042
2043 /// Get the current selection range as a pair of the start position and the end position. The range is bounded
2044 /// inclusively below and exclusively above. The positions are 0-base character-wise (row, col) values.
2045 /// The first element of the pair is always smaller than the second one even when it is ahead of the cursor.
2046 /// When no text is selected, this method returns `None`.
2047 /// ```
2048 /// use tui_textarea::TextArea;
2049 /// use tui_textarea::CursorMove;
2050 ///
2051 /// let mut textarea = TextArea::from([
2052 /// "aaa",
2053 /// "bbb",
2054 /// "ccc",
2055 /// ]);
2056 ///
2057 /// // It returns `None` when the text selection is not ongoing
2058 /// assert_eq!(textarea.selection_range(), None);
2059 ///
2060 /// textarea.start_selection();
2061 /// assert_eq!(textarea.selection_range(), Some(((0, 0), (0, 0))));
2062 ///
2063 /// textarea.move_cursor(CursorMove::Forward);
2064 /// assert_eq!(textarea.selection_range(), Some(((0, 0), (0, 1))));
2065 ///
2066 /// textarea.move_cursor(CursorMove::Down);
2067 /// assert_eq!(textarea.selection_range(), Some(((0, 0), (1, 1))));
2068 ///
2069 /// // Start selection at (1, 1)
2070 /// textarea.cancel_selection();
2071 /// textarea.start_selection();
2072 ///
2073 /// // The first element of the pair is always smaller than the second one
2074 /// textarea.move_cursor(CursorMove::Back);
2075 /// assert_eq!(textarea.selection_range(), Some(((1, 0), (1, 1))));
2076 /// ```
2077 pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
2078 self.selection_start.map(|pos| {
2079 if pos > self.cursor {
2080 (self.cursor, pos)
2081 } else {
2082 (pos, self.cursor)
2083 }
2084 })
2085 }
2086
2087 /// Set text alignment. When [`Alignment::Center`] or [`Alignment::Right`] is set, line number is automatically
2088 /// disabled because those alignments don't work well with line numbers.
2089 /// ```
2090 /// use ratatui::layout::Alignment;
2091 /// use tui_textarea::TextArea;
2092 ///
2093 /// let mut textarea = TextArea::default();
2094 ///
2095 /// textarea.set_alignment(Alignment::Center);
2096 /// assert_eq!(textarea.alignment(), Alignment::Center);
2097 /// ```
2098 pub fn set_alignment(&mut self, alignment: Alignment) {
2099 if let Alignment::Center | Alignment::Right = alignment {
2100 self.line_number_style = None;
2101 }
2102 self.alignment = alignment;
2103 }
2104
2105 /// Get current text alignment. The default alignment is [`Alignment::Left`].
2106 /// ```
2107 /// use ratatui::layout::Alignment;
2108 /// use tui_textarea::TextArea;
2109 ///
2110 /// let mut textarea = TextArea::default();
2111 ///
2112 /// assert_eq!(textarea.alignment(), Alignment::Left);
2113 /// ```
2114 pub fn alignment(&self) -> Alignment {
2115 self.alignment
2116 }
2117
2118 /// Check if the textarea has a empty content.
2119 /// ```
2120 /// use tui_textarea::TextArea;
2121 ///
2122 /// let textarea = TextArea::default();
2123 /// assert!(textarea.is_empty());
2124 ///
2125 /// let textarea = TextArea::from(["hello"]);
2126 /// assert!(!textarea.is_empty());
2127 /// ```
2128 pub fn is_empty(&self) -> bool {
2129 self.lines == [""]
2130 }
2131
2132 /// Get the yanked text. Text is automatically yanked when deleting strings by [`TextArea::delete_line_by_head`],
2133 /// [`TextArea::delete_line_by_end`], [`TextArea::delete_word`], [`TextArea::delete_next_word`],
2134 /// [`TextArea::delete_str`], [`TextArea::copy`], and [`TextArea::cut`]. When multiple lines were yanked, they are
2135 /// always joined with `\n`.
2136 /// ```
2137 /// use tui_textarea::TextArea;
2138 ///
2139 /// let mut textarea = TextArea::from(["abc"]);
2140 /// textarea.delete_next_word();
2141 /// assert_eq!(textarea.yank_text(), "abc");
2142 ///
2143 /// // Multiple lines are joined with \n
2144 /// let mut textarea = TextArea::from(["abc", "def"]);
2145 /// textarea.delete_str(5);
2146 /// assert_eq!(textarea.yank_text(), "abc\nd");
2147 /// ```
2148 pub fn yank_text(&self) -> String {
2149 self.yank.to_string()
2150 }
2151
2152 /// Set a yanked text. The text can be inserted by [`TextArea::paste`]. `\n` and `\r\n` are recognized as newline
2153 /// but `\r` isn't.
2154 /// ```
2155 /// use tui_textarea::TextArea;
2156 ///
2157 /// let mut textarea = TextArea::default();
2158 ///
2159 /// textarea.set_yank_text("hello\nworld");
2160 /// textarea.paste();
2161 /// assert_eq!(textarea.lines(), ["hello", "world"]);
2162 /// ```
2163 pub fn set_yank_text(&mut self, text: impl Into<String>) {
2164 // `str::lines` is not available since it strips a newline at end
2165 let lines: Vec<_> = text
2166 .into()
2167 .split('\n')
2168 .map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
2169 .collect();
2170 self.yank = lines.into();
2171 }
2172
2173 /// Set a regular expression pattern for text search. Setting an empty string stops the text search.
2174 /// When a valid pattern is set, all matches will be highlighted in the textarea. Note that the cursor does not
2175 /// move. To move the cursor, use [`TextArea::search_forward`] and [`TextArea::search_back`].
2176 ///
2177 /// Grammar of regular expression follows [regex crate](https://docs.rs/regex/latest/regex). Patterns don't match
2178 /// to newlines so match passes across no newline.
2179 ///
2180 /// When the pattern is invalid, the search pattern will not be updated and an error will be returned.
2181 ///
2182 /// ```
2183 /// use tui_textarea::TextArea;
2184 ///
2185 /// let mut textarea = TextArea::from(["hello, world", "goodbye, world"]);
2186 ///
2187 /// // Search "world"
2188 /// textarea.set_search_pattern("world").unwrap();
2189 ///
2190 /// assert_eq!(textarea.cursor(), (0, 0));
2191 /// textarea.search_forward(false);
2192 /// assert_eq!(textarea.cursor(), (0, 7));
2193 /// textarea.search_forward(false);
2194 /// assert_eq!(textarea.cursor(), (1, 9));
2195 ///
2196 /// // Stop the text search
2197 /// textarea.set_search_pattern("");
2198 ///
2199 /// // Invalid search pattern
2200 /// assert!(textarea.set_search_pattern("(hello").is_err());
2201 /// ```
2202 #[cfg(feature = "search")]
2203 #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2204 pub fn set_search_pattern(&mut self, query: impl AsRef<str>) -> Result<(), regex::Error> {
2205 self.search.set_pattern(query.as_ref())
2206 }
2207
2208 /// Get a regular expression which was set by [`TextArea::set_search_pattern`]. When no text search is ongoing, this
2209 /// method returns `None`.
2210 ///
2211 /// ```
2212 /// use tui_textarea::TextArea;
2213 ///
2214 /// let mut textarea = TextArea::default();
2215 ///
2216 /// assert!(textarea.search_pattern().is_none());
2217 /// textarea.set_search_pattern("hello+").unwrap();
2218 /// assert!(textarea.search_pattern().is_some());
2219 /// assert_eq!(textarea.search_pattern().unwrap().as_str(), "hello+");
2220 /// ```
2221 #[cfg(feature = "search")]
2222 #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2223 pub fn search_pattern(&self) -> Option<®ex::Regex> {
2224 self.search.pat.as_ref()
2225 }
2226
2227 /// Search the pattern set by [`TextArea::set_search_pattern`] forward and move the cursor to the next match
2228 /// position based on the current cursor position. Text search wraps around a text buffer. It returns `true` when
2229 /// some match was found. Otherwise it returns `false`.
2230 ///
2231 /// The `match_cursor` parameter represents if the search matches to the current cursor position or not. When `true`
2232 /// is set and the cursor position matches to the pattern, the cursor will not move. When `false`, the cursor will
2233 /// move to the next match ignoring the match at the current position.
2234 ///
2235 /// ```
2236 /// use tui_textarea::TextArea;
2237 ///
2238 /// let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
2239 ///
2240 /// textarea.set_search_pattern("hello+").unwrap();
2241 ///
2242 /// // Move to next position
2243 /// let match_found = textarea.search_forward(false);
2244 /// assert!(match_found);
2245 /// assert_eq!(textarea.cursor(), (1, 0));
2246 ///
2247 /// // Since the cursor position matches to "hello+", it does not move
2248 /// textarea.search_forward(true);
2249 /// assert_eq!(textarea.cursor(), (1, 0));
2250 ///
2251 /// // When `match_current` parameter is set to `false`, match at the cursor position is ignored
2252 /// textarea.search_forward(false);
2253 /// assert_eq!(textarea.cursor(), (2, 0));
2254 ///
2255 /// // Text search wrap around the buffer
2256 /// textarea.search_forward(false);
2257 /// assert_eq!(textarea.cursor(), (0, 0));
2258 ///
2259 /// // `false` is returned when no match was found
2260 /// textarea.set_search_pattern("bye+").unwrap();
2261 /// let match_found = textarea.search_forward(false);
2262 /// assert!(!match_found);
2263 /// ```
2264 #[cfg(feature = "search")]
2265 #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2266 pub fn search_forward(&mut self, match_cursor: bool) -> bool {
2267 if let Some(cursor) = self.search.forward(&self.lines, self.cursor, match_cursor) {
2268 self.cursor = cursor;
2269 true
2270 } else {
2271 false
2272 }
2273 }
2274
2275 /// Search the pattern set by [`TextArea::set_search_pattern`] backward and move the cursor to the next match
2276 /// position based on the current cursor position. Text search wraps around a text buffer. It returns `true` when
2277 /// some match was found. Otherwise it returns `false`.
2278 ///
2279 /// The `match_cursor` parameter represents if the search matches to the current cursor position or not. When `true`
2280 /// is set and the cursor position matches to the pattern, the cursor will not move. When `false`, the cursor will
2281 /// move to the next match ignoring the match at the current position.
2282 ///
2283 /// ```
2284 /// use tui_textarea::TextArea;
2285 ///
2286 /// let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
2287 ///
2288 /// textarea.set_search_pattern("hello+").unwrap();
2289 ///
2290 /// // Move to next position with wrapping around the text buffer
2291 /// let match_found = textarea.search_back(false);
2292 /// assert!(match_found);
2293 /// assert_eq!(textarea.cursor(), (2, 0));
2294 ///
2295 /// // Since the cursor position matches to "hello+", it does not move
2296 /// textarea.search_back(true);
2297 /// assert_eq!(textarea.cursor(), (2, 0));
2298 ///
2299 /// // When `match_current` parameter is set to `false`, match at the cursor position is ignored
2300 /// textarea.search_back(false);
2301 /// assert_eq!(textarea.cursor(), (1, 0));
2302 ///
2303 /// // `false` is returned when no match was found
2304 /// textarea.set_search_pattern("bye+").unwrap();
2305 /// let match_found = textarea.search_back(false);
2306 /// assert!(!match_found);
2307 /// ```
2308 #[cfg(feature = "search")]
2309 #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2310 pub fn search_back(&mut self, match_cursor: bool) -> bool {
2311 if let Some(cursor) = self.search.back(&self.lines, self.cursor, match_cursor) {
2312 self.cursor = cursor;
2313 true
2314 } else {
2315 false
2316 }
2317 }
2318
2319 /// Get the text style at matches of text search. The default style is colored with blue in background.
2320 ///
2321 /// ```
2322 /// use ratatui::style::{Style, Color};
2323 /// use tui_textarea::TextArea;
2324 ///
2325 /// let textarea = TextArea::default();
2326 ///
2327 /// assert_eq!(textarea.search_style(), Style::default().bg(Color::Blue));
2328 /// ```
2329 #[cfg(feature = "search")]
2330 #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2331 pub fn search_style(&self) -> Style {
2332 self.search.style
2333 }
2334
2335 /// Set the text style at matches of text search. The default style is colored with blue in background.
2336 ///
2337 /// ```
2338 /// use ratatui::style::{Style, Color};
2339 /// use tui_textarea::TextArea;
2340 ///
2341 /// let mut textarea = TextArea::default();
2342 ///
2343 /// let red_bg = Style::default().bg(Color::Red);
2344 /// textarea.set_search_style(red_bg);
2345 ///
2346 /// assert_eq!(textarea.search_style(), red_bg);
2347 /// ```
2348 #[cfg(feature = "search")]
2349 #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2350 pub fn set_search_style(&mut self, style: Style) {
2351 self.search.style = style;
2352 }
2353
2354 /// Scroll the textarea. See [`Scrolling`] for the argument.
2355 /// The cursor will not move until it goes out the viewport. When the cursor position is outside the viewport after scroll,
2356 /// the cursor position will be adjusted to stay in the viewport using the same logic as [`CursorMove::InViewport`].
2357 ///
2358 /// ```
2359 /// # use ratatui::buffer::Buffer;
2360 /// # use ratatui::layout::Rect;
2361 /// # use ratatui::widgets::Widget as _;
2362 /// use tui_textarea::TextArea;
2363 ///
2364 /// // Let's say terminal height is 8.
2365 ///
2366 /// // Create textarea with 20 lines "0", "1", "2", "3", ...
2367 /// let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
2368 /// # // Call `render` at least once to populate terminal size
2369 /// # let r = Rect { x: 0, y: 0, width: 24, height: 8 };
2370 /// # let mut b = Buffer::empty(r.clone());
2371 /// # textarea.render(r, &mut b);
2372 ///
2373 /// // Scroll down by 15 lines. Since terminal height is 8, cursor will go out
2374 /// // the viewport.
2375 /// textarea.scroll((15, 0));
2376 /// // So the cursor position was updated to stay in the viewport after the scrolling.
2377 /// assert_eq!(textarea.cursor(), (15, 0));
2378 ///
2379 /// // Scroll up by 5 lines. Since the scroll amount is smaller than the terminal
2380 /// // height, cursor position will not be updated.
2381 /// textarea.scroll((-5, 0));
2382 /// assert_eq!(textarea.cursor(), (15, 0));
2383 ///
2384 /// // Scroll up by 5 lines again. The terminal height is 8. So a cursor reaches to
2385 /// // the top of viewport after scrolling up by 7 lines. Since we have already
2386 /// // scrolled up by 5 lines, scrolling up by 5 lines again makes the cursor overrun
2387 /// // the viewport by 5 - 2 = 3 lines. To keep the cursor stay in the viewport, the
2388 /// // cursor position will be adjusted from line 15 to line 12.
2389 /// textarea.scroll((-5, 0));
2390 /// assert_eq!(textarea.cursor(), (12, 0));
2391 /// ```
2392 pub fn scroll(&mut self, scrolling: impl Into<Scrolling>) {
2393 self.scroll_with_shift(scrolling.into(), self.selection_start.is_some());
2394 }
2395
2396 fn scroll_with_shift(&mut self, scrolling: Scrolling, shift: bool) {
2397 if shift && self.selection_start.is_none() {
2398 self.selection_start = Some(self.cursor);
2399 }
2400 scrolling.scroll(&mut self.viewport);
2401 self.move_cursor_with_shift(CursorMove::InViewport, shift);
2402 }
2403}
2404
2405#[cfg(test)]
2406mod tests {
2407 use super::*;
2408
2409 // Separate tests for tui-rs support
2410 #[test]
2411 fn scroll() {
2412 use crate::ratatui::buffer::Buffer;
2413 use crate::ratatui::layout::Rect;
2414 use crate::ratatui::widgets::Widget as _;
2415
2416 let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
2417 let r = Rect {
2418 x: 0,
2419 y: 0,
2420 width: 24,
2421 height: 8,
2422 };
2423 let mut b = Buffer::empty(r);
2424 textarea.render(r, &mut b);
2425
2426 textarea.scroll((15, 0));
2427 assert_eq!(textarea.cursor(), (15, 0));
2428 textarea.scroll((-5, 0));
2429 assert_eq!(textarea.cursor(), (15, 0));
2430 textarea.scroll((-5, 0));
2431 assert_eq!(textarea.cursor(), (12, 0));
2432 }
2433}