diff --git a/src/history.rs b/src/history.rs index 3fd3ae8..e8da053 100644 --- a/src/history.rs +++ b/src/history.rs @@ -6,12 +6,14 @@ pub enum EditKind { DeleteChar(char, usize), InsertNewline(usize), DeleteNewline(usize), - Insert(String, usize), - Remove(String, usize), + InsertStr(String, usize), + DeleteStr(String, usize), + InsertChunk(Vec, usize, usize), + DeleteChunk(Vec, usize, usize), } impl EditKind { - fn apply(&self, row: usize, lines: &mut Vec) { + pub(crate) fn apply(&self, row: usize, lines: &mut Vec) { match self { EditKind::InsertChar(c, i) => { lines[row].insert(*i, *c); @@ -31,13 +33,38 @@ impl EditKind { lines[row - 1].push_str(&line); } } - EditKind::Insert(s, i) => { + EditKind::InsertStr(s, i) => { lines[row].insert_str(*i, s.as_str()); } - EditKind::Remove(s, i) => { + EditKind::DeleteStr(s, i) => { let end = *i + s.len(); lines[row].replace_range(*i..end, ""); } + EditKind::InsertChunk(c, row, i) => { + debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c); + let row = *row; + + // Handle first line of chunk + let first_line = &mut lines[row]; + let mut last_line = first_line.drain(*i..).as_str().to_string(); + first_line.push_str(&c[0]); + + // Handle last line of chunk + last_line.insert_str(0, c.last().unwrap()); + lines.insert(row + 1, last_line); + + // Handle last line of chunk + lines.splice(row + 1..row + 1, c[1..c.len() - 1].iter().cloned()); + } + EditKind::DeleteChunk(c, row, i) => { + debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c); + let row = *row; + + lines[row].truncate(*i); + let mut last_line = lines.drain(row + 1..row + c.len()).last().unwrap(); + last_line.drain(..c[c.len() - 1].len()); + lines[row].push_str(&last_line); + } } } @@ -48,8 +75,10 @@ impl EditKind { DeleteChar(c, i) => InsertChar(c, i), InsertNewline(i) => DeleteNewline(i), DeleteNewline(i) => InsertNewline(i), - Insert(s, i) => Remove(s, i), - Remove(s, i) => Insert(s, i), + InsertStr(s, i) => DeleteStr(s, i), + DeleteStr(s, i) => InsertStr(s, i), + InsertChunk(c, r, i) => DeleteChunk(c, r, i), + DeleteChunk(c, r, i) => InsertChunk(c, r, i), } } } @@ -148,3 +177,467 @@ impl History { self.max_items } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn insert_delete_chunk() { + #[rustfmt::skip] + let tests = [ + // Positions + ( + // Text before edit + &[ + "ab", + "cd", + "ef", + ][..], + // (row, offset) position before edit + (0, 0), + // Chunk to be inserted + &[ + "x", "y", + ][..], + // Text after edit + &[ + "x", + "yab", + "cd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 1), + &[ + "x", "y", + ][..], + &[ + "ax", + "yb", + "cd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 2), + &[ + "x", "y", + ][..], + &[ + "abx", + "y", + "cd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 0), + &[ + "x", "y", + ][..], + &[ + "ab", + "x", + "ycd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 1), + &[ + "x", "y", + ][..], + &[ + "ab", + "cx", + "yd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 2), + &[ + "x", "y", + ][..], + &[ + "ab", + "cdx", + "y", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 0), + &[ + "x", "y", + ][..], + &[ + "ab", + "cd", + "x", + "yef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 1), + &[ + "x", "y", + ][..], + &[ + "ab", + "cd", + "ex", + "yf", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 2), + &[ + "x", "y", + ][..], + &[ + "ab", + "cd", + "efx", + "y", + ][..], + ), + // More than 2 lines + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 1), + &[ + "x", "y", "z", "w" + ][..], + &[ + "ab", + "cx", + "y", + "z", + "wd", + "ef", + ][..], + ), + // Empty lines + ( + &[ + "", + "", + "", + ][..], + (0, 0), + &[ + "x", "y", "z" + ][..], + &[ + "x", + "y", + "z", + "", + "", + ][..], + ), + ( + &[ + "", + "", + "", + ][..], + (1, 0), + &[ + "x", "y", "z" + ][..], + &[ + "", + "x", + "y", + "z", + "", + ][..], + ), + ( + &[ + "", + "", + "", + ][..], + (2, 0), + &[ + "x", "y", "z" + ][..], + &[ + "", + "", + "x", + "y", + "z", + ][..], + ), + // Empty buffer + ( + &[ + "", + ][..], + (0, 0), + &[ + "x", "y", "z" + ][..], + &[ + "x", + "y", + "z", + ][..], + ), + // Insert empty lines + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + &[ + "", "", "", + ][..], + &[ + "", + "", + "ab", + "cd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 0), + &[ + "", "", "", + ][..], + &[ + "ab", + "", + "", + "cd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 1), + &[ + "", "", "", + ][..], + &[ + "ab", + "c", + "", + "d", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 2), + &[ + "", "", "", + ][..], + &[ + "ab", + "cd", + "", + "", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 2), + &[ + "", "", "", + ][..], + &[ + "ab", + "cd", + "ef", + "", + "", + ][..], + ), + // Multi-byte characters + ( + &[ + "šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + (0, 0), + &[ + "šŸ·", "šŸ¼", "šŸ“", + ][..], + &[ + "šŸ·", + "šŸ¼", + "šŸ“šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + ), + ( + &[ + "šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + (0, 4 * 2), + &[ + "šŸ·", "šŸ¼", "šŸ“", + ][..], + &[ + "šŸ¶šŸ±šŸ·", + "šŸ¼", + "šŸ“", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + ), + ( + &[ + "šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + (1, 0), + &[ + "šŸ·", "šŸ¼", "šŸ“", + ][..], + &[ + "šŸ¶šŸ±", + "šŸ·", + "šŸ¼", + "šŸ“šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + ), + ( + &[ + "šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + (1, 4 * 1), + &[ + "šŸ·", "šŸ¼", "šŸ“", + ][..], + &[ + "šŸ¶šŸ±", + "šŸ®šŸ·", + "šŸ¼", + "šŸ“šŸ°", + "šŸ§šŸ­", + ][..], + ), + ( + &[ + "šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­", + ][..], + (2, 4 * 2), + &[ + "šŸ·", "šŸ¼", "šŸ“", + ][..], + &[ + "šŸ¶šŸ±", + "šŸ®šŸ°", + "šŸ§šŸ­šŸ·", + "šŸ¼", + "šŸ“", + ][..], + ), + ]; + + for (before, pos, input, expected) in tests { + let (row, offset) = pos; + let mut lines: Vec<_> = before.iter().map(|s| s.to_string()).collect(); + let chunk: Vec<_> = input.iter().map(|s| s.to_string()).collect(); + + let edit = EditKind::InsertChunk(chunk.clone(), row, offset); + edit.apply(row, &mut lines); + assert_eq!( + &lines, expected, + "{:?} at {:?} with {:?}", + before, pos, input, + ); + + let edit = EditKind::DeleteChunk(chunk, row, offset); + edit.apply(row, &mut lines); + assert_eq!( + &lines, &before, + "{:?} at {:?} with {:?}", + before, pos, input, + ); + } + } +} diff --git a/src/textarea.rs b/src/textarea.rs index 8bfb794..946e4ca 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -17,6 +17,33 @@ use ratatui::text::Line; use tui::text::Spans as Line; use unicode_width::UnicodeWidthChar as _; +#[derive(Debug, Clone)] +enum YankText { + Piece(String), + Chunk(Vec), +} + +impl Default for YankText { + fn default() -> Self { + Self::Piece(String::new()) + } +} + +impl> From for YankText { + fn from(s: S) -> Self { + Self::Piece(s.into()) + } +} + +impl ToString for YankText { + fn to_string(&self) -> String { + match self { + Self::Piece(s) => s.clone(), + Self::Chunk(ss) => ss.join("\n"), + } + } +} + /// A type to manage state of textarea. /// /// [`TextArea::default`] creates an empty textarea. [`TextArea::new`] creates a textarea with given text lines. @@ -50,7 +77,7 @@ pub struct TextArea<'a> { line_number_style: Option