Skip to main content

ratatui_textarea/
textarea.rs

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