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<&regex::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}