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<®ex::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}