From 2a447d45c2d47d24c9999deefc380485b4210637 Mon Sep 17 00:00:00 2001 From: rhysd Date: Wed, 1 Nov 2023 20:15:42 +0900 Subject: [PATCH] add `EditKind::InsertChunks` and `EditKind::DeleteChunk` edit kinds --- src/history.rs | 414 +++++++++++++++++++++++++++++++++++++++++++++++- src/textarea.rs | 54 +++++-- 2 files changed, 451 insertions(+), 17 deletions(-) diff --git a/src/history.rs b/src/history.rs index 3fd3ae8..a58ed03 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), + DeleteChunk(Vec, 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,36 @@ 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, i) => { + debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c); + + // 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, i) => { + debug_assert!(c.len() > 1, "Chunk size must be > 1: {:?}", c); + + 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 +73,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, i) => DeleteChunk(c, i), + DeleteChunk(c, i) => InsertChunk(c, i), } } } @@ -148,3 +175,376 @@ 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", + "", + "", + ][..], + ), + ]; + + 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(), offset); + edit.apply(row, &mut lines); + assert_eq!( + &lines, expected, + "{:?} at {:?} with {:?}", + before, pos, input, + ); + + let edit = EditKind::DeleteChunk(chunk, 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 2d4c620..eb6911c 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -13,6 +13,7 @@ use crate::widget::{Renderer, Viewport}; use crate::word::{find_word_end_forward, find_word_start_backward}; #[cfg(feature = "ratatui")] use ratatui::text::Line; +use std::mem; #[cfg(feature = "tuirs")] use tui::text::Spans as Line; use unicode_width::UnicodeWidthChar as _; @@ -612,8 +613,7 @@ impl<'a> TextArea<'a> { self.push_history(EditKind::InsertChar(c, i), (row, col)); } - /// Insert a string at current cursor position. Currently the string must not contain any newlines. This method - /// returns if some text was inserted or not in the textarea. + /// Insert a string at current cursor position. This method returns if some text was inserted or not in the textarea. /// ``` /// use tui_textarea::TextArea; /// @@ -621,9 +621,43 @@ impl<'a> TextArea<'a> { /// /// textarea.insert_str("hello"); /// assert_eq!(textarea.lines(), ["hello"]); + /// + /// textarea.insert_str(", world\ngoodbye, world"); + /// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]); /// ``` - pub fn insert_str>(&mut self, s: S) -> bool { - let s = s.into(); + pub fn insert_str(&mut self, s: &str) -> bool { + let mut lines: Vec<_> = s.lines().map(ToString::to_string).collect(); + match lines.len() { + 0 => false, + 1 => self.insert_piece(mem::take(&mut lines[0])), + _ => self.insert_chunk(lines), + } + } + + fn insert_chunk(&mut self, chunk: Vec) -> 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()); + + self.cursor = ( + row + chunk.len() - 1, + col + chunk[chunk.len() - 1].chars().count(), + ); + + let edit = EditKind::InsertChunk(chunk, i); + edit.apply(row, &mut self.lines); + + self.push_history(edit, (row, col)); + true + } + + fn insert_piece(&mut self, s: String) -> bool { if s.is_empty() { return false; } @@ -631,7 +665,7 @@ impl<'a> TextArea<'a> { let (row, col) = self.cursor; let line = &mut self.lines[row]; debug_assert!( - !line.contains('\n'), + !s.contains('\n'), "string given to insert_str must not contain newline: {:?}", line, ); @@ -644,7 +678,7 @@ impl<'a> TextArea<'a> { line.insert_str(i, &s); self.cursor.1 += s.chars().count(); - self.push_history(EditKind::Insert(s, i), (row, col)); + self.push_history(EditKind::InsertStr(s, i), (row, col)); true } @@ -676,7 +710,7 @@ impl<'a> TextArea<'a> { line.replace_range(i..i + bytes, ""); self.cursor = (row, col); - self.push_history(EditKind::Remove(removed.clone(), i), cursor_before); + self.push_history(EditKind::DeleteStr(removed.clone(), i), cursor_before); self.yank = removed; true } else { @@ -704,7 +738,7 @@ impl<'a> TextArea<'a> { } if self.hard_tab_indent { - return self.insert_str("\t"); + return self.insert_piece("\t".to_string()); } let (row, col) = self.cursor; @@ -714,7 +748,7 @@ impl<'a> TextArea<'a> { .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)) + self.insert_piece(spaces(len).to_string()) } /// Insert a newline at current cursor position. @@ -937,7 +971,7 @@ impl<'a> TextArea<'a> { /// assert_eq!(textarea.lines(), [" bbb cccaaa"]); /// ``` pub fn paste(&mut self) -> bool { - self.insert_str(self.yank.to_string()) + self.insert_piece(self.yank.to_string()) } /// Move the cursor to the position specified by the [`CursorMove`] parameter. For each kind of cursor moves, see