diff --git a/CHANGELOG.md b/CHANGELOG.md index 15e8a95..4ef32f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.12.2] - 2026-08-17 [Changes][v0.12.2] + +### Features + +- **Common word-deletion hotkeys**: Bind `Ctrl+Backspace` to [`TextArea::delete_word`] and `Ctrl+Delete` to [`TextArea::delete_next_word`] in the default key map, matching the de-facto terminal/editor convention (readline, VS Code, opencode). + ## [0.12.1] - 2026-07-10 [Changes][v0.12.1] ### Features diff --git a/Cargo.toml b/Cargo.toml index f60208f..6cc9f0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "tui-textarea-2" homepage = "https://github.com/srothgan/tui-textarea#readme" repository = "https://github.com/srothgan/tui-textarea" documentation = "https://docs.rs/tui-textarea-2/latest/tui_textarea/" -version = "0.12.1" +version = "0.12.2" edition = "2024" rust-version = "1.88.0" # for Ratatui 0.30 split crates authors = ["Simon Peter Rothgang ", "rhysd "] diff --git a/src/textarea.rs b/src/textarea.rs index 4e7b3cc..c9ba1fa 100644 --- a/src/textarea.rs +++ b/src/textarea.rs @@ -480,6 +480,12 @@ impl<'a> TextArea<'a> { ctrl: false, alt: true, .. + } + | Input { + key: Key::Backspace, + ctrl: true, + alt: false, + .. } => self.delete_word(), Input { key: Key::Delete, @@ -492,6 +498,12 @@ impl<'a> TextArea<'a> { ctrl: false, alt: true, .. + } + | Input { + key: Key::Delete, + ctrl: true, + alt: false, + .. } => self.delete_next_word(), Input { key: Key::Char('n'), @@ -3294,4 +3306,95 @@ mod tests { assert!(panic.is_err()); } + + fn key_input(key: Key, ctrl: bool, alt: bool) -> Input { + Input { + key, + ctrl, + alt, + shift: false, + } + } + + #[test] + fn ctrl_backspace_deletes_word_before_cursor() { + let mut textarea = TextArea::from(["aaa bbb ccc"]); + textarea.move_cursor(CursorMove::End); + + assert!(textarea.input(key_input(Key::Backspace, true, false))); + + assert_eq!(textarea.lines(), ["aaa bbb "]); + assert_eq!(textarea.cursor(), (0, 8)); + } + + #[test] + fn ctrl_backspace_at_whitespace_skips_run_and_deletes_previous_word() { + let mut textarea = TextArea::from(["aaa bbb"]); + textarea.move_cursor(CursorMove::End); + + textarea.input(key_input(Key::Backspace, true, false)); + + assert_eq!(textarea.lines(), ["aaa "]); + assert_eq!(textarea.cursor(), (0, 6)); + } + + #[test] + fn ctrl_delete_deletes_word_after_cursor() { + let mut textarea = TextArea::from(["aaa bbb ccc"]); + + assert!(textarea.input(key_input(Key::Delete, true, false))); + + assert_eq!(textarea.lines(), [" bbb ccc"]); + assert_eq!(textarea.cursor(), (0, 0)); + } + + #[test] + fn ctrl_delete_at_end_of_word_deletes_next_word() { + let mut textarea = TextArea::from(["aaa bbb ccc"]); + textarea.move_cursor(CursorMove::Jump(0, 4)); + + assert!(textarea.input(key_input(Key::Delete, true, false))); + + assert_eq!(textarea.lines(), ["aaa ccc"]); + assert_eq!(textarea.cursor(), (0, 4)); + } + + #[test] + fn plain_backspace_still_deletes_single_char() { + let mut textarea = TextArea::from(["aaa bbb"]); + textarea.move_cursor(CursorMove::End); + + assert!(textarea.input(key_input(Key::Backspace, false, false))); + + assert_eq!(textarea.lines(), ["aaa bb"]); + } + + #[test] + fn plain_delete_still_deletes_single_char() { + let mut textarea = TextArea::from(["aaa bbb"]); + + assert!(textarea.input(key_input(Key::Delete, false, false))); + + assert_eq!(textarea.lines(), ["aa bbb"]); + } + + #[test] + fn alt_backspace_still_deletes_word() { + let mut textarea = TextArea::from(["aaa bbb ccc"]); + textarea.move_cursor(CursorMove::End); + + assert!(textarea.input(key_input(Key::Backspace, false, true))); + + assert_eq!(textarea.lines(), ["aaa bbb "]); + } + + #[test] + fn ctrl_w_still_deletes_word() { + let mut textarea = TextArea::from(["aaa bbb ccc"]); + textarea.move_cursor(CursorMove::End); + + assert!(textarea.input(key_input(Key::Char('w'), true, false))); + + assert_eq!(textarea.lines(), ["aaa bbb "]); + } }