Skip to main content

ratatui_textarea/
textarea.rs

1use crate::cursor::CursorMove;
2use crate::highlight::LineHighlighter;
3use crate::history::{Edit, EditKind, History};
4use crate::input::{Input, Key};
5use crate::scroll::Scrolling;
6#[cfg(feature = "search")]
7use crate::search::Search;
8use crate::util::{Pos, spaces};
9use crate::widget::Viewport;
10use crate::word::{find_word_exclusive_end_forward, find_word_start_backward};
11use ratatui_core::layout::Alignment;
12use ratatui_core::style::{Color, Modifier, Style};
13use ratatui_core::text::Line;
14use ratatui_core::widgets::Widget;
15use ratatui_widgets::block::Block;
16use std::cmp::Ordering;
17use std::fmt;
18use unicode_width::UnicodeWidthChar as _;
19
20#[derive(Debug, Clone)]
21enum YankText {
22    Piece(String),
23    Chunk(Vec<String>),
24}
25
26impl Default for YankText {
27    fn default() -> Self {
28        Self::Piece(String::new())
29    }
30}
31
32impl From<String> for YankText {
33    fn from(s: String) -> Self {
34        Self::Piece(s)
35    }
36}
37impl From<Vec<String>> for YankText {
38    fn from(mut c: Vec<String>) -> Self {
39        match c.len() {
40            0 => Self::default(),
41            1 => Self::Piece(c.remove(0)),
42            _ => Self::Chunk(c),
43        }
44    }
45}
46
47impl fmt::Display for YankText {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::Piece(s) => write!(f, "{}", s),
51            Self::Chunk(ss) => write!(f, "{}", ss.join("\n")),
52        }
53    }
54}
55
56/// A type to manage state of textarea. These are some important methods:
57///
58/// - [`TextArea::default`] creates an empty textarea.
59/// - [`TextArea::new`] creates a textarea with given text lines.
60/// - [`TextArea::from`] creates a textarea from an iterator of lines.
61/// - [`TextArea::input`] handles key input.
62/// - [`TextArea::lines`] returns line texts.
63/// ```
64/// use ratatui_textarea::{TextArea, Input, Key};
65/// use ratatui::backend::CrosstermBackend;
66/// use ratatui::layout::{Constraint, Direction, Layout};
67/// use ratatui::Terminal;
68///
69/// let mut textarea = TextArea::default();
70///
71/// // Input 'a'
72/// let input = Input { key: Key::Char('a'), ctrl: false, alt: false, shift: false };
73/// textarea.input(input);
74///
75/// // Get lines as String.
76/// println!("Lines: {:?}", textarea.lines());
77/// ```
78///
79/// It implements [`ratatui_core::widgets::Widget`] trait so it can be rendered to a terminal screen via
80/// [`ratatui_core::terminal::Frame::render_widget`] method.
81/// ```no_run
82/// use ratatui::backend::CrosstermBackend;
83/// use ratatui::layout::{Constraint, Direction, Layout};
84/// use ratatui::Terminal;
85/// use ratatui_textarea::TextArea;
86///
87/// let mut textarea = TextArea::default();
88///
89/// let layout = Layout::default()
90///     .direction(Direction::Vertical)
91///     .constraints([Constraint::Min(1)].as_ref());
92/// let backend = CrosstermBackend::new(std::io::stdout());
93/// let mut term = Terminal::new(backend).unwrap();
94///
95/// loop {
96///     term.draw(|f| {
97///         let chunks = layout.split(f.area());
98///         f.render_widget(&textarea, chunks[0]);
99///     }).unwrap();
100///
101///     // ...
102/// }
103/// ```
104#[derive(Clone, Debug)]
105pub struct TextArea<'a> {
106    lines: Vec<String>,
107    block: Option<Block<'a>>,
108    style: Style,
109    cursor: (usize, usize), // 0-base
110    tab_len: u8,
111    hard_tab_indent: bool,
112    history: History,
113    cursor_line_style: Style,
114    line_number_style: Option<Style>,
115    pub(crate) viewport: Viewport,
116    pub(crate) cursor_style: Style,
117    yank: YankText,
118    #[cfg(feature = "search")]
119    search: Search,
120    alignment: Alignment,
121    pub(crate) placeholder: String,
122    pub(crate) placeholder_style: Style,
123    mask: Option<char>,
124    selection_start: Option<(usize, usize)>,
125    select_style: Style,
126}
127
128/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
129/// handled as line. Ensure that the strings don't contain any newlines. This method is useful to create [`TextArea`]
130/// from [`std::str::Lines`].
131/// ```
132/// use ratatui_textarea::TextArea;
133///
134/// // From `String`
135/// let text = "hello\nworld";
136/// let textarea = TextArea::from(text.lines());
137/// assert_eq!(textarea.lines(), ["hello", "world"]);
138///
139/// // From array of `&str`
140/// let textarea = TextArea::from(["hello", "world"]);
141/// assert_eq!(textarea.lines(), ["hello", "world"]);
142///
143/// // From slice of `&str`
144/// let slice = &["hello", "world"];
145/// let textarea = TextArea::from(slice.iter().copied());
146/// assert_eq!(textarea.lines(), ["hello", "world"]);
147/// ```
148impl<I> From<I> for TextArea<'_>
149where
150    I: IntoIterator,
151    I::Item: Into<String>,
152{
153    fn from(i: I) -> Self {
154        Self::new(i.into_iter().map(|s| s.into()).collect::<Vec<String>>())
155    }
156}
157
158/// Collect line texts from iterator as [`TextArea`]. It is useful when creating a textarea with text read from a file.
159/// [`Iterator::collect`] handles errors which may happen on reading each lines. The following example reads text from
160/// a file efficiently line-by-line.
161/// ```
162/// use std::fs;
163/// use std::io::{self, BufRead};
164/// use std::path::Path;
165/// use ratatui_textarea::TextArea;
166///
167/// fn read_from_file<'a>(path: impl AsRef<Path>) -> io::Result<TextArea<'a>> {
168///     let file = fs::File::open(path)?;
169///     io::BufReader::new(file).lines().collect()
170/// }
171///
172/// let textarea = read_from_file("README.md").unwrap();
173/// assert!(!textarea.is_empty());
174/// ```
175impl<S: Into<String>> FromIterator<S> for TextArea<'_> {
176    fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
177        iter.into()
178    }
179}
180
181/// Create [`TextArea`] instance with empty text content.
182/// ```
183/// use ratatui_textarea::TextArea;
184///
185/// let textarea = TextArea::default();
186/// assert_eq!(textarea.lines(), [""]);
187/// assert!(textarea.is_empty());
188/// ```
189impl Default for TextArea<'_> {
190    fn default() -> Self {
191        Self::new(vec![String::new()])
192    }
193}
194
195impl<'a> TextArea<'a> {
196    /// Create [`TextArea`] instance with given lines. If you have value other than `Vec<String>`, [`TextArea::from`]
197    /// may be more useful.
198    /// ```
199    /// use ratatui_textarea::TextArea;
200    ///
201    /// let lines = vec!["hello".to_string(), "...".to_string(), "goodbye".to_string()];
202    /// let textarea = TextArea::new(lines);
203    /// assert_eq!(textarea.lines(), ["hello", "...", "goodbye"]);
204    /// ```
205    pub fn new(mut lines: Vec<String>) -> Self {
206        if lines.is_empty() {
207            lines.push(String::new());
208        }
209
210        Self {
211            lines,
212            block: None,
213            style: Style::default(),
214            cursor: (0, 0),
215            tab_len: 4,
216            hard_tab_indent: false,
217            history: History::new(50),
218            cursor_line_style: Style::default().add_modifier(Modifier::UNDERLINED),
219            line_number_style: None,
220            viewport: Viewport::default(),
221            cursor_style: Style::default().add_modifier(Modifier::REVERSED),
222            yank: YankText::default(),
223            #[cfg(feature = "search")]
224            search: Search::default(),
225            alignment: Alignment::Left,
226            placeholder: String::new(),
227            placeholder_style: Style::default().fg(Color::DarkGray),
228            mask: None,
229            selection_start: None,
230            select_style: Style::default().bg(Color::LightBlue),
231        }
232    }
233
234    /// Handle a key input with default key mappings. For default key mappings, see the table in
235    /// [the module document](./index.html).
236    /// `crossterm`, `termion`, and `termwiz` features enable conversion from their own key event types into
237    /// [`Input`] so this method can take the event values directly.
238    /// This method returns if the input modified text contents or not in the textarea.
239    /// ```ignore
240    /// use ratatui_textarea::{TextArea, Key, Input};
241    ///
242    /// let mut textarea = TextArea::default();
243    ///
244    /// // Handle crossterm key events
245    /// let event: crossterm::event::Event = ...;
246    /// textarea.input(event);
247    /// if let crossterm::event::Event::Key(key) = event {
248    ///     textarea.input(key);
249    /// }
250    ///
251    /// // Handle termion key events
252    /// let event: termion::event::Event = ...;
253    /// textarea.input(event);
254    /// if let termion::event::Event::Key(key) = event {
255    ///     textarea.input(key);
256    /// }
257    ///
258    /// // Handle termwiz key events
259    /// let event: termwiz::input::InputEvent = ...;
260    /// textarea.input(event);
261    /// if let termwiz::input::InputEvent::Key(key) = event {
262    ///     textarea.input(key);
263    /// }
264    ///
265    /// // Handle backend-agnostic key input
266    /// let input = Input { key: Key::Char('a'), ctrl: false, alt: false, shift: false };
267    /// let modified = textarea.input(input);
268    /// assert!(modified);
269    /// ```
270    pub fn input(&mut self, input: impl Into<Input>) -> bool {
271        let input = input.into();
272        let modified = match input {
273            Input {
274                key: Key::Char('m'),
275                ctrl: true,
276                alt: false,
277                ..
278            }
279            | Input {
280                key: Key::Char('\n' | '\r'),
281                ctrl: false,
282                alt: false,
283                ..
284            }
285            | Input {
286                key: Key::Enter, ..
287            } => {
288                self.insert_newline();
289                true
290            }
291            Input {
292                key: Key::Char(c),
293                ctrl: false,
294                alt: false,
295                ..
296            } => {
297                self.insert_char(c);
298                true
299            }
300            Input {
301                key: Key::Tab,
302                ctrl: false,
303                alt: false,
304                ..
305            } => self.insert_tab(),
306            Input {
307                key: Key::Char('h'),
308                ctrl: true,
309                alt: false,
310                ..
311            }
312            | Input {
313                key: Key::Backspace,
314                ctrl: false,
315                alt: false,
316                ..
317            } => self.delete_char(),
318            Input {
319                key: Key::Char('d'),
320                ctrl: true,
321                alt: false,
322                ..
323            }
324            | Input {
325                key: Key::Delete,
326                ctrl: false,
327                alt: false,
328                ..
329            } => self.delete_next_char(),
330            Input {
331                key: Key::Char('k'),
332                ctrl: true,
333                alt: false,
334                ..
335            } => self.delete_line_by_end(),
336            Input {
337                key: Key::Char('j'),
338                ctrl: true,
339                alt: false,
340                ..
341            } => self.delete_line_by_head(),
342            Input {
343                key: Key::Char('w'),
344                ctrl: true,
345                alt: false,
346                ..
347            }
348            | Input {
349                key: Key::Char('h'),
350                ctrl: false,
351                alt: true,
352                ..
353            }
354            | Input {
355                key: Key::Backspace,
356                ctrl: false,
357                alt: true,
358                ..
359            } => self.delete_word(),
360            Input {
361                key: Key::Delete,
362                ctrl: false,
363                alt: true,
364                ..
365            }
366            | Input {
367                key: Key::Char('d'),
368                ctrl: false,
369                alt: true,
370                ..
371            } => self.delete_next_word(),
372            Input {
373                key: Key::Char('n'),
374                ctrl: true,
375                alt: false,
376                shift,
377            }
378            | Input {
379                key: Key::Down,
380                ctrl: false,
381                alt: false,
382                shift,
383            } => {
384                self.move_cursor_with_shift(CursorMove::Down, shift);
385                false
386            }
387            Input {
388                key: Key::Char('p'),
389                ctrl: true,
390                alt: false,
391                shift,
392            }
393            | Input {
394                key: Key::Up,
395                ctrl: false,
396                alt: false,
397                shift,
398            } => {
399                self.move_cursor_with_shift(CursorMove::Up, shift);
400                false
401            }
402            Input {
403                key: Key::Char('f'),
404                ctrl: true,
405                alt: false,
406                shift,
407            }
408            | Input {
409                key: Key::Right,
410                ctrl: false,
411                alt: false,
412                shift,
413            } => {
414                self.move_cursor_with_shift(CursorMove::Forward, shift);
415                false
416            }
417            Input {
418                key: Key::Char('b'),
419                ctrl: true,
420                alt: false,
421                shift,
422            }
423            | Input {
424                key: Key::Left,
425                ctrl: false,
426                alt: false,
427                shift,
428            } => {
429                self.move_cursor_with_shift(CursorMove::Back, shift);
430                false
431            }
432            Input {
433                key: Key::Char('a'),
434                ctrl: true,
435                alt: false,
436                shift,
437            }
438            | Input {
439                key: Key::Home,
440                shift,
441                ..
442            }
443            | Input {
444                key: Key::Left | Key::Char('b'),
445                ctrl: true,
446                alt: true,
447                shift,
448            } => {
449                self.move_cursor_with_shift(CursorMove::Head, shift);
450                false
451            }
452            Input {
453                key: Key::Char('e'),
454                ctrl: true,
455                alt: false,
456                shift,
457            }
458            | Input {
459                key: Key::End,
460                shift,
461                ..
462            }
463            | Input {
464                key: Key::Right | Key::Char('f'),
465                ctrl: true,
466                alt: true,
467                shift,
468            } => {
469                self.move_cursor_with_shift(CursorMove::End, shift);
470                false
471            }
472            Input {
473                key: Key::Char('<'),
474                ctrl: false,
475                alt: true,
476                shift,
477            }
478            | Input {
479                key: Key::Up | Key::Char('p'),
480                ctrl: true,
481                alt: true,
482                shift,
483            } => {
484                self.move_cursor_with_shift(CursorMove::Top, shift);
485                false
486            }
487            Input {
488                key: Key::Char('>'),
489                ctrl: false,
490                alt: true,
491                shift,
492            }
493            | Input {
494                key: Key::Down | Key::Char('n'),
495                ctrl: true,
496                alt: true,
497                shift,
498            } => {
499                self.move_cursor_with_shift(CursorMove::Bottom, shift);
500                false
501            }
502            Input {
503                key: Key::Char('f'),
504                ctrl: false,
505                alt: true,
506                shift,
507            }
508            | Input {
509                key: Key::Right,
510                ctrl: true,
511                alt: false,
512                shift,
513            } => {
514                self.move_cursor_with_shift(CursorMove::WordForward, shift);
515                false
516            }
517            Input {
518                key: Key::Char('b'),
519                ctrl: false,
520                alt: true,
521                shift,
522            }
523            | Input {
524                key: Key::Left,
525                ctrl: true,
526                alt: false,
527                shift,
528            } => {
529                self.move_cursor_with_shift(CursorMove::WordBack, shift);
530                false
531            }
532            Input {
533                key: Key::Char(']'),
534                ctrl: false,
535                alt: true,
536                shift,
537            }
538            | Input {
539                key: Key::Char('n'),
540                ctrl: false,
541                alt: true,
542                shift,
543            }
544            | Input {
545                key: Key::Down,
546                ctrl: true,
547                alt: false,
548                shift,
549            } => {
550                self.move_cursor_with_shift(CursorMove::ParagraphForward, shift);
551                false
552            }
553            Input {
554                key: Key::Char('['),
555                ctrl: false,
556                alt: true,
557                shift,
558            }
559            | Input {
560                key: Key::Char('p'),
561                ctrl: false,
562                alt: true,
563                shift,
564            }
565            | Input {
566                key: Key::Up,
567                ctrl: true,
568                alt: false,
569                shift,
570            } => {
571                self.move_cursor_with_shift(CursorMove::ParagraphBack, shift);
572                false
573            }
574            Input {
575                key: Key::Char('u'),
576                ctrl: true,
577                alt: false,
578                ..
579            } => self.undo(),
580            Input {
581                key: Key::Char('r'),
582                ctrl: true,
583                alt: false,
584                ..
585            } => self.redo(),
586            Input {
587                key: Key::Char('y'),
588                ctrl: true,
589                alt: false,
590                ..
591            }
592            | Input {
593                key: Key::Paste, ..
594            } => self.paste(),
595            Input {
596                key: Key::Char('x'),
597                ctrl: true,
598                alt: false,
599                ..
600            }
601            | Input { key: Key::Cut, .. } => self.cut(),
602            Input {
603                key: Key::Char('c'),
604                ctrl: true,
605                alt: false,
606                ..
607            }
608            | Input { key: Key::Copy, .. } => {
609                self.copy();
610                false
611            }
612            Input {
613                key: Key::Char('v'),
614                ctrl: true,
615                alt: false,
616                shift,
617            }
618            | Input {
619                key: Key::PageDown,
620                shift,
621                ..
622            } => {
623                self.scroll_with_shift(Scrolling::PageDown, shift);
624                false
625            }
626            Input {
627                key: Key::Char('v'),
628                ctrl: false,
629                alt: true,
630                shift,
631            }
632            | Input {
633                key: Key::PageUp,
634                shift,
635                ..
636            } => {
637                self.scroll_with_shift(Scrolling::PageUp, shift);
638                false
639            }
640            Input {
641                key: Key::MouseScrollDown,
642                shift,
643                ..
644            } => {
645                self.scroll_with_shift((1, 0).into(), shift);
646                false
647            }
648            Input {
649                key: Key::MouseScrollUp,
650                shift,
651                ..
652            } => {
653                self.scroll_with_shift((-1, 0).into(), shift);
654                false
655            }
656            _ => false,
657        };
658
659        // Check invariants
660        debug_assert!(!self.lines.is_empty(), "no line after {:?}", input);
661        let (r, c) = self.cursor;
662        debug_assert!(
663            self.lines.len() > r,
664            "cursor {:?} exceeds max lines {} after {:?}",
665            self.cursor,
666            self.lines.len(),
667            input,
668        );
669        debug_assert!(
670            self.lines[r].chars().count() >= c,
671            "cursor {:?} exceeds max col {} at line {:?} after {:?}",
672            self.cursor,
673            self.lines[r].chars().count(),
674            self.lines[r],
675            input,
676        );
677
678        modified
679    }
680
681    /// Handle a key input without default key mappings. This method handles only
682    ///
683    /// - Single character input without modifier keys
684    /// - Tab
685    /// - Enter
686    /// - Backspace
687    /// - Delete
688    ///
689    /// This method returns if the input modified text contents or not in the textarea.
690    ///
691    /// This method is useful when you want to define your own key mappings and don't want default key mappings.
692    /// See 'Define your own key mappings' section in [the module document](./index.html).
693    pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool {
694        match input.into() {
695            Input {
696                key: Key::Char(c),
697                ctrl: false,
698                alt: false,
699                ..
700            } => {
701                self.insert_char(c);
702                true
703            }
704            Input {
705                key: Key::Tab,
706                ctrl: false,
707                alt: false,
708                ..
709            } => self.insert_tab(),
710            Input {
711                key: Key::Backspace,
712                ..
713            } => self.delete_char(),
714            Input {
715                key: Key::Delete, ..
716            } => self.delete_next_char(),
717            Input {
718                key: Key::Enter, ..
719            } => {
720                self.insert_newline();
721                true
722            }
723            Input {
724                key: Key::MouseScrollDown,
725                ..
726            } => {
727                self.scroll((1, 0));
728                false
729            }
730            Input {
731                key: Key::MouseScrollUp,
732                ..
733            } => {
734                self.scroll((-1, 0));
735                false
736            }
737            _ => false,
738        }
739    }
740
741    fn push_history(&mut self, kind: EditKind, before: Pos, after_offset: usize) {
742        let (row, col) = self.cursor;
743        let after = Pos::new(row, col, after_offset);
744        let edit = Edit::new(kind, before, after);
745        self.history.push(edit);
746    }
747
748    /// Insert a single character at current cursor position.
749    /// ```
750    /// use ratatui_textarea::TextArea;
751    ///
752    /// let mut textarea = TextArea::default();
753    ///
754    /// textarea.insert_char('a');
755    /// assert_eq!(textarea.lines(), ["a"]);
756    /// ```
757    pub fn insert_char(&mut self, c: char) {
758        if c == '\n' || c == '\r' {
759            self.insert_newline();
760            return;
761        }
762
763        self.delete_selection(false);
764        let (row, col) = self.cursor;
765        let line = &mut self.lines[row];
766        let i = line
767            .char_indices()
768            .nth(col)
769            .map(|(i, _)| i)
770            .unwrap_or(line.len());
771        line.insert(i, c);
772        self.cursor.1 += 1;
773        self.push_history(
774            EditKind::InsertChar(c),
775            Pos::new(row, col, i),
776            i + c.len_utf8(),
777        );
778    }
779
780    /// Insert a string at current cursor position. This method returns if some text was inserted or not in the textarea.
781    /// Both `\n` and `\r\n` are recognized as newlines but `\r` isn't.
782    /// ```
783    /// use ratatui_textarea::TextArea;
784    ///
785    /// let mut textarea = TextArea::default();
786    ///
787    /// textarea.insert_str("hello");
788    /// assert_eq!(textarea.lines(), ["hello"]);
789    ///
790    /// textarea.insert_str(", world\ngoodbye, world");
791    /// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]);
792    /// ```
793    pub fn insert_str<S: AsRef<str>>(&mut self, s: S) -> bool {
794        let modified = self.delete_selection(false);
795        let mut lines: Vec<_> = s
796            .as_ref()
797            .split('\n')
798            .map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
799            .collect();
800        match lines.len() {
801            0 => modified,
802            1 => self.insert_piece(lines.remove(0)),
803            _ => self.insert_chunk(lines),
804        }
805    }
806
807    fn insert_chunk(&mut self, chunk: Vec<String>) -> bool {
808        debug_assert!(chunk.len() > 1, "Chunk size must be > 1: {:?}", chunk);
809
810        let (row, col) = self.cursor;
811        let line = &mut self.lines[row];
812        let i = line
813            .char_indices()
814            .nth(col)
815            .map(|(i, _)| i)
816            .unwrap_or(line.len());
817        let before = Pos::new(row, col, i);
818
819        let (row, col) = (
820            row + chunk.len() - 1,
821            chunk[chunk.len() - 1].chars().count(),
822        );
823        self.cursor = (row, col);
824
825        let end_offset = chunk.last().unwrap().len();
826
827        let edit = EditKind::InsertChunk(chunk);
828        edit.apply(&mut self.lines, &before, &Pos::new(row, col, end_offset));
829
830        self.push_history(edit, before, end_offset);
831        true
832    }
833
834    fn insert_piece(&mut self, s: String) -> bool {
835        if s.is_empty() {
836            return false;
837        }
838
839        let (row, col) = self.cursor;
840        let line = &mut self.lines[row];
841        debug_assert!(
842            !s.contains('\n'),
843            "string given to TextArea::insert_piece must not contain newline: {:?}",
844            line,
845        );
846
847        let i = line
848            .char_indices()
849            .nth(col)
850            .map(|(i, _)| i)
851            .unwrap_or(line.len());
852        line.insert_str(i, &s);
853        let end_offset = i + s.len();
854
855        self.cursor.1 += s.chars().count();
856        self.push_history(EditKind::InsertStr(s), Pos::new(row, col, i), end_offset);
857        true
858    }
859
860    fn delete_range(&mut self, start: Pos, end: Pos, should_yank: bool) {
861        self.cursor = (start.row, start.col);
862
863        if start.row == end.row {
864            let removed = self.lines[start.row]
865                .drain(start.offset..end.offset)
866                .as_str()
867                .to_string();
868            if should_yank {
869                self.yank = removed.clone().into();
870            }
871            self.push_history(EditKind::DeleteStr(removed), end, start.offset);
872            return;
873        }
874
875        let mut deleted = vec![
876            self.lines[start.row]
877                .drain(start.offset..)
878                .as_str()
879                .to_string(),
880        ];
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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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_core::terminal::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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_textarea::TextArea;
2049    /// use ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_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 ratatui_core::buffer::Buffer;
2413        use ratatui_core::layout::Rect;
2414
2415        let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
2416        let r = Rect {
2417            x: 0,
2418            y: 0,
2419            width: 24,
2420            height: 8,
2421        };
2422        let mut b = Buffer::empty(r);
2423        textarea.render(r, &mut b);
2424
2425        textarea.scroll((15, 0));
2426        assert_eq!(textarea.cursor(), (15, 0));
2427        textarea.scroll((-5, 0));
2428        assert_eq!(textarea.cursor(), (15, 0));
2429        textarea.scroll((-5, 0));
2430        assert_eq!(textarea.cursor(), (12, 0));
2431    }
2432}