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