diff --git a/README.md b/README.md index 7413bf2..d3e3c57 100644 --- a/README.md +++ b/README.md @@ -497,6 +497,7 @@ notify how to move the cursor. | `textarea.move_cursor(CursorMove::Up)` | Move cursor up by one line | | `textarea.move_cursor(CursorMove::Down)` | Move cursor down by one line | | `textarea.move_cursor(CursorMove::WordForward)` | Move cursor forward by word | +| `textarea.move_cursor(CursorMove::WordEnd)` | Move cursor to next end of word | | `textarea.move_cursor(CursorMove::WordBack)` | Move cursor backward by word | | `textarea.move_cursor(CursorMove::ParagraphForward)` | Move cursor up by paragraph | | `textarea.move_cursor(CursorMove::ParagraphBack)` | Move cursor down by paragraph | @@ -662,7 +663,7 @@ Values of the following types can be serialized/deserialized: Here is an example for deserializing key input from JSON using [serde_json][]. -```rust +```rust,ignore use tui_textarea::Input; let json = r#" diff --git a/examples/vim.rs b/examples/vim.rs index d182fc4..3977f80 100644 --- a/examples/vim.rs +++ b/examples/vim.rs @@ -112,6 +112,11 @@ impl Vim { key: Key::Char('w'), .. } => textarea.move_cursor(CursorMove::WordForward), + Input { + key: Key::Char('e'), + ctrl: false, + .. + } => textarea.move_cursor(CursorMove::WordEnd), Input { key: Key::Char('b'), ctrl: false, diff --git a/src/cursor.rs b/src/cursor.rs index e9b084b..2ed576f 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,5 +1,5 @@ use crate::widget::Viewport; -use crate::word::{find_word_start_backward, find_word_start_forward}; +use crate::word::{find_word_end_next, find_word_start_backward, find_word_start_forward}; #[cfg(feature = "arbitrary")] use arbitrary::Arbitrary; #[cfg(feature = "serde")] @@ -121,6 +121,29 @@ pub enum CursorMove { /// assert_eq!(textarea.cursor(), (0, 8)); /// ``` WordForward, + /// Move cursor forward to the next end of word. Word boundary appears at spaces, punctuations, and others. For example + /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`. When the cursor is at the end of line, it moves to the + /// end of the first word of the next line. + /// ``` + /// use tui_textarea::{TextArea, CursorMove}; + /// + /// let mut textarea = TextArea::from(["aaa bbb (c)", " dd"]); + /// + /// + /// textarea.move_cursor(CursorMove::WordEnd); + /// assert_eq!(textarea.cursor(), (0, 2)); + /// textarea.move_cursor(CursorMove::WordEnd); + /// assert_eq!(textarea.cursor(), (0, 6)); + /// textarea.move_cursor(CursorMove::WordEnd); + /// assert_eq!(textarea.cursor(), (0, 8)); + /// textarea.move_cursor(CursorMove::WordEnd); + /// assert_eq!(textarea.cursor(), (0, 9)); + /// textarea.move_cursor(CursorMove::WordEnd); + /// assert_eq!(textarea.cursor(), (0, 10)); + /// textarea.move_cursor(CursorMove::WordEnd); + /// assert_eq!(textarea.cursor(), (1, 2)); + /// ``` + WordEnd, /// Move cursor backward by one word. Word boundary appears at spaces, punctuations, and others. For example /// `fn foo(a)` consists of words `fn`, `foo`, `(`, `a`, `)`.When the cursor is at the head of line, it moves to /// the end of previous line. @@ -263,6 +286,20 @@ impl CursorMove { let row = lines.len() - 1; Some((row, fit_col(col, &lines[row]))) } + WordEnd => { + if let Some(col) = find_word_end_next(&lines[row], col) { + Some((row, col)) + } else if let Some(col) = (row + 1 < lines.len()) + .then(|| find_word_end_next(&lines[row + 1], 0)) + .unwrap_or_default() + { + Some((row + 1, col)) + } else if row + 1 < lines.len() { + Some((row + 1, lines[row + 1].chars().count().saturating_sub(1))) + } else { + Some((row, lines[row].chars().count().saturating_sub(1))) + } + } WordForward => { if let Some(col) = find_word_start_forward(&lines[row], col) { Some((row, col)) diff --git a/src/word.rs b/src/word.rs index 8edb814..dcad8be 100644 --- a/src/word.rs +++ b/src/word.rs @@ -43,6 +43,26 @@ pub fn find_word_end_forward(line: &str, start_col: usize) -> Option { None } +pub fn find_word_end_next(line: &str, start_col: usize) -> Option { + let mut it = line.chars().enumerate().skip(start_col); + let (mut cur_col, cur_char) = it.next()?; + let mut cur = CharKind::new(cur_char); + for (next_col, c) in it { + let next = CharKind::new(c); + // if cursor started at the end of a word, don't stop + if next_col.saturating_sub(start_col) > 1 && cur != CharKind::Space && next != cur { + return Some(next_col.saturating_sub(1)); + } + cur = next; + cur_col = next_col; + } + // if end of line is whitespace, don't stop the cursor + if cur != CharKind::Space && cur_col.saturating_sub(start_col) >= 1 { + return Some(cur_col); + } + None +} + pub fn find_word_start_backward(line: &str, start_col: usize) -> Option { let idx = line .char_indices() diff --git a/tests/cursor.rs b/tests/cursor.rs index b35ca3c..22b09e2 100644 --- a/tests/cursor.rs +++ b/tests/cursor.rs @@ -17,6 +17,7 @@ fn empty_textarea() { Top, Bottom, WordForward, + WordEnd, WordBack, ParagraphForward, ParagraphBack,