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, num_digits, 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    /// Get the width in columns of the line number gutter: the digits of the last line number plus one margin column
1965    /// on each side. It is `0` when line numbers are not enabled. Text is drawn to the right of the gutter, so this
1966    /// is the offset to subtract when converting a column within the rendered area into a text column for
1967    /// [`TextArea::screen_to_data`].
1968    /// ```
1969    /// use ratatui_core::style::Style;
1970    /// use ratatui_textarea::TextArea;
1971    ///
1972    /// let mut textarea: TextArea = (1..=10).map(|i| i.to_string()).collect();
1973    /// assert_eq!(textarea.line_number_width(), 0);
1974    ///
1975    /// textarea.set_line_number_style(Style::default());
1976    /// assert_eq!(textarea.line_number_width(), 4); // "10" plus a margin on each side
1977    /// ```
1978    pub fn line_number_width(&self) -> u16 {
1979        if self.line_number_style.is_some() {
1980            u16::from(num_digits(self.lines.len())) + 2 // `+ 2` for margins
1981        } else {
1982            0
1983        }
1984    }
1985
1986    /// Set the placeholder text. The text is set in the textarea when no text is input. Setting a non-empty string `""`
1987    /// enables the placeholder. The default value is an empty string so the placeholder is disabled by default.
1988    /// To customize the text style, see [`TextArea::set_placeholder_style`].
1989    /// ```
1990    /// use ratatui_textarea::TextArea;
1991    ///
1992    /// let mut textarea = TextArea::default();
1993    /// assert_eq!(textarea.placeholder_text(), "");
1994    /// assert!(textarea.placeholder_style().is_none());
1995    ///
1996    /// textarea.set_placeholder_text("Hello");
1997    /// assert_eq!(textarea.placeholder_text(), "Hello");
1998    /// assert!(textarea.placeholder_style().is_some());
1999    /// ```
2000    pub fn set_placeholder_text(&mut self, placeholder: impl Into<String>) {
2001        let placeholder: String = placeholder.into();
2002        if placeholder.is_empty() {
2003            self.placeholder.lines.clear();
2004        } else {
2005            self.placeholder.lines = vec![Line::raw(placeholder)];
2006        }
2007    }
2008
2009    /// Set the style of the placeholder text. The default style is a dark gray text.
2010    /// ```
2011    /// use ratatui_core::style::{Style, Color};
2012    /// use ratatui_textarea::TextArea;
2013    ///
2014    /// let mut textarea = TextArea::default();
2015    /// assert_eq!(textarea.placeholder_style(), None); // When the placeholder is disabled
2016    ///
2017    /// textarea.set_placeholder_text("Enter your message"); // Enable placeholder by setting non-empty text
2018    ///
2019    /// let style = Style::default().bg(Color::Blue);
2020    /// textarea.set_placeholder_style(style);
2021    /// assert_eq!(textarea.placeholder_style(), Some(style));
2022    /// ```
2023    pub fn set_placeholder_style(&mut self, style: Style) {
2024        self.placeholder.style = style;
2025    }
2026
2027    /// Get the placeholder. The placeholder is shown when the textarea is empty.
2028    /// ```
2029    /// use ratatui_textarea::TextArea;
2030    ///
2031    /// let mut textarea = TextArea::default();
2032    /// textarea.set_styled_placeholder("Hello");
2033    /// assert_eq!(textarea.placeholder().lines[0].spans[0].content, "Hello");
2034    /// ```
2035    pub fn placeholder(&self) -> &Text<'_> {
2036        &self.placeholder
2037    }
2038
2039    /// Set the placeholder as a styled [`ratatui_core::text::Text`] value. The placeholder is shown when the textarea
2040    /// is empty. Accepts anything that converts into `Text`: a plain `&str`, a styled `Line`, or a full `Text`.
2041    ///
2042    /// To disable the placeholder, pass an empty `Text`: `textarea.set_styled_placeholder(Text::default())`.
2043    /// Alternatively, [`set_placeholder_text`](Self::set_placeholder_text) called with `""` also clears it.
2044    /// ```
2045    /// use ratatui_core::style::{Color, Style, Stylize};
2046    /// use ratatui_core::text::{Line, Span, Text};
2047    /// use ratatui_textarea::TextArea;
2048    ///
2049    /// let mut textarea = TextArea::default();
2050    ///
2051    /// // Plain string
2052    /// textarea.set_styled_placeholder("Enter your message");
2053    /// assert!(!textarea.placeholder().lines.is_empty());
2054    ///
2055    /// // Styled span
2056    /// textarea.set_styled_placeholder(Line::from(vec![
2057    ///     Span::styled("Required: ", Style::default().fg(Color::Red)),
2058    ///     Span::raw("enter your name"),
2059    /// ]));
2060    ///
2061    /// // Multi-line Text
2062    /// textarea.set_styled_placeholder(Text::from_iter([
2063    ///     Line::raw("Line 1"),
2064    ///     Line::raw("Line 2"),
2065    /// ]));
2066    /// ```
2067    pub fn set_styled_placeholder(&mut self, placeholder: impl Into<Text<'a>>) {
2068        self.placeholder = placeholder.into();
2069    }
2070
2071    /// Get the placeholder text. An empty string means the placeholder is disabled. The default value is an empty string.
2072    /// ```
2073    /// use ratatui_textarea::TextArea;
2074    ///
2075    /// let textarea = TextArea::default();
2076    /// assert_eq!(textarea.placeholder_text(), "");
2077    /// ```
2078    pub fn placeholder_text(&self) -> &'_ str {
2079        let line = match self.placeholder.lines.first() {
2080            Some(line) => line,
2081            None => return "",
2082        };
2083        match line.spans.first() {
2084            Some(span) => &span.content,
2085            None => "",
2086        }
2087    }
2088
2089    /// Get the placeholder style. When the placeholder text is empty, it returns `None` since the placeholder is disabled.
2090    /// The default style is a dark gray text.
2091    /// ```
2092    /// use ratatui_textarea::TextArea;
2093    ///
2094    /// let mut textarea = TextArea::default();
2095    /// assert_eq!(textarea.placeholder_style(), None);
2096    ///
2097    /// textarea.set_placeholder_text("hello");
2098    /// assert!(textarea.placeholder_style().is_some());
2099    /// ```
2100    pub fn placeholder_style(&self) -> Option<Style> {
2101        if self.placeholder.lines.is_empty() {
2102            None
2103        } else {
2104            Some(self.placeholder.style)
2105        }
2106    }
2107
2108    /// Specify a character masking the text. All characters in the textarea will be replaced by this character.
2109    /// This API is useful for making a kind of credentials form such as a password input.
2110    /// ```
2111    /// use ratatui_textarea::TextArea;
2112    ///
2113    /// let mut textarea = TextArea::default();
2114    ///
2115    /// textarea.set_mask_char('*');
2116    /// assert_eq!(textarea.mask_char(), Some('*'));
2117    /// textarea.set_mask_char('●');
2118    /// assert_eq!(textarea.mask_char(), Some('●'));
2119    /// ```
2120    pub fn set_mask_char(&mut self, mask: char) {
2121        self.mask = Some(mask);
2122    }
2123
2124    /// Clear the masking character previously set by [`TextArea::set_mask_char`].
2125    /// ```
2126    /// use ratatui_textarea::TextArea;
2127    ///
2128    /// let mut textarea = TextArea::default();
2129    ///
2130    /// textarea.set_mask_char('*');
2131    /// assert_eq!(textarea.mask_char(), Some('*'));
2132    /// textarea.clear_mask_char();
2133    /// assert_eq!(textarea.mask_char(), None);
2134    /// ```
2135    pub fn clear_mask_char(&mut self) {
2136        self.mask = None;
2137    }
2138
2139    /// Get the character to mask text. When no character is set, `None` is returned.
2140    /// ```
2141    /// use ratatui_textarea::TextArea;
2142    ///
2143    /// let mut textarea = TextArea::default();
2144    ///
2145    /// assert_eq!(textarea.mask_char(), None);
2146    /// textarea.set_mask_char('*');
2147    /// assert_eq!(textarea.mask_char(), Some('*'));
2148    /// ```
2149    pub fn mask_char(&self) -> Option<char> {
2150        self.mask
2151    }
2152
2153    /// Set the style of cursor. By default, a cursor is rendered in the reversed color. Setting the same style as
2154    /// cursor line hides a cursor.
2155    /// ```
2156    /// use ratatui_core::style::{Style, Color};
2157    /// use ratatui_textarea::TextArea;
2158    ///
2159    /// let mut textarea = TextArea::default();
2160    ///
2161    /// let style = Style::default().bg(Color::Red);
2162    /// textarea.set_cursor_style(style);
2163    /// assert_eq!(textarea.cursor_style(), style);
2164    /// ```
2165    pub fn set_cursor_style(&mut self, style: Style) {
2166        self.cursor_style = style;
2167    }
2168
2169    /// Get the style of cursor.
2170    pub fn cursor_style(&self) -> Style {
2171        self.cursor_style
2172    }
2173
2174    /// Get slice of line texts. This method borrows the content, but not moves. Note that the returned slice will
2175    /// never be empty because an empty text means a slice containing one empty line. This is correct since any text
2176    /// file must end with a newline.
2177    /// ```
2178    /// use ratatui_textarea::TextArea;
2179    ///
2180    /// let mut textarea = TextArea::default();
2181    /// assert_eq!(textarea.lines(), [""]);
2182    ///
2183    /// textarea.insert_char('a');
2184    /// assert_eq!(textarea.lines(), ["a"]);
2185    ///
2186    /// textarea.insert_newline();
2187    /// assert_eq!(textarea.lines(), ["a", ""]);
2188    ///
2189    /// textarea.insert_char('b');
2190    /// assert_eq!(textarea.lines(), ["a", "b"]);
2191    /// ```
2192    pub fn lines(&'a self) -> &'a [String] {
2193        &self.lines
2194    }
2195
2196    /// Convert [`TextArea`] instance into line texts.
2197    /// ```
2198    /// use ratatui_textarea::TextArea;
2199    ///
2200    /// let mut textarea = TextArea::default();
2201    ///
2202    /// textarea.insert_char('a');
2203    /// textarea.insert_newline();
2204    /// textarea.insert_char('b');
2205    ///
2206    /// assert_eq!(textarea.into_lines(), ["a", "b"]);
2207    /// ```
2208    pub fn into_lines(self) -> Vec<String> {
2209        self.lines
2210    }
2211
2212    /// Get the current cursor position. 0-base character-wise (row, col) cursor position.
2213    /// ```
2214    /// use ratatui_textarea::TextArea;
2215    ///
2216    /// let mut textarea = TextArea::default();
2217    /// assert_eq!(textarea.cursor(), (0, 0));
2218    ///
2219    /// textarea.insert_char('a');
2220    /// textarea.insert_newline();
2221    /// textarea.insert_char('b');
2222    ///
2223    /// assert_eq!(textarea.cursor(), (1, 1));
2224    /// ```
2225    pub fn cursor(&self) -> DataCursor {
2226        self.cursor
2227    }
2228
2229    pub fn screen_cursor(&self) -> ScreenCursor {
2230        self.array_to_screen(self.cursor)
2231    }
2232
2233    /// Get the scroll position as a 0-base `(row, col)` pair: the display
2234    /// line and column drawn at the top-left of the rendered area.
2235    ///
2236    /// Display lines are counted after soft wrapping, so with
2237    /// [`WrapMode::None`] a display line is a line of the text and
2238    /// otherwise a long line spans several. This is the offset that turns
2239    /// a position within the rendered area into the screen position
2240    /// [`TextArea::screen_to_data`] expects.
2241    ///
2242    /// The value is recorded while rendering, so it is `(0, 0)` until the
2243    /// widget has been drawn at least once.
2244    /// ```
2245    /// use ratatui_core::buffer::Buffer;
2246    /// use ratatui_core::layout::Rect;
2247    /// use ratatui_core::widgets::Widget as _;
2248    /// use ratatui_textarea::{CursorMove, TextArea};
2249    ///
2250    /// let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
2251    /// assert_eq!(textarea.scroll_offset(), (0, 0));
2252    ///
2253    /// // The viewport follows the cursor while drawing.
2254    /// textarea.move_cursor(CursorMove::Bottom);
2255    /// let area = Rect { x: 0, y: 0, width: 8, height: 4 };
2256    /// (&textarea).render(area, &mut Buffer::empty(area));
2257    ///
2258    /// let (row, _) = textarea.scroll_offset();
2259    /// assert_eq!(row, 16, "the last four lines are on screen");
2260    /// ```
2261    pub fn scroll_offset(&self) -> (u16, u16) {
2262        self.viewport.scroll_top()
2263    }
2264
2265    /// Convert a screen position into the position in the text drawn
2266    /// there, accounting for soft wrapping and tab expansion.
2267    ///
2268    /// This is the mapping a mouse click needs. `row` and `col` are 0-base
2269    /// display coordinates counted from the top-left of the *text*, not of
2270    /// the rendered area — add [`TextArea::scroll_offset`] to coordinates
2271    /// taken from the area the widget was drawn into, and subtract
2272    /// [`TextArea::line_number_width`] from the column when line numbers
2273    /// are enabled.
2274    ///
2275    /// Positions outside the text clamp rather than being rejected: a row
2276    /// past the last display line resolves to that line, and a column past
2277    /// the end of a line resolves to its end. A click below or to the
2278    /// right of the text therefore lands where a reader would expect.
2279    /// ```
2280    /// use ratatui_textarea::{CursorMove, DataCursor, TextArea};
2281    ///
2282    /// let mut textarea = TextArea::from(["hello", "world"]);
2283    /// assert_eq!(textarea.screen_to_data(1, 3), DataCursor(1, 3));
2284    ///
2285    /// // Below the text clamps to the last line.
2286    /// assert_eq!(textarea.screen_to_data(99, 0), DataCursor(1, 0));
2287    ///
2288    /// // Move the cursor to a clicked position.
2289    /// let clicked = textarea.screen_to_data(0, 2);
2290    /// textarea.move_cursor(CursorMove::Jump(clicked.0 as u16, clicked.1 as u16));
2291    /// assert_eq!(textarea.cursor(), (0, 2));
2292    /// ```
2293    pub fn screen_to_data(&self, row: usize, col: usize) -> DataCursor {
2294        // Every internal caller passes a row that came out of the screen
2295        // map, so the bound is checked here, at the public boundary.
2296        let row = row.min(self.screen_lines_count().saturating_sub(1));
2297        self.screen_to_array(ScreenCursor {
2298            row,
2299            col,
2300            char: None,
2301            dc: None,
2302        })
2303    }
2304
2305    /// Get the current selection range as a pair of the start position and the end position. The range is bounded
2306    /// inclusively below and exclusively above. The positions are 0-base character-wise (row, col) values.
2307    /// The first element of the pair is always smaller than the second one even when it is ahead of the cursor.
2308    /// When no text is selected, this method returns `None`.
2309    /// ```
2310    /// use ratatui_textarea::TextArea;
2311    /// use ratatui_textarea::CursorMove;
2312    ///
2313    /// let mut textarea = TextArea::from([
2314    ///     "aaa",
2315    ///     "bbb",
2316    ///     "ccc",
2317    /// ]);
2318    ///
2319    /// // It returns `None` when the text selection is not ongoing
2320    /// assert_eq!(textarea.selection_range(), None);
2321    ///
2322    /// textarea.start_selection();
2323    /// assert_eq!(textarea.selection_range(), Some(((0, 0), (0, 0))));
2324    ///
2325    /// textarea.move_cursor(CursorMove::Forward);
2326    /// assert_eq!(textarea.selection_range(), Some(((0, 0), (0, 1))));
2327    ///
2328    /// textarea.move_cursor(CursorMove::Down);
2329    /// assert_eq!(textarea.selection_range(), Some(((0, 0), (1, 1))));
2330    ///
2331    /// // Start selection at (1, 1)
2332    /// textarea.cancel_selection();
2333    /// textarea.start_selection();
2334    ///
2335    /// // The first element of the pair is always smaller than the second one
2336    /// textarea.move_cursor(CursorMove::Back);
2337    /// assert_eq!(textarea.selection_range(), Some(((1, 0), (1, 1))));
2338    /// ```
2339    pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
2340        self.selection_start.map(|pos| {
2341            if pos > self.cursor {
2342                ((self.cursor.0, self.cursor.1), (pos.0, pos.1))
2343            } else {
2344                ((pos.0, pos.1), (self.cursor.0, self.cursor.1))
2345            }
2346        })
2347    }
2348
2349    /// Set text alignment. When [`Alignment::Center`] or [`Alignment::Right`] is set, line number is automatically
2350    /// disabled because those alignments don't work well with line numbers.
2351    /// ```
2352    /// use ratatui_core::layout::Alignment;
2353    /// use ratatui_textarea::TextArea;
2354    ///
2355    /// let mut textarea = TextArea::default();
2356    ///
2357    /// textarea.set_alignment(Alignment::Center);
2358    /// assert_eq!(textarea.alignment(), Alignment::Center);
2359    /// ```
2360    pub fn set_alignment(&mut self, alignment: Alignment) {
2361        if let Alignment::Center | Alignment::Right = alignment {
2362            self.line_number_style = None;
2363        }
2364        self.alignment = alignment;
2365        self.refresh_screen_map();
2366    }
2367
2368    /// Get current text alignment. The default alignment is [`Alignment::Left`].
2369    /// ```
2370    /// use ratatui_core::layout::Alignment;
2371    /// use ratatui_textarea::TextArea;
2372    ///
2373    /// let mut textarea = TextArea::default();
2374    ///
2375    /// assert_eq!(textarea.alignment(), Alignment::Left);
2376    /// ```
2377    pub fn alignment(&self) -> Alignment {
2378        self.alignment
2379    }
2380
2381    /// Set the soft-wrap mode used when rendering text.
2382    pub fn set_wrap_mode(&mut self, mode: WrapMode) {
2383        self.wrap_mode = mode;
2384        self.refresh_screen_map();
2385    }
2386
2387    /// Get the current soft-wrap mode.
2388    pub fn wrap_mode(&self) -> WrapMode {
2389        self.wrap_mode
2390    }
2391
2392    /// Check if the textarea has a empty content.
2393    /// ```
2394    /// use ratatui_textarea::TextArea;
2395    ///
2396    /// let textarea = TextArea::default();
2397    /// assert!(textarea.is_empty());
2398    ///
2399    /// let textarea = TextArea::from(["hello"]);
2400    /// assert!(!textarea.is_empty());
2401    /// ```
2402    pub fn is_empty(&self) -> bool {
2403        self.lines == [""]
2404    }
2405
2406    /// Get the yanked text. Text is automatically yanked when deleting strings by [`TextArea::delete_line_by_head`],
2407    /// [`TextArea::delete_line_by_end`], [`TextArea::delete_word`], [`TextArea::delete_next_word`],
2408    /// [`TextArea::delete_str`], [`TextArea::copy`], and [`TextArea::cut`]. When multiple lines were yanked, they are
2409    /// always joined with `\n`.
2410    /// ```
2411    /// use ratatui_textarea::TextArea;
2412    ///
2413    /// let mut textarea = TextArea::from(["abc"]);
2414    /// textarea.delete_next_word();
2415    /// assert_eq!(textarea.yank_text(), "abc");
2416    ///
2417    /// // Multiple lines are joined with \n
2418    /// let mut textarea = TextArea::from(["abc", "def"]);
2419    /// textarea.delete_str(5);
2420    /// assert_eq!(textarea.yank_text(), "abc\nd");
2421    /// ```
2422    pub fn yank_text(&self) -> String {
2423        self.yank.to_string()
2424    }
2425
2426    /// Set a yanked text. The text can be inserted by [`TextArea::paste`]. `\n` and `\r\n` are recognized as newline
2427    /// but `\r` isn't.
2428    /// ```
2429    /// use ratatui_textarea::TextArea;
2430    ///
2431    /// let mut textarea = TextArea::default();
2432    ///
2433    /// textarea.set_yank_text("hello\nworld");
2434    /// textarea.paste();
2435    /// assert_eq!(textarea.lines(), ["hello", "world"]);
2436    /// ```
2437    pub fn set_yank_text(&mut self, text: impl Into<String>) {
2438        // `str::lines` is not available since it strips a newline at end
2439        let lines: Vec<_> = text
2440            .into()
2441            .split('\n')
2442            .map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
2443            .collect();
2444        self.yank = lines.into();
2445    }
2446
2447    /// Set a regular expression pattern for text search. Setting an empty string stops the text search.
2448    /// When a valid pattern is set, all matches will be highlighted in the textarea. Note that the cursor does not
2449    /// move. To move the cursor, use [`TextArea::search_forward`] and [`TextArea::search_back`].
2450    ///
2451    /// Grammar of regular expression follows [regex crate](https://docs.rs/regex/latest/regex). Patterns don't match
2452    /// to newlines so match passes across no newline.
2453    ///
2454    /// When the pattern is invalid, the search pattern will not be updated and an error will be returned.
2455    ///
2456    /// ```
2457    /// use ratatui_textarea::TextArea;
2458    ///
2459    /// let mut textarea = TextArea::from(["hello, world", "goodbye, world"]);
2460    ///
2461    /// // Search "world"
2462    /// textarea.set_search_pattern("world").unwrap();
2463    ///
2464    /// assert_eq!(textarea.cursor(), (0, 0));
2465    /// textarea.search_forward(false);
2466    /// assert_eq!(textarea.cursor(), (0, 7));
2467    /// textarea.search_forward(false);
2468    /// assert_eq!(textarea.cursor(), (1, 9));
2469    ///
2470    /// // Stop the text search
2471    /// textarea.set_search_pattern("");
2472    ///
2473    /// // Invalid search pattern
2474    /// assert!(textarea.set_search_pattern("(hello").is_err());
2475    /// ```
2476    #[cfg(feature = "search")]
2477    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2478    pub fn set_search_pattern(&mut self, query: impl AsRef<str>) -> Result<(), regex::Error> {
2479        self.search.set_pattern(query.as_ref())
2480    }
2481
2482    /// Get a regular expression which was set by [`TextArea::set_search_pattern`]. When no text search is ongoing, this
2483    /// method returns `None`.
2484    ///
2485    /// ```
2486    /// use ratatui_textarea::TextArea;
2487    ///
2488    /// let mut textarea = TextArea::default();
2489    ///
2490    /// assert!(textarea.search_pattern().is_none());
2491    /// textarea.set_search_pattern("hello+").unwrap();
2492    /// assert!(textarea.search_pattern().is_some());
2493    /// assert_eq!(textarea.search_pattern().unwrap().as_str(), "hello+");
2494    /// ```
2495    #[cfg(feature = "search")]
2496    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2497    pub fn search_pattern(&self) -> Option<&regex::Regex> {
2498        self.search.pat.as_ref()
2499    }
2500
2501    /// Search the pattern set by [`TextArea::set_search_pattern`] forward and move the cursor to the next match
2502    /// position based on the current cursor position. Text search wraps around a text buffer. It returns `true` when
2503    /// some match was found. Otherwise it returns `false`.
2504    ///
2505    /// The `match_cursor` parameter represents if the search matches to the current cursor position or not. When `true`
2506    /// is set and the cursor position matches to the pattern, the cursor will not move. When `false`, the cursor will
2507    /// move to the next match ignoring the match at the current position.
2508    ///
2509    /// ```
2510    /// use ratatui_textarea::TextArea;
2511    ///
2512    /// let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
2513    ///
2514    /// textarea.set_search_pattern("hello+").unwrap();
2515    ///
2516    /// // Move to next position
2517    /// let match_found = textarea.search_forward(false);
2518    /// assert!(match_found);
2519    /// assert_eq!(textarea.cursor(), (1, 0));
2520    ///
2521    /// // Since the cursor position matches to "hello+", it does not move
2522    /// textarea.search_forward(true);
2523    /// assert_eq!(textarea.cursor(), (1, 0));
2524    ///
2525    /// // When `match_current` parameter is set to `false`, match at the cursor position is ignored
2526    /// textarea.search_forward(false);
2527    /// assert_eq!(textarea.cursor(), (2, 0));
2528    ///
2529    /// // Text search wrap around the buffer
2530    /// textarea.search_forward(false);
2531    /// assert_eq!(textarea.cursor(), (0, 0));
2532    ///
2533    /// // `false` is returned when no match was found
2534    /// textarea.set_search_pattern("bye+").unwrap();
2535    /// let match_found = textarea.search_forward(false);
2536    /// assert!(!match_found);
2537    /// ```
2538    #[cfg(feature = "search")]
2539    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2540    pub fn search_forward(&mut self, match_cursor: bool) -> bool {
2541        if let Some(cursor) = self.search.forward(&self.lines, self.cursor, match_cursor) {
2542            self.cursor = cursor.into();
2543            true
2544        } else {
2545            false
2546        }
2547    }
2548
2549    /// Search the pattern set by [`TextArea::set_search_pattern`] backward and move the cursor to the next match
2550    /// position based on the current cursor position. Text search wraps around a text buffer. It returns `true` when
2551    /// some match was found. Otherwise it returns `false`.
2552    ///
2553    /// The `match_cursor` parameter represents if the search matches to the current cursor position or not. When `true`
2554    /// is set and the cursor position matches to the pattern, the cursor will not move. When `false`, the cursor will
2555    /// move to the next match ignoring the match at the current position.
2556    ///
2557    /// ```
2558    /// use ratatui_textarea::TextArea;
2559    ///
2560    /// let mut textarea = TextArea::from(["hello", "helloo", "hellooo"]);
2561    ///
2562    /// textarea.set_search_pattern("hello+").unwrap();
2563    ///
2564    /// // Move to next position with wrapping around the text buffer
2565    /// let match_found = textarea.search_back(false);
2566    /// assert!(match_found);
2567    /// assert_eq!(textarea.cursor(), (2, 0));
2568    ///
2569    /// // Since the cursor position matches to "hello+", it does not move
2570    /// textarea.search_back(true);
2571    /// assert_eq!(textarea.cursor(), (2, 0));
2572    ///
2573    /// // When `match_current` parameter is set to `false`, match at the cursor position is ignored
2574    /// textarea.search_back(false);
2575    /// assert_eq!(textarea.cursor(), (1, 0));
2576    ///
2577    /// // `false` is returned when no match was found
2578    /// textarea.set_search_pattern("bye+").unwrap();
2579    /// let match_found = textarea.search_back(false);
2580    /// assert!(!match_found);
2581    /// ```
2582    #[cfg(feature = "search")]
2583    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2584    pub fn search_back(&mut self, match_cursor: bool) -> bool {
2585        if let Some(cursor) = self.search.back(&self.lines, self.cursor, match_cursor) {
2586            self.cursor = cursor.into();
2587            true
2588        } else {
2589            false
2590        }
2591    }
2592
2593    /// Get the text style at matches of text search. The default style is colored with blue in background.
2594    ///
2595    /// ```
2596    /// use ratatui_core::style::{Style, Color};
2597    /// use ratatui_textarea::TextArea;
2598    ///
2599    /// let textarea = TextArea::default();
2600    ///
2601    /// assert_eq!(textarea.search_style(), Style::default().bg(Color::Blue));
2602    /// ```
2603    #[cfg(feature = "search")]
2604    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2605    pub fn search_style(&self) -> Style {
2606        self.search.style
2607    }
2608
2609    /// Set the text style at matches of text search. The default style is colored with blue in background.
2610    ///
2611    /// ```
2612    /// use ratatui_core::style::{Style, Color};
2613    /// use ratatui_textarea::TextArea;
2614    ///
2615    /// let mut textarea = TextArea::default();
2616    ///
2617    /// let red_bg = Style::default().bg(Color::Red);
2618    /// textarea.set_search_style(red_bg);
2619    ///
2620    /// assert_eq!(textarea.search_style(), red_bg);
2621    /// ```
2622    #[cfg(feature = "search")]
2623    #[cfg_attr(docsrs, doc(cfg(feature = "search")))]
2624    pub fn set_search_style(&mut self, style: Style) {
2625        self.search.style = style;
2626    }
2627
2628    /// Scroll the textarea. See [`Scrolling`] for the argument.
2629    /// The cursor will not move until it goes out the viewport. When the cursor position is outside the viewport after scroll,
2630    /// the cursor position will be adjusted to stay in the viewport using the same logic as [`CursorMove::InViewport`].
2631    ///
2632    /// ```
2633    /// # use ratatui_core::buffer::Buffer;
2634    /// # use ratatui_core::layout::Rect;
2635    /// # use ratatui_core::widgets::Widget as _;
2636    /// use ratatui_textarea::TextArea;
2637    ///
2638    /// // Let's say terminal height is 8.
2639    ///
2640    /// // Create textarea with 20 lines "0", "1", "2", "3", ...
2641    /// let mut textarea: TextArea = (0..20).into_iter().map(|i| i.to_string()).collect();
2642    /// # // Call `render` at least once to populate terminal size
2643    /// # let r = Rect { x: 0, y: 0, width: 24, height: 8 };
2644    /// # let mut b = Buffer::empty(r.clone());
2645    /// # textarea.render(r, &mut b);
2646    ///
2647    /// // Scroll down by 15 lines. Since terminal height is 8, cursor will go out
2648    /// // the viewport.
2649    /// textarea.scroll((15, 0));
2650    /// // So the cursor position was updated to stay in the viewport after the scrolling.
2651    /// assert_eq!(textarea.cursor(), (15, 0));
2652    ///
2653    /// // Scroll up by 5 lines. Since the scroll amount is smaller than the terminal
2654    /// // height, cursor position will not be updated.
2655    /// textarea.scroll((-5, 0));
2656    /// assert_eq!(textarea.cursor(), (15, 0));
2657    ///
2658    /// // Scroll up by 5 lines again. The terminal height is 8. So a cursor reaches to
2659    /// // the top of viewport after scrolling up by 7 lines. Since we have already
2660    /// // scrolled up by 5 lines, scrolling up by 5 lines again makes the cursor overrun
2661    /// // the viewport by 5 - 2 = 3 lines. To keep the cursor stay in the viewport, the
2662    /// // cursor position will be adjusted from line 15 to line 12.
2663    /// textarea.scroll((-5, 0));
2664    /// assert_eq!(textarea.cursor(), (12, 0));
2665    /// ```
2666    pub fn scroll(&mut self, scrolling: impl Into<Scrolling>) {
2667        self.scroll_with_shift(scrolling.into(), self.selection_start.is_some());
2668    }
2669
2670    fn scroll_with_shift(&mut self, scrolling: Scrolling, shift: bool) {
2671        if shift && self.selection_start.is_none() {
2672            self.selection_start = Some(self.cursor);
2673        }
2674        scrolling.scroll(&mut self.viewport);
2675        self.move_cursor_with_shift(CursorMove::InViewport, shift);
2676    }
2677}
2678
2679#[cfg(test)]
2680mod tests {
2681    use super::*;
2682
2683    // Separate tests for tui-rs support
2684    #[test]
2685    fn scroll() {
2686        use ratatui_core::buffer::Buffer;
2687        use ratatui_core::layout::Rect;
2688
2689        let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
2690        let r = Rect {
2691            x: 0,
2692            y: 0,
2693            width: 24,
2694            height: 8,
2695        };
2696        let mut b = Buffer::empty(r);
2697        textarea.render(r, &mut b);
2698
2699        textarea.scroll((15, 0));
2700        assert_eq!(textarea.cursor(), (15, 0));
2701        textarea.scroll((-5, 0));
2702        assert_eq!(textarea.cursor(), (15, 0));
2703        textarea.scroll((-5, 0));
2704        assert_eq!(textarea.cursor(), (12, 0));
2705    }
2706}