use crate::cursor::CursorMove;
use crate::highlight::LineHighlighter;
use crate::history::{Edit, EditKind, History};
use crate::input::{Input, Key};
use crate::ratatui::layout::Alignment;
use crate::ratatui::style::{Color, Modifier, Style};
use crate::ratatui::widgets::{Block, Widget};
use crate::scroll::Scrolling;
#[cfg(feature = "search")]
use crate::search::Search;
use crate::util::spaces;
use crate::widget::{Renderer, Viewport};
use crate::word::{find_word_end_forward, find_word_start_backward};
#[cfg(feature = "ratatui")]
use ratatui::text::Line;
#[cfg(feature = "tuirs")]
use tui::text::Spans as Line;
use unicode_width::UnicodeWidthChar as _;
#[derive(Clone, Debug)]
pub struct TextArea<'a> {
lines: Vec<String>,
block: Option<Block<'a>>,
style: Style,
cursor: (usize, usize), tab_len: u8,
hard_tab_indent: bool,
history: History,
cursor_line_style: Style,
line_number_style: Option<Style>,
pub(crate) viewport: Viewport,
cursor_style: Style,
yank: String,
#[cfg(feature = "search")]
search: Search,
alignment: Alignment,
pub(crate) placeholder: String,
pub(crate) placeholder_style: Style,
mask: Option<char>,
}
impl<'a, I> From<I> for TextArea<'a>
where
I: IntoIterator,
I::Item: Into<String>,
{
fn from(i: I) -> Self {
Self::new(i.into_iter().map(|s| s.into()).collect::<Vec<String>>())
}
}
impl<'a, S: Into<String>> FromIterator<S> for TextArea<'a> {
fn from_iter<I: IntoIterator<Item = S>>(iter: I) -> Self {
iter.into()
}
}
impl<'a> Default for TextArea<'a> {
fn default() -> Self {
Self::new(vec![String::new()])
}
}
impl<'a> TextArea<'a> {
pub fn new(mut lines: Vec<String>) -> Self {
if lines.is_empty() {
lines.push(String::new());
}
Self {
lines,
block: None,
style: Style::default(),
cursor: (0, 0),
tab_len: 4,
hard_tab_indent: false,
history: History::new(50),
cursor_line_style: Style::default().add_modifier(Modifier::UNDERLINED),
line_number_style: None,
viewport: Viewport::default(),
cursor_style: Style::default().add_modifier(Modifier::REVERSED),
yank: String::new(),
#[cfg(feature = "search")]
search: Search::default(),
alignment: Alignment::Left,
placeholder: String::new(),
placeholder_style: Style::default().fg(Color::DarkGray),
mask: None,
}
}
pub fn input(&mut self, input: impl Into<Input>) -> bool {
let input = input.into();
let modified = match input {
Input {
key: Key::Char('m'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Char('\n' | '\r'),
ctrl: false,
alt: false,
}
| Input {
key: Key::Enter, ..
} => {
self.insert_newline();
true
}
Input {
key: Key::Char(c),
ctrl: false,
alt: false,
} => {
self.insert_char(c);
true
}
Input {
key: Key::Tab,
ctrl: false,
alt: false,
} => self.insert_tab(),
Input {
key: Key::Char('h'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Backspace,
ctrl: false,
alt: false,
} => self.delete_char(),
Input {
key: Key::Char('d'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Delete,
ctrl: false,
alt: false,
} => self.delete_next_char(),
Input {
key: Key::Char('k'),
ctrl: true,
alt: false,
} => self.delete_line_by_end(),
Input {
key: Key::Char('j'),
ctrl: true,
alt: false,
} => self.delete_line_by_head(),
Input {
key: Key::Char('w'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Char('h'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Backspace,
ctrl: false,
alt: true,
} => self.delete_word(),
Input {
key: Key::Delete,
ctrl: false,
alt: true,
}
| Input {
key: Key::Char('d'),
ctrl: false,
alt: true,
} => self.delete_next_word(),
Input {
key: Key::Char('n'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Down,
ctrl: false,
alt: false,
} => {
self.move_cursor(CursorMove::Down);
false
}
Input {
key: Key::Char('p'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Up,
ctrl: false,
alt: false,
} => {
self.move_cursor(CursorMove::Up);
false
}
Input {
key: Key::Char('f'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Right,
ctrl: false,
alt: false,
} => {
self.move_cursor(CursorMove::Forward);
false
}
Input {
key: Key::Char('b'),
ctrl: true,
alt: false,
}
| Input {
key: Key::Left,
ctrl: false,
alt: false,
} => {
self.move_cursor(CursorMove::Back);
false
}
Input {
key: Key::Char('a'),
ctrl: true,
alt: false,
}
| Input { key: Key::Home, .. }
| Input {
key: Key::Left | Key::Char('b'),
ctrl: true,
alt: true,
} => {
self.move_cursor(CursorMove::Head);
false
}
Input {
key: Key::Char('e'),
ctrl: true,
alt: false,
}
| Input { key: Key::End, .. }
| Input {
key: Key::Right | Key::Char('f'),
ctrl: true,
alt: true,
} => {
self.move_cursor(CursorMove::End);
false
}
Input {
key: Key::Char('<'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Up | Key::Char('p'),
ctrl: true,
alt: true,
} => {
self.move_cursor(CursorMove::Top);
false
}
Input {
key: Key::Char('>'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Down | Key::Char('n'),
ctrl: true,
alt: true,
} => {
self.move_cursor(CursorMove::Bottom);
false
}
Input {
key: Key::Char('f'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Right,
ctrl: true,
alt: false,
} => {
self.move_cursor(CursorMove::WordForward);
false
}
Input {
key: Key::Char('b'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Left,
ctrl: true,
alt: false,
} => {
self.move_cursor(CursorMove::WordBack);
false
}
Input {
key: Key::Char(']'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Char('n'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Down,
ctrl: true,
alt: false,
} => {
self.move_cursor(CursorMove::ParagraphForward);
false
}
Input {
key: Key::Char('['),
ctrl: false,
alt: true,
}
| Input {
key: Key::Char('p'),
ctrl: false,
alt: true,
}
| Input {
key: Key::Up,
ctrl: true,
alt: false,
} => {
self.move_cursor(CursorMove::ParagraphBack);
false
}
Input {
key: Key::Char('u'),
ctrl: true,
alt: false,
} => self.undo(),
Input {
key: Key::Char('r'),
ctrl: true,
alt: false,
} => self.redo(),
Input {
key: Key::Char('y'),
ctrl: true,
alt: false,
} => self.paste(),
Input {
key: Key::Char('v'),
ctrl: true,
alt: false,
}
| Input {
key: Key::PageDown, ..
} => {
self.scroll(Scrolling::PageDown);
false
}
Input {
key: Key::Char('v'),
ctrl: false,
alt: true,
}
| Input {
key: Key::PageUp, ..
} => {
self.scroll(Scrolling::PageUp);
false
}
Input {
key: Key::MouseScrollDown,
..
} => {
self.scroll((1, 0));
false
}
Input {
key: Key::MouseScrollUp,
..
} => {
self.scroll((-1, 0));
false
}
_ => false,
};
debug_assert!(!self.lines.is_empty(), "no line after {:?}", input);
let (r, c) = self.cursor;
debug_assert!(
self.lines.len() > r,
"cursor {:?} exceeds max lines {} after {:?}",
self.cursor,
self.lines.len(),
input,
);
debug_assert!(
self.lines[r].chars().count() >= c,
"cursor {:?} exceeds max col {} at line {:?} after {:?}",
self.cursor,
self.lines[r].chars().count(),
self.lines[r],
input,
);
modified
}
pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool {
match input.into() {
Input {
key: Key::Char(c),
ctrl: false,
alt: false,
} => {
self.insert_char(c);
true
}
Input {
key: Key::Tab,
ctrl: false,
alt: false,
} => self.insert_tab(),
Input {
key: Key::Backspace,
..
} => self.delete_char(),
Input {
key: Key::Delete, ..
} => self.delete_next_char(),
Input {
key: Key::Enter, ..
} => {
self.insert_newline();
true
}
Input {
key: Key::MouseScrollDown,
..
} => {
self.scroll((1, 0));
false
}
Input {
key: Key::MouseScrollUp,
..
} => {
self.scroll((-1, 0));
false
}
_ => false,
}
}
fn push_history(&mut self, kind: EditKind, cursor_before: (usize, usize)) {
let edit = Edit::new(kind, cursor_before, self.cursor);
self.history.push(edit);
}
pub fn insert_char(&mut self, c: char) {
let (row, col) = self.cursor;
let line = &mut self.lines[row];
let i = line
.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
line.insert(i, c);
self.cursor.1 += 1;
self.push_history(EditKind::InsertChar(c, i), (row, col));
}
pub fn insert_str<S: Into<String>>(&mut self, s: S) -> bool {
let s = s.into();
if s.is_empty() {
return false;
}
let (row, col) = self.cursor;
let line = &mut self.lines[row];
debug_assert!(
!line.contains('\n'),
"string given to insert_str must not contain newline: {:?}",
line,
);
let i = line
.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
line.insert_str(i, &s);
self.cursor.1 += s.chars().count();
self.push_history(EditKind::Insert(s, i), (row, col));
true
}
pub fn delete_str(&mut self, col: usize, chars: usize) -> bool {
if chars == 0 {
return false;
}
let cursor_before = self.cursor;
let row = cursor_before.0;
let line = &mut self.lines[row];
if let Some((i, _)) = line.char_indices().nth(col) {
let bytes = line[i..]
.char_indices()
.nth(chars)
.map(|(i, _)| i)
.unwrap_or_else(|| line[i..].len());
let removed = line[i..i + bytes].to_string();
line.replace_range(i..i + bytes, "");
self.cursor = (row, col);
self.push_history(EditKind::Remove(removed.clone(), i), cursor_before);
self.yank = removed;
true
} else {
false
}
}
pub fn insert_tab(&mut self) -> bool {
if self.tab_len == 0 {
return false;
}
if self.hard_tab_indent {
return self.insert_str("\t");
}
let (row, col) = self.cursor;
let width: usize = self.lines[row]
.chars()
.take(col)
.map(|c| c.width().unwrap_or(0))
.sum();
let len = self.tab_len - (width % self.tab_len as usize) as u8;
self.insert_str(spaces(len))
}
pub fn insert_newline(&mut self) {
let (row, col) = self.cursor;
let line = &mut self.lines[row];
let idx = line
.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
let next_line = line[idx..].to_string();
line.truncate(idx);
self.lines.insert(row + 1, next_line);
self.cursor = (row + 1, 0);
self.push_history(EditKind::InsertNewline(idx), (row, col));
}
pub fn delete_newline(&mut self) -> bool {
let (row, col) = self.cursor;
if row == 0 {
return false;
}
let line = self.lines.remove(row);
let prev_line = &mut self.lines[row - 1];
let prev_line_end = prev_line.len();
self.cursor = (row - 1, prev_line.chars().count());
prev_line.push_str(&line);
self.push_history(EditKind::DeleteNewline(prev_line_end), (row, col));
true
}
pub fn delete_char(&mut self) -> bool {
let (row, col) = self.cursor;
if col == 0 {
return self.delete_newline();
}
let line = &mut self.lines[row];
if let Some((i, c)) = line.char_indices().nth(col - 1) {
line.remove(i);
self.cursor.1 -= 1;
self.push_history(EditKind::DeleteChar(c, i), (row, col));
true
} else {
false
}
}
pub fn delete_next_char(&mut self) -> bool {
let before = self.cursor;
self.move_cursor(CursorMove::Forward);
if before == self.cursor {
return false; }
self.delete_char()
}
pub fn delete_line_by_end(&mut self) -> bool {
if self.delete_str(self.cursor.1, usize::MAX) {
return true;
}
self.delete_next_char() }
pub fn delete_line_by_head(&mut self) -> bool {
if self.delete_str(0, self.cursor.1) {
return true;
}
self.delete_newline()
}
pub fn delete_word(&mut self) -> bool {
let (r, c) = self.cursor;
if let Some(col) = find_word_start_backward(&self.lines[r], c) {
self.delete_str(col, c - col)
} else if c > 0 {
self.delete_str(0, c)
} else {
self.delete_newline()
}
}
pub fn delete_next_word(&mut self) -> bool {
let (r, c) = self.cursor;
let line = &self.lines[r];
if let Some(col) = find_word_end_forward(line, c) {
self.delete_str(c, col - c)
} else {
let end_col = line.chars().count();
if c < end_col {
self.delete_str(c, end_col - c)
} else if r + 1 < self.lines.len() {
self.cursor = (r + 1, 0);
self.delete_newline()
} else {
false
}
}
}
pub fn paste(&mut self) -> bool {
self.insert_str(self.yank.to_string())
}
pub fn move_cursor(&mut self, m: CursorMove) {
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
self.cursor = cursor;
}
}
pub fn undo(&mut self) -> bool {
if let Some(cursor) = self.history.undo(&mut self.lines) {
self.cursor = cursor;
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if let Some(cursor) = self.history.redo(&mut self.lines) {
self.cursor = cursor;
true
} else {
false
}
}
pub(crate) fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Line<'b> {
let mut hl = LineHighlighter::new(line, self.cursor_style, self.tab_len, self.mask);
if let Some(style) = self.line_number_style {
hl.line_number(row, lnum_len, style);
}
if row == self.cursor.0 {
hl.cursor_line(self.cursor.1, self.cursor_line_style);
}
#[cfg(feature = "search")]
if let Some(matches) = self.search.matches(line) {
hl.search(matches, self.search.style);
}
hl.into_spans()
}
pub fn widget(&'a self) -> impl Widget + 'a {
Renderer::new(self)
}
pub fn set_style(&mut self, style: Style) {
self.style = style;
}
pub fn style(&self) -> Style {
self.style
}
pub fn set_block(&mut self, block: Block<'a>) {
self.block = Some(block);
}
pub fn remove_block(&mut self) {
self.block = None;
}
pub fn block<'s>(&'s self) -> Option<&'s Block<'a>> {
self.block.as_ref()
}
pub fn set_tab_length(&mut self, len: u8) {
self.tab_len = len;
}
pub fn tab_length(&self) -> u8 {
self.tab_len
}
pub fn set_hard_tab_indent(&mut self, enabled: bool) {
self.hard_tab_indent = enabled;
}
pub fn hard_tab_indent(&self) -> bool {
self.hard_tab_indent
}
pub fn indent(&self) -> &'static str {
if self.hard_tab_indent {
"\t"
} else {
spaces(self.tab_len)
}
}
pub fn set_max_histories(&mut self, max: usize) {
self.history = History::new(max);
}
pub fn max_histories(&self) -> usize {
self.history.max_items()
}
pub fn set_cursor_line_style(&mut self, style: Style) {
self.cursor_line_style = style;
}
pub fn cursor_line_style(&self) -> Style {
self.cursor_line_style
}
pub fn set_line_number_style(&mut self, style: Style) {
self.line_number_style = Some(style);
}
pub fn remove_line_number(&mut self) {
self.line_number_style = None;
}
pub fn line_number_style(&self) -> Option<Style> {
self.line_number_style
}
pub fn set_placeholder_text(&mut self, placeholder: impl Into<String>) {
self.placeholder = placeholder.into();
}
pub fn set_placeholder_style(&mut self, style: Style) {
self.placeholder_style = style;
}
pub fn placeholder_text(&self) -> &'_ str {
self.placeholder.as_str()
}
pub fn placeholder_style(&self) -> Option<Style> {
if self.placeholder.is_empty() {
None
} else {
Some(self.placeholder_style)
}
}
pub fn set_mask_char(&mut self, mask: char) {
self.mask = Some(mask);
}
pub fn clear_mask_char(&mut self) {
self.mask = None;
}
pub fn mask_char(&self) -> Option<char> {
self.mask
}
pub fn set_cursor_style(&mut self, style: Style) {
self.cursor_style = style;
}
pub fn cursor_style(&self) -> Style {
self.cursor_style
}
pub fn lines(&'a self) -> &'a [String] {
&self.lines
}
pub fn into_lines(self) -> Vec<String> {
self.lines
}
pub fn cursor(&self) -> (usize, usize) {
self.cursor
}
pub fn set_alignment(&mut self, alignment: Alignment) {
if let Alignment::Center | Alignment::Right = alignment {
self.line_number_style = None;
}
self.alignment = alignment;
}
pub fn alignment(&self) -> Alignment {
self.alignment
}
pub fn is_empty(&self) -> bool {
self.lines == [""]
}
pub fn yank_text(&'a self) -> &'a str {
&self.yank
}
pub fn set_yank_text(&mut self, text: impl Into<String>) {
self.yank = text.into();
}
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub fn set_search_pattern(&mut self, query: impl AsRef<str>) -> Result<(), regex::Error> {
self.search.set_pattern(query.as_ref())
}
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub fn search_pattern(&self) -> Option<®ex::Regex> {
self.search.pat.as_ref()
}
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub fn search_forward(&mut self, match_cursor: bool) -> bool {
if let Some(cursor) = self.search.forward(&self.lines, self.cursor, match_cursor) {
self.cursor = cursor;
true
} else {
false
}
}
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub fn search_back(&mut self, match_cursor: bool) -> bool {
if let Some(cursor) = self.search.back(&self.lines, self.cursor, match_cursor) {
self.cursor = cursor;
true
} else {
false
}
}
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub fn search_style(&self) -> Style {
self.search.style
}
#[cfg(feature = "search")]
#[cfg_attr(docsrs, doc(cfg(feature = "search")))]
pub fn set_search_style(&mut self, style: Style) {
self.search.style = style;
}
pub fn scroll(&mut self, scrolling: impl Into<Scrolling>) {
scrolling.into().scroll(&mut self.viewport);
self.move_cursor(CursorMove::InViewport);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scroll() {
use crate::ratatui::buffer::Buffer;
use crate::ratatui::layout::Rect;
use crate::ratatui::widgets::Widget;
let mut textarea: TextArea = (0..20).map(|i| i.to_string()).collect();
let r = Rect {
x: 0,
y: 0,
width: 24,
height: 8,
};
let mut b = Buffer::empty(r);
textarea.widget().render(r, &mut b);
textarea.scroll((15, 0));
assert_eq!(textarea.cursor(), (15, 0));
textarea.scroll((-5, 0));
assert_eq!(textarea.cursor(), (15, 0));
textarea.scroll((-5, 0));
assert_eq!(textarea.cursor(), (12, 0));
}
}