diff --git a/examples/minimal.rs b/examples/minimal.rs index e6ab805..4b4a102 100644 --- a/examples/minimal.rs +++ b/examples/minimal.rs @@ -18,7 +18,7 @@ fn main() -> io::Result<()> { let mut term = Terminal::new(backend)?; let mut textarea = TextArea::default(); - textarea.block(Block::default().borders(Borders::ALL).title("EXAMPLE")); + textarea.set_block(Block::default().borders(Borders::ALL).title("EXAMPLE")); let layout = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(1)].as_ref()); diff --git a/src/edit.rs b/src/edit.rs new file mode 100644 index 0000000..9bd0563 --- /dev/null +++ b/src/edit.rs @@ -0,0 +1,93 @@ +pub enum EditKind { + InsertChar(char, usize), + DeleteChar(char, usize), + InsertNewline(usize), + DeleteNewline(usize), + Insert(String, usize), +} + +pub struct Edit { + kind: EditKind, + cursor_before: (usize, usize), + cursor_after: (usize, usize), +} + +impl Edit { + pub fn new( + kind: EditKind, + cursor_before: (usize, usize), + cursor_after: (usize, usize), + ) -> Self { + Self { + kind, + cursor_before, + cursor_after, + } + } + + pub fn redo(&self, lines: &mut Vec) { + let (row, _) = self.cursor_before; + match self.kind { + EditKind::InsertChar(c, i) => { + lines[row].insert(i, c); + } + EditKind::DeleteChar(_, i) => { + lines[row].remove(i); + } + EditKind::InsertNewline(i) => { + let line = &mut lines[row]; + let next_line = line[i..].to_string(); + line.truncate(i); + line.push(' '); + lines.insert(row + 1, next_line); + } + EditKind::DeleteNewline(_) => { + if row > 0 { + let line = lines.remove(row); + let prev_line = &mut lines[row - 1]; + prev_line.pop(); // Remove trailing space + prev_line.push_str(&line); + } + } + EditKind::Insert(ref s, i) => { + lines[row].insert_str(i, s.as_str()); + } + } + } + + pub fn undo(&self, lines: &mut Vec) { + let (row, _) = self.cursor_after; + match self.kind { + EditKind::InsertChar(_, i) => { + lines[row].remove(i); + } + EditKind::DeleteChar(c, i) => { + lines[row].insert(i, c); + } + EditKind::InsertNewline(_) => { + let line = lines.remove(row); + let prev_line = &mut lines[row - 1]; + prev_line.pop(); // Remove trailing space + prev_line.push_str(&line); + } + EditKind::DeleteNewline(i) => { + let line = &mut lines[row]; + let next_line = line[i..].to_string(); + line.truncate(i); + line.push(' '); + lines.insert(row + 1, next_line); + } + EditKind::Insert(ref s, i) => { + lines[row].replace_range(i..s.len(), ""); + } + } + } + + pub fn cursor_before(&self) -> (usize, usize) { + self.cursor_before + } + + pub fn cursor_after(&self) -> (usize, usize) { + self.cursor_after + } +} diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..5da0766 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,52 @@ +use crate::edit::Edit; +use std::collections::VecDeque; + +pub struct EditHistory { + index: usize, + max_items: usize, + edits: VecDeque, +} + +impl EditHistory { + pub fn new(max_items: usize) -> Self { + Self { + index: 0, + max_items, + edits: VecDeque::new(), + } + } + + pub fn push(&mut self, edit: Edit) { + if self.edits.len() == self.max_items { + self.edits.pop_front(); + self.index -= 1; + } + + if self.index < self.edits.len() { + self.edits.truncate(self.index); + } + + self.index += 1; + self.edits.push_back(edit); + } + + pub fn redo(&mut self, lines: &mut Vec) -> Option<(usize, usize)> { + if self.index == self.edits.len() { + return None; + } + let edit = &self.edits[self.index]; + edit.redo(lines); + self.index += 1; + Some(edit.cursor_after()) + } + + pub fn undo(&mut self, lines: &mut Vec) -> Option<(usize, usize)> { + if self.index == 0 { + return None; + } + self.index -= 1; + let edit = &self.edits[self.index]; + edit.undo(lines); + Some(edit.cursor_before()) + } +} diff --git a/src/lib.rs b/src/lib.rs index fa23561..6990d0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,5 @@ +mod edit; +mod history; mod input; mod textarea; diff --git a/src/textarea.rs b/src/textarea.rs index 7ea7db9..adedb44 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -1,3 +1,5 @@ +use crate::edit::{Edit, EditKind}; +use crate::history::EditHistory; use crate::input::{Input, Key}; use tui::style::{Modifier, Style}; use tui::text::{Span, Spans, Text}; @@ -9,6 +11,7 @@ pub struct TextArea<'a> { style: Style, cursor: (usize, usize), // 0-base tab: &'a str, + history: EditHistory, } impl<'a> Default for TextArea<'a> { @@ -19,6 +22,7 @@ impl<'a> Default for TextArea<'a> { style: Style::default(), cursor: (0, 0), tab: " ", + history: EditHistory::new(50), } } } @@ -36,6 +40,8 @@ impl<'a> TextArea<'a> { Key::Char('b') => self.cursor_back(), Key::Char('a') => self.cursor_start(), Key::Char('e') => self.cursor_end(), + Key::Char('u') => self.undo(), + Key::Char('r') => self.redo(), _ => {} } } else { @@ -83,12 +89,18 @@ impl<'a> TextArea<'a> { ); } + 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]; if let Some((i, _)) = line.char_indices().nth(col) { line.insert(i, c); self.cursor.1 += 1; + self.push_history(EditKind::InsertChar(c, i), (row, col)); } } @@ -103,6 +115,7 @@ impl<'a> TextArea<'a> { if let Some((i, _)) = line.char_indices().nth(col) { line.insert_str(i, s); self.cursor.1 += s.chars().count(); + self.push_history(EditKind::Insert(s.to_string(), i), (row, col)); } } @@ -126,6 +139,7 @@ impl<'a> TextArea<'a> { line.push(' '); self.lines.insert(row + 1, next_line); self.cursor = (row + 1, 0); + self.push_history(EditKind::InsertNewline(idx), (row, col)); } pub fn delete_char(&mut self) { @@ -135,16 +149,19 @@ impl<'a> TextArea<'a> { let line = self.lines.remove(row); let prev_line = &mut self.lines[row - 1]; prev_line.pop(); // Remove trailing space + let prev_line_end = prev_line.len(); prev_line.push_str(&line); self.cursor = (row - 1, prev_line.chars().count() - 1); + self.push_history(EditKind::DeleteNewline(prev_line_end), (row, col)); } return; } let line = &mut self.lines[row]; - if let Some((i, _)) = line.char_indices().nth(col - 1) { + 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)); } } @@ -202,6 +219,18 @@ impl<'a> TextArea<'a> { self.cursor.1 = self.lines[self.cursor.0].chars().count() - 1; } + pub fn undo(&mut self) { + if let Some(cursor) = self.history.undo(&mut self.lines) { + self.cursor = cursor; + } + } + + pub fn redo(&mut self) { + if let Some(cursor) = self.history.redo(&mut self.lines) { + self.cursor = cursor; + } + } + pub fn widget(&'a self) -> impl Widget + 'a { let mut lines = Vec::with_capacity(self.lines.len()); for (i, l) in self.lines.iter().enumerate() { @@ -227,29 +256,29 @@ impl<'a> TextArea<'a> { p } - pub fn style(&mut self, style: Style) -> &mut Self { + pub fn set_style(&mut self, style: Style) { self.style = style; - self } - pub fn block(&mut self, block: Block<'a>) -> &mut Self { + pub fn set_block(&mut self, block: Block<'a>) { self.block = Some(block); - self } - pub fn remove_block(&mut self) -> &mut Self { + pub fn remove_block(&mut self) { self.block = None; - self } - pub fn tab(&mut self, tab: &'a str) -> &mut Self { + pub fn set_tab(&mut self, tab: &'a str) { assert!( tab.chars().all(|c| c == ' '), "tab string must consist of spaces but got {:?}", tab, ); self.tab = tab; - self + } + + pub fn set_max_histories(&mut self, max: usize) { + self.history = EditHistory::new(max); } pub fn lines(&'a self) -> impl Iterator {