diff --git a/src/textarea.rs b/src/textarea.rs index e7a078c..665e845 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -681,17 +681,93 @@ impl<'a> TextArea<'a> { true } - /// Delete a string in current cursor line. The `chars` parameter means number of characters, not a byte length of - /// the string. This method returns if some text was deleted or not in the textarea. + /// Delete a string from the current cursor position. The `chars` parameter means number of characters, not a byte + /// length of the string. Newlines at the end of lines are counted in the number. This method returns if some text + /// was deleted or not. /// ``` - /// use tui_textarea::TextArea; + /// use tui_textarea::{TextArea, CursorMove}; /// /// let mut textarea = TextArea::from(["🐱🐶🐰🐮"]); + /// textarea.move_cursor(CursorMove::Forward); /// - /// textarea.delete_str(1, 2); + /// textarea.delete_str(2); /// assert_eq!(textarea.lines(), ["🐱🐮"]); + /// + /// let mut textarea = TextArea::from(["🐱", "🐶", "🐰", "🐮"]); + /// textarea.move_cursor(CursorMove::Down); + /// + /// textarea.delete_str(4); // Deletes 🐶, \n, 🐰, \n + /// assert_eq!(textarea.lines(), ["🐱", "🐮"]); /// ``` - pub fn delete_str(&mut self, col: usize, chars: usize) -> bool { + pub fn delete_str(&mut self, chars: usize) -> bool { + if chars == 0 { + return false; + } + + let (row, col) = self.cursor; + + let mut remaining = chars; + let mut find_end = move |line: &str| { + for (i, _) in line.char_indices() { + if remaining == 0 { + return Some(i); + } + remaining -= 1; + } + if remaining == 0 { + Some(line.len()) + } else { + remaining -= 1; + None + } + }; + + let line = &self.lines[row]; + let first_start = { + line.char_indices() + .nth(col) + .map(|(i, _)| i) + .unwrap_or(line.len()) + }; + + // First line + if let Some(offset) = find_end(&line[first_start..]) { + let removed = self.lines[row] + .drain(first_start..first_start + offset) + .as_str() + .to_string(); + self.yank = removed.clone(); + self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor); + return true; + } + + let mut r = row + 1; + let mut i = 0; + + while r < self.lines.len() { + let line = &self.lines[r]; + if let Some(offset) = find_end(line) { + i = offset; + break; + } + r += 1; + } + + let mut deleted = vec![self.lines[row].drain(first_start..).as_str().to_string()]; + deleted.extend(self.lines.drain(row + 1..r)); + if row + 1 < self.lines.len() { + let mut last_line = self.lines.remove(row + 1); + self.lines[row].push_str(&last_line[i..]); + last_line.truncate(i); + deleted.push(last_line); + } + + self.yank = deleted.join("\n"); // TODO + self.push_history(EditKind::DeleteChunk(deleted, first_start), self.cursor); + true + } + + fn delete_piece(&mut self, col: usize, chars: usize) -> bool { if chars == 0 { return false; } @@ -866,7 +942,7 @@ impl<'a> TextArea<'a> { /// assert_eq!(textarea.lines(), ["ab"]); /// ``` pub fn delete_line_by_end(&mut self) -> bool { - if self.delete_str(self.cursor.1, usize::MAX) { + if self.delete_piece(self.cursor.1, usize::MAX) { return true; } self.delete_next_char() // At the end of the line. Try to delete next line @@ -887,7 +963,7 @@ impl<'a> TextArea<'a> { /// assert_eq!(textarea.lines(), ["cde"]); /// ``` pub fn delete_line_by_head(&mut self) -> bool { - if self.delete_str(0, self.cursor.1) { + if self.delete_piece(0, self.cursor.1) { return true; } self.delete_newline() @@ -914,9 +990,9 @@ impl<'a> TextArea<'a> { 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) + self.delete_piece(col, c - col) } else if c > 0 { - self.delete_str(0, c) + self.delete_piece(0, c) } else { self.delete_newline() } @@ -942,11 +1018,11 @@ impl<'a> TextArea<'a> { 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) + self.delete_piece(c, col - c) } else { let end_col = line.chars().count(); if c < end_col { - self.delete_str(c, end_col - c) + self.delete_piece(c, end_col - c) } else if r + 1 < self.lines.len() { self.cursor = (r + 1, 0); self.delete_newline() diff --git a/tests/textarea.rs b/tests/textarea.rs index 99b3fe0..f713663 100644 --- a/tests/textarea.rs +++ b/tests/textarea.rs @@ -37,6 +37,10 @@ fn test_insert_str_one_line() { want.insert(i, 'x'); assert_eq!(&want, have, "{}", i); } + + let mut t = TextArea::default(); + assert!(t.insert_str("x")); + assert_eq!(t.lines(), ["x"]); } #[test] @@ -385,3 +389,317 @@ fn test_insert_str_multiple_lines() { assert_eq!(t.lines(), expected, "{:?}", test); } } + +#[test] +fn test_delete_str_delete_nothing() { + for i in 0..="ab".len() { + let mut t = TextArea::from(["ab"]); + assert!(!t.delete_str(0), "{}", i); + } + let mut t = TextArea::default(); + assert!(!t.delete_str(0)); +} + +#[test] +fn test_delete_str_delete_within_line() { + for i in 0.."abc".len() { + for j in 1..="abc".len() - i { + let mut t = TextArea::from(["abc"]); + t.move_cursor(CursorMove::Jump(0, i as _)); + assert!(t.delete_str(j), "at {}, size={}", i, j); + let have = &t.lines()[0]; + + let mut want = "abc".to_string(); + want.drain(i..i + j); + assert_eq!(&want, have, "at {}, size={}", i, j); + } + } +} + +#[test] +fn test_delete_str_delete_multiple_lines() { + #[rustfmt::skip] + let tests = [ + // Length + ( + // Text before edit + &[ + "ab", + "cd", + "ef", + ][..], + // (row, offset) cursor position + (0, 0), + // Chars to be deleted + 3, + // Deleted text + "ab\n", + // Text after edit + &[ + "cd", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + 4, + "ab\nc", + &[ + "d", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + 5, + "ab\ncd", + &[ + "", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + 6, + "ab\ncd\n", + &[ + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + 7, + "ab\ncd\ne", + &[ + "f", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + 8, + "ab\ncd\nef", + &[ + "", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 0), + 9, + "ab\ncd\nef", + &[ + "", + ][..], + ), + // Positions + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 1), + 3, + "b\nc", + &[ + "ad", + "ef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (0, 2), + 4, + "\ncd\n", + &[ + "abef", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (1, 0), + 4, + "cd\ne", + &[ + "ab", + "f", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 0), + 3, + "ef", + &[ + "ab", + "cd", + "", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 1), + 2, + "f", + &[ + "ab", + "cd", + "e", + ][..], + ), + ( + &[ + "ab", + "cd", + "ef", + ][..], + (2, 2), + 1, + "", + &[ + "ab", + "cd", + "ef", + ][..], + ), + // Empty lines + ( + &[ + "", + "", + "", + ][..], + (0, 0), + 1, + "\n", + &[ + "", + "", + ][..], + ), + ( + &[ + "", + "", + "", + ][..], + (0, 0), + 2, + "\n\n", + &[ + "", + ][..], + ), + ( + &[ + "", + "", + "", + ][..], + (0, 0), + 3, + "\n\n", + &[ + "", + ][..], + ), + ( + &[ + "", + "", + "", + ][..], + (1, 0), + 1, + "\n", + &[ + "", + "", + ][..], + ), + ( + &[ + "", + "", + "", + ][..], + (2, 0), + 1, + "", + &[ + "", + "", + "", + ][..], + ), + // Empty buffer + ( + &[ + "", + ][..], + (0, 0), + 1, + "", + &[ + "", + ][..], + ), + ]; + + for test in tests { + let (before, (row, col), chars, deleted, after) = test; + + let mut t = TextArea::from(before.iter().map(|s| s.to_string())); + t.move_cursor(CursorMove::Jump(row as _, col as _)); + + assert!(t.delete_str(chars), "did not modified: {:?}", test); + assert_eq!(t.cursor(), (row, col), "cursor position: {:?}", test); + assert_eq!(t.lines(), after, "text buffer content: {:?}", test); + assert_eq!(t.yank_text(), deleted, "yanked text: {:?}", test); + } +}