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, Pos};
use crate::widget::Viewport;
use crate::word::{find_word_exclusive_end_forward, find_word_start_backward};
#[cfg(feature = "ratatui")]
use ratatui::text::Line;
use std::cmp::Ordering;
use std::fmt;
#[cfg(feature = "tuirs")]
use tui::text::Spans as Line;
use unicode_width::UnicodeWidthChar as _;
#[derive(Debug, Clone)]
enum YankText {
Piece(String),
Chunk(Vec<String>),
}
impl Default for YankText {
fn default() -> Self {
Self::Piece(String::new())
}
}
impl From<String> for YankText {
fn from(s: String) -> Self {
Self::Piece(s)
}
}
impl From<Vec<String>> for YankText {
fn from(mut c: Vec<String>) -> Self {
match c.len() {
0 => Self::default(),
1 => Self::Piece(c.remove(0)),
_ => Self::Chunk(c),
}
}
}
impl fmt::Display for YankText {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Piece(s) => write!(f, "{}", s),
Self::Chunk(ss) => write!(f, "{}", ss.join("\n")),
}
}
}
#[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,
pub(crate) cursor_style: Style,
yank: YankText,
#[cfg(feature = "search")]
search: Search,
alignment: Alignment,
pub(crate) placeholder: String,
pub(crate) placeholder_style: Style,
mask: Option<char>,
selection_start: Option<(usize, usize)>,
select_style: Style,
}
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: YankText::default(),
#[cfg(feature = "search")]
search: Search::default(),
alignment: Alignment::Left,
placeholder: String::new(),
placeholder_style: Style::default().fg(Color::DarkGray),
mask: None,
selection_start: None,
select_style: Style::default().bg(Color::LightBlue),
}
}
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,
shift,
}
| Input {
key: Key::Down,
ctrl: false,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Down, shift);
false
}
Input {
key: Key::Char('p'),
ctrl: true,
alt: false,
shift,
}
| Input {
key: Key::Up,
ctrl: false,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Up, shift);
false
}
Input {
key: Key::Char('f'),
ctrl: true,
alt: false,
shift,
}
| Input {
key: Key::Right,
ctrl: false,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Forward, shift);
false
}
Input {
key: Key::Char('b'),
ctrl: true,
alt: false,
shift,
}
| Input {
key: Key::Left,
ctrl: false,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Back, shift);
false
}
Input {
key: Key::Char('a'),
ctrl: true,
alt: false,
shift,
}
| Input {
key: Key::Home,
shift,
..
}
| Input {
key: Key::Left | Key::Char('b'),
ctrl: true,
alt: true,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Head, shift);
false
}
Input {
key: Key::Char('e'),
ctrl: true,
alt: false,
shift,
}
| Input {
key: Key::End,
shift,
..
}
| Input {
key: Key::Right | Key::Char('f'),
ctrl: true,
alt: true,
shift,
} => {
self.move_cursor_with_shift(CursorMove::End, shift);
false
}
Input {
key: Key::Char('<'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Up | Key::Char('p'),
ctrl: true,
alt: true,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Top, shift);
false
}
Input {
key: Key::Char('>'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Down | Key::Char('n'),
ctrl: true,
alt: true,
shift,
} => {
self.move_cursor_with_shift(CursorMove::Bottom, shift);
false
}
Input {
key: Key::Char('f'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Right,
ctrl: true,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::WordForward, shift);
false
}
Input {
key: Key::Char('b'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Left,
ctrl: true,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::WordBack, shift);
false
}
Input {
key: Key::Char(']'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Char('n'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Down,
ctrl: true,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::ParagraphForward, shift);
false
}
Input {
key: Key::Char('['),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Char('p'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::Up,
ctrl: true,
alt: false,
shift,
} => {
self.move_cursor_with_shift(CursorMove::ParagraphBack, shift);
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,
..
}
| Input {
key: Key::Paste, ..
} => self.paste(),
Input {
key: Key::Char('x'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Cut, .. } => self.cut(),
Input {
key: Key::Char('c'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Copy, .. } => {
self.copy();
false
}
Input {
key: Key::Char('v'),
ctrl: true,
alt: false,
shift,
}
| Input {
key: Key::PageDown,
shift,
..
} => {
self.scroll_with_shift(Scrolling::PageDown, shift);
false
}
Input {
key: Key::Char('v'),
ctrl: false,
alt: true,
shift,
}
| Input {
key: Key::PageUp,
shift,
..
} => {
self.scroll_with_shift(Scrolling::PageUp, shift);
false
}
Input {
key: Key::MouseScrollDown,
shift,
..
} => {
self.scroll_with_shift((1, 0).into(), shift);
false
}
Input {
key: Key::MouseScrollUp,
shift,
..
} => {
self.scroll_with_shift((-1, 0).into(), shift);
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, before: Pos, after_offset: usize) {
let (row, col) = self.cursor;
let after = Pos::new(row, col, after_offset);
let edit = Edit::new(kind, before, after);
self.history.push(edit);
}
pub fn insert_char(&mut self, c: char) {
if c == '\n' || c == '\r' {
self.insert_newline();
return;
}
self.delete_selection(false);
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),
Pos::new(row, col, i),
i + c.len_utf8(),
);
}
pub fn insert_str<S: AsRef<str>>(&mut self, s: S) -> bool {
let modified = self.delete_selection(false);
let mut lines: Vec<_> = s
.as_ref()
.split('\n')
.map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
.collect();
match lines.len() {
0 => modified,
1 => self.insert_piece(lines.remove(0)),
_ => self.insert_chunk(lines),
}
}
fn insert_chunk(&mut self, chunk: Vec<String>) -> bool {
debug_assert!(chunk.len() > 1, "Chunk size must be > 1: {:?}", chunk);
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());
let before = Pos::new(row, col, i);
let (row, col) = (
row + chunk.len() - 1,
chunk[chunk.len() - 1].chars().count(),
);
self.cursor = (row, col);
let end_offset = chunk.last().unwrap().len();
let edit = EditKind::InsertChunk(chunk);
edit.apply(&mut self.lines, &before, &Pos::new(row, col, end_offset));
self.push_history(edit, before, end_offset);
true
}
fn insert_piece(&mut self, s: String) -> bool {
if s.is_empty() {
return false;
}
let (row, col) = self.cursor;
let line = &mut self.lines[row];
debug_assert!(
!s.contains('\n'),
"string given to TextArea::insert_piece must not contain newline: {:?}",
line,
);
let i = line
.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
line.insert_str(i, &s);
let end_offset = i + s.len();
self.cursor.1 += s.chars().count();
self.push_history(EditKind::InsertStr(s), Pos::new(row, col, i), end_offset);
true
}
fn delete_range(&mut self, start: Pos, end: Pos, should_yank: bool) {
self.cursor = (start.row, start.col);
if start.row == end.row {
let removed = self.lines[start.row]
.drain(start.offset..end.offset)
.as_str()
.to_string();
if should_yank {
self.yank = removed.clone().into();
}
self.push_history(EditKind::DeleteStr(removed), end, start.offset);
return;
}
let mut deleted = vec![self.lines[start.row]
.drain(start.offset..)
.as_str()
.to_string()];
deleted.extend(self.lines.drain(start.row + 1..end.row));
if start.row + 1 < self.lines.len() {
let mut last_line = self.lines.remove(start.row + 1);
self.lines[start.row].push_str(&last_line[end.offset..]);
last_line.truncate(end.offset);
deleted.push(last_line);
}
if should_yank {
self.yank = YankText::Chunk(deleted.clone());
}
let edit = if deleted.len() == 1 {
EditKind::DeleteStr(deleted.remove(0))
} else {
EditKind::DeleteChunk(deleted)
};
self.push_history(edit, end, start.offset);
}
pub fn delete_str(&mut self, chars: usize) -> bool {
if self.delete_selection(false) {
return true;
}
if chars == 0 {
return false;
}
let (start_row, start_col) = self.cursor;
let mut remaining = chars;
let mut find_end = move |line: &str| {
let mut col = 0usize;
for (i, _) in line.char_indices() {
if remaining == 0 {
return Some((i, col));
}
col += 1;
remaining -= 1;
}
if remaining == 0 {
Some((line.len(), col))
} else {
remaining -= 1;
None
}
};
let line = &self.lines[start_row];
let start_offset = {
line.char_indices()
.nth(start_col)
.map(|(i, _)| i)
.unwrap_or(line.len())
};
if let Some((offset_delta, col_delta)) = find_end(&line[start_offset..]) {
let end_offset = start_offset + offset_delta;
let end_col = start_col + col_delta;
let removed = self.lines[start_row]
.drain(start_offset..end_offset)
.as_str()
.to_string();
self.yank = removed.clone().into();
self.push_history(
EditKind::DeleteStr(removed),
Pos::new(start_row, end_col, end_offset),
start_offset,
);
return true;
}
let mut r = start_row + 1;
let mut offset = 0;
let mut col = 0;
while r < self.lines.len() {
let line = &self.lines[r];
if let Some((o, c)) = find_end(line) {
offset = o;
col = c;
break;
}
r += 1;
}
let start = Pos::new(start_row, start_col, start_offset);
let end = Pos::new(r, col, offset);
self.delete_range(start, end, true);
true
}
fn delete_piece(&mut self, col: usize, chars: usize) -> bool {
if chars == 0 {
return false;
}
#[inline]
fn bytes_and_chars(claimed: usize, s: &str) -> (usize, usize) {
let mut last_col = 0;
for (col, (bytes, _)) in s.char_indices().enumerate() {
if col == claimed {
return (bytes, claimed);
}
last_col = col;
}
(s.len(), last_col + 1)
}
let (row, _) = self.cursor;
let line = &mut self.lines[row];
if let Some((i, _)) = line.char_indices().nth(col) {
let (bytes, chars) = bytes_and_chars(chars, &line[i..]);
let removed = line.drain(i..i + bytes).as_str().to_string();
self.cursor = (row, col);
self.push_history(
EditKind::DeleteStr(removed.clone()),
Pos::new(row, col + chars, i + bytes),
i,
);
self.yank = removed.into();
true
} else {
false
}
}
pub fn insert_tab(&mut self) -> bool {
let modified = self.delete_selection(false);
if self.tab_len == 0 {
return modified;
}
if self.hard_tab_indent {
self.insert_char('\t');
return true;
}
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_piece(spaces(len).to_string())
}
pub fn insert_newline(&mut self) {
self.delete_selection(false);
let (row, col) = self.cursor;
let line = &mut self.lines[row];
let offset = line
.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len());
let next_line = line[offset..].to_string();
line.truncate(offset);
self.lines.insert(row + 1, next_line);
self.cursor = (row + 1, 0);
self.push_history(EditKind::InsertNewline, Pos::new(row, col, offset), 0);
}
pub fn delete_newline(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
let (row, _) = 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, Pos::new(row, 0, 0), prev_line_end);
true
}
pub fn delete_char(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
let (row, col) = self.cursor;
if col == 0 {
return self.delete_newline();
}
let line = &mut self.lines[row];
if let Some((offset, c)) = line.char_indices().nth(col - 1) {
line.remove(offset);
self.cursor.1 -= 1;
self.push_history(
EditKind::DeleteChar(c),
Pos::new(row, col, offset + c.len_utf8()),
offset,
);
true
} else {
false
}
}
pub fn delete_next_char(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
let before = self.cursor;
self.move_cursor_with_shift(CursorMove::Forward, false);
if before == self.cursor {
return false; }
self.delete_char()
}
pub fn delete_line_by_end(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
if self.delete_piece(self.cursor.1, usize::MAX) {
return true;
}
self.delete_next_char() }
pub fn delete_line_by_head(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
if self.delete_piece(0, self.cursor.1) {
return true;
}
self.delete_newline()
}
pub fn delete_word(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
let (r, c) = self.cursor;
if let Some(col) = find_word_start_backward(&self.lines[r], c) {
self.delete_piece(col, c - col)
} else if c > 0 {
self.delete_piece(0, c)
} else {
self.delete_newline()
}
}
pub fn delete_next_word(&mut self) -> bool {
if self.delete_selection(false) {
return true;
}
let (r, c) = self.cursor;
let line = &self.lines[r];
if let Some(col) = find_word_exclusive_end_forward(line, c) {
self.delete_piece(c, col - c)
} else {
let end_col = line.chars().count();
if c < end_col {
self.delete_piece(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.delete_selection(false);
match self.yank.clone() {
YankText::Piece(s) => self.insert_piece(s),
YankText::Chunk(c) => self.insert_chunk(c),
}
}
pub fn start_selection(&mut self) {
self.selection_start = Some(self.cursor);
}
pub fn cancel_selection(&mut self) {
self.selection_start = None;
}
pub fn select_all(&mut self) {
self.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
self.selection_start = Some((0, 0));
}
pub fn is_selecting(&self) -> bool {
self.selection_start.is_some()
}
fn line_offset(&self, row: usize, col: usize) -> usize {
let line = self
.lines
.get(row)
.unwrap_or(&self.lines[self.lines.len() - 1]);
line.char_indices()
.nth(col)
.map(|(i, _)| i)
.unwrap_or(line.len())
}
pub fn set_selection_style(&mut self, style: Style) {
self.select_style = style;
}
pub fn selection_style(&mut self) -> Style {
self.select_style
}
fn selection_positions(&self) -> Option<(Pos, Pos)> {
let (sr, sc) = self.selection_start?;
let (er, ec) = self.cursor;
let (so, eo) = (self.line_offset(sr, sc), self.line_offset(er, ec));
let s = Pos::new(sr, sc, so);
let e = Pos::new(er, ec, eo);
match (sr, so).cmp(&(er, eo)) {
Ordering::Less => Some((s, e)),
Ordering::Equal => None,
Ordering::Greater => Some((e, s)),
}
}
fn take_selection_positions(&mut self) -> Option<(Pos, Pos)> {
let range = self.selection_positions();
self.cancel_selection();
range
}
pub fn copy(&mut self) {
if let Some((start, end)) = self.take_selection_positions() {
if start.row == end.row {
self.yank = self.lines[start.row][start.offset..end.offset]
.to_string()
.into();
return;
}
let mut chunk = vec![self.lines[start.row][start.offset..].to_string()];
chunk.extend(self.lines[start.row + 1..end.row].iter().cloned());
chunk.push(self.lines[end.row][..end.offset].to_string());
self.yank = YankText::Chunk(chunk);
}
}
pub fn cut(&mut self) -> bool {
self.delete_selection(true)
}
fn delete_selection(&mut self, should_yank: bool) -> bool {
if let Some((s, e)) = self.take_selection_positions() {
self.delete_range(s, e, should_yank);
return true;
}
false
}
pub fn move_cursor(&mut self, m: CursorMove) {
self.move_cursor_with_shift(m, self.selection_start.is_some());
}
fn move_cursor_with_shift(&mut self, m: CursorMove, shift: bool) {
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
if shift {
if self.selection_start.is_none() {
self.start_selection();
}
} else {
self.cancel_selection();
}
self.cursor = cursor;
}
}
pub fn undo(&mut self) -> bool {
if let Some(cursor) = self.history.undo(&mut self.lines) {
self.cancel_selection();
self.cursor = cursor;
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if let Some(cursor) = self.history.redo(&mut self.lines) {
self.cancel_selection();
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,
self.select_style,
);
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);
}
if let Some((start, end)) = self.selection_positions() {
hl.selection(row, start.row, start.offset, end.row, end.offset);
}
hl.into_spans()
}
#[deprecated(
since = "0.5.3",
note = "calling this method is no longer necessary on rendering a textarea. pass &TextArea reference to `Frame::render_widget` method call directly"
)]
pub fn widget(&'a self) -> impl Widget + 'a {
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 selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
self.selection_start.map(|pos| {
if pos > self.cursor {
(self.cursor, pos)
} else {
(pos, 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(&self) -> String {
self.yank.to_string()
}
pub fn set_yank_text(&mut self, text: impl Into<String>) {
let lines: Vec<_> = text
.into()
.split('\n')
.map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
.collect();
self.yank = lines.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>) {
self.scroll_with_shift(scrolling.into(), self.selection_start.is_some());
}
fn scroll_with_shift(&mut self, scrolling: Scrolling, shift: bool) {
if shift && self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
}
scrolling.scroll(&mut self.viewport);
self.move_cursor_with_shift(CursorMove::InViewport, shift);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scroll() {
use crate::ratatui::buffer::Buffer;
use crate::ratatui::layout::Rect;
use crate::ratatui::widgets::Widget as _;
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.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));
}
}