diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 0d7673ba..b60ca085 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -77,6 +77,9 @@ impl Editor { } EditCommand::MoveWordRightEnd { select } => self.move_word_right_end(*select), EditCommand::MoveBigWordRightEnd { select } => self.move_big_word_right_end(*select), + EditCommand::MoveViWordLeft { select } => self.move_vi_word_left(*select), + EditCommand::MoveViWordRightStart { select } => self.move_vi_word_right_start(*select), + EditCommand::MoveViWordRightEnd { select } => self.move_vi_word_right_end(*select), EditCommand::InsertChar(c) => self.insert_char(*c), EditCommand::Complete => {} EditCommand::InsertString(str) => self.insert_str(str), @@ -107,8 +110,11 @@ impl Editor { EditCommand::KillLine => self.kill_line(), EditCommand::CutWordLeft => self.cut_word_left(), EditCommand::CutBigWordLeft => self.cut_big_word_left(), + EditCommand::CutViWordLeft => self.cut_vi_word_left(), EditCommand::CutWordRight => self.cut_word_right(), EditCommand::CutBigWordRight => self.cut_big_word_right(), + EditCommand::CutViWordRightEnd => self.cut_vi_word_right_end(), + EditCommand::CutViBigWordRightEnd => self.cut_vi_big_word_right_end(), EditCommand::CutWordRightToNext => self.cut_word_right_to_next(), EditCommand::CutBigWordRightToNext => self.cut_big_word_right_to_next(), EditCommand::PasteCutBufferBefore => self.insert_cut_buffer_before(), @@ -150,8 +156,11 @@ impl Editor { EditCommand::CopyToLineEnd => self.copy_to_line_end(), EditCommand::CopyWordLeft => self.copy_word_left(), EditCommand::CopyBigWordLeft => self.copy_big_word_left(), + EditCommand::CopyViWordLeft => self.copy_vi_word_left(), EditCommand::CopyWordRight => self.copy_word_right(), EditCommand::CopyBigWordRight => self.copy_big_word_right(), + EditCommand::CopyViWordRightEnd => self.copy_vi_word_right_end(), + EditCommand::CopyViBigWordRightEnd => self.copy_vi_big_word_right_end(), EditCommand::CopyWordRightToNext => self.copy_word_right_to_next(), EditCommand::CopyBigWordRightToNext => self.copy_big_word_right_to_next(), EditCommand::CopyRightUntil(c) => self.copy_right_until_char(*c, false, true), @@ -472,7 +481,7 @@ impl Editor { fn cut_word_left(&mut self) { let insertion_offset = self.line_buffer.insertion_point(); - let word_start = self.line_buffer.word_left_index(); + let word_start = self.line_buffer.emacs_word_left_index(); self.cut_range(word_start..insertion_offset); } @@ -482,9 +491,15 @@ impl Editor { self.cut_range(big_word_start..insertion_offset); } + fn cut_vi_word_left(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let word_start = self.line_buffer.vi_word_left_index(); + self.cut_range(word_start..insertion_offset); + } + fn cut_word_right(&mut self) { let insertion_offset = self.line_buffer.insertion_point(); - let word_end = self.line_buffer.word_right_index(); + let word_end = self.line_buffer.emacs_word_right_index(); self.cut_range(insertion_offset..word_end); } @@ -494,9 +509,27 @@ impl Editor { self.cut_range(insertion_offset..big_word_end); } + /// Cut from cursor to end of next word (inclusive). + /// Used by Vi `de` — the `e` motion is inclusive. + fn cut_vi_word_right_end(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let word_end = self.line_buffer.vi_word_right_end_index(); + let inclusive_end = self.line_buffer.grapheme_right_index_from_pos(word_end); + self.cut_range(insertion_offset..inclusive_end); + } + + /// Cut from cursor to end of next WORD (inclusive). + /// Used by Vi `dE` — the `E` motion is inclusive. + fn cut_vi_big_word_right_end(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let word_end = self.line_buffer.big_word_right_end_index(); + let inclusive_end = self.line_buffer.grapheme_right_index_from_pos(word_end); + self.cut_range(insertion_offset..inclusive_end); + } + fn cut_word_right_to_next(&mut self) { let insertion_offset = self.line_buffer.insertion_point(); - let next_word_start = self.line_buffer.word_right_start_index(); + let next_word_start = self.line_buffer.vi_word_right_start_index(); self.cut_range(insertion_offset..next_word_start); } @@ -736,7 +769,7 @@ impl Editor { } fn move_word_left(&mut self, select: bool) { - self.move_to_position(self.line_buffer.word_left_index(), select); + self.move_to_position(self.line_buffer.emacs_word_left_index(), select); } fn move_big_word_left(&mut self, select: bool) { @@ -744,11 +777,11 @@ impl Editor { } fn move_word_right(&mut self, select: bool) { - self.move_to_position(self.line_buffer.word_right_index(), select); + self.move_to_position(self.line_buffer.emacs_word_right_index(), select); } fn move_word_right_start(&mut self, select: bool) { - self.move_to_position(self.line_buffer.word_right_start_index(), select); + self.move_to_position(self.line_buffer.emacs_word_right_start_index(), select); } fn move_big_word_right_start(&mut self, select: bool) { @@ -756,13 +789,25 @@ impl Editor { } fn move_word_right_end(&mut self, select: bool) { - self.move_to_position(self.line_buffer.word_right_end_index(), select); + self.move_to_position(self.line_buffer.emacs_word_right_end_index(), select); } fn move_big_word_right_end(&mut self, select: bool) { self.move_to_position(self.line_buffer.big_word_right_end_index(), select); } + fn move_vi_word_left(&mut self, select: bool) { + self.move_to_position(self.line_buffer.vi_word_left_index(), select); + } + + fn move_vi_word_right_start(&mut self, select: bool) { + self.move_to_position(self.line_buffer.vi_word_right_start_index(), select); + } + + fn move_vi_word_right_end(&mut self, select: bool) { + self.move_to_position(self.line_buffer.vi_word_right_end_index(), select); + } + fn insert_char(&mut self, c: char) { self.delete_selection(); self.line_buffer.insert_char(c); @@ -845,7 +890,7 @@ impl Editor { self.line_buffer .current_whitespace_range() .unwrap_or_else(|| { - let word_range = self.line_buffer.current_word_range(); + let word_range = self.line_buffer.emacs_current_word_range(); match text_object_scope { TextObjectScope::Inner => word_range, TextObjectScope::Around => { @@ -1028,7 +1073,7 @@ impl Editor { pub(crate) fn copy_word_left(&mut self) { let insertion_offset = self.line_buffer.insertion_point(); - let word_start = self.line_buffer.word_left_index(); + let word_start = self.line_buffer.emacs_word_left_index(); self.copy_range(word_start..insertion_offset); } @@ -1038,9 +1083,15 @@ impl Editor { self.copy_range(big_word_start..insertion_offset); } + pub(crate) fn copy_vi_word_left(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let word_start = self.line_buffer.vi_word_left_index(); + self.copy_range(word_start..insertion_offset); + } + pub(crate) fn copy_word_right(&mut self) { let insertion_offset = self.line_buffer.insertion_point(); - let word_end = self.line_buffer.word_right_index(); + let word_end = self.line_buffer.emacs_word_right_index(); self.copy_range(insertion_offset..word_end); } @@ -1050,9 +1101,27 @@ impl Editor { self.copy_range(insertion_offset..big_word_end); } + /// Copy from cursor to end of next word (inclusive). + /// Used by Vi `ye` — the `e` motion is inclusive. + pub(crate) fn copy_vi_word_right_end(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let word_end = self.line_buffer.vi_word_right_end_index(); + let inclusive_end = self.line_buffer.grapheme_right_index_from_pos(word_end); + self.copy_range(insertion_offset..inclusive_end); + } + + /// Copy from cursor to end of next WORD (inclusive). + /// Used by Vi `yE` — the `E` motion is inclusive. + pub(crate) fn copy_vi_big_word_right_end(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let word_end = self.line_buffer.big_word_right_end_index(); + let inclusive_end = self.line_buffer.grapheme_right_index_from_pos(word_end); + self.copy_range(insertion_offset..inclusive_end); + } + pub(crate) fn copy_word_right_to_next(&mut self) { let insertion_offset = self.line_buffer.insertion_point(); - let next_word_start = self.line_buffer.word_right_start_index(); + let next_word_start = self.line_buffer.vi_word_right_start_index(); self.copy_range(insertion_offset..next_word_start); } @@ -1192,6 +1261,137 @@ mod test { assert_eq!(editor.get_buffer(), expected); } + #[rstest] + // de from start of "abc def": cuts "abc" (inclusive end of word) + #[case("abc def", 0, "abc", " def")] + // de on 'b' in "a.b.c": cuts "b." (word 'b' end is 'b', next word '.' + // end is '.', so inclusive range covers "b.") + #[case("a.b.c", 2, "b.", "a.c")] + // de on 'a' in "abc.def": cuts "abc" (end of word 'abc') + #[case("abc.def", 0, "abc", ".def")] + fn test_cut_vi_word_right_end( + #[case] input: &str, + #[case] position: usize, + #[case] expected_cut: &str, + #[case] expected_buffer: &str, + ) { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.cut_vi_word_right_end(); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + + #[rstest] + // dE from start of "abc-def ghi": cuts "abc-def" (inclusive end of WORD) + #[case("abc-def ghi", 0, "abc-def", " ghi")] + // dE from start of "abc def": cuts "abc" (inclusive end of WORD) + #[case("abc def", 0, "abc", " def")] + // dE on '-' in "abc-def ghi": cuts "-def" (rest of WORD) + #[case("abc-def ghi", 3, "-def", "abc ghi")] + fn test_cut_vi_big_word_right_end( + #[case] input: &str, + #[case] position: usize, + #[case] expected_cut: &str, + #[case] expected_buffer: &str, + ) { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.cut_vi_big_word_right_end(); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + + #[rstest] + // ye from start of "abc def": yanks "abc" (inclusive), buffer unchanged + #[case("abc def", 0, "abc", "abc def")] + // ye on 'a' in "abc.def": yanks "abc" + #[case("abc.def", 0, "abc", "abc.def")] + // ye on '.' in "a.b.c": yanks ".b" + #[case("a.b.c", 1, ".b", "a.b.c")] + fn test_copy_vi_word_right_end( + #[case] input: &str, + #[case] position: usize, + #[case] expected_copy: &str, + #[case] expected_buffer: &str, + ) { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.copy_vi_word_right_end(); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_copy); + } + + #[rstest] + // yE from start of "abc-def ghi": yanks "abc-def" (inclusive), buffer unchanged + #[case("abc-def ghi", 0, "abc-def", "abc-def ghi")] + // yE from '-' in "abc-def ghi": yanks "-def" + #[case("abc-def ghi", 3, "-def", "abc-def ghi")] + fn test_copy_vi_big_word_right_end( + #[case] input: &str, + #[case] position: usize, + #[case] expected_copy: &str, + #[case] expected_buffer: &str, + ) { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.copy_vi_big_word_right_end(); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_copy); + } + + #[rstest] + // db from end of "abc.def": cuts "def" + #[case("abc.def", 7, "def", "abc.")] + // db from '.' in "abc.def": cuts "." + #[case("abc.def", 4, ".", "abcdef")] + // db from end of "abc def": cuts "def" + #[case("abc def", 7, "def", "abc ")] + fn test_cut_vi_word_left( + #[case] input: &str, + #[case] position: usize, + #[case] expected_cut: &str, + #[case] expected_buffer: &str, + ) { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.cut_vi_word_left(); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_cut); + } + + #[rstest] + // yb from end of "abc.def": yanks "def", buffer unchanged + #[case("abc.def", 7, "def", "abc.def")] + // yb from '.' in "abc.def": yanks "." + #[case("abc.def", 4, ".", "abc.def")] + // yb from end of "abc def": yanks "def" + #[case("abc def", 7, "def", "abc def")] + fn test_copy_vi_word_left( + #[case] input: &str, + #[case] position: usize, + #[case] expected_copy: &str, + #[case] expected_buffer: &str, + ) { + let mut editor = editor_with(input); + editor.line_buffer.set_insertion_point(position); + + editor.copy_vi_word_left(); + + assert_eq!(editor.get_buffer(), expected_buffer); + assert_eq!(editor.cut_buffer.get().0, expected_copy); + } + #[rstest] #[case("hello world", 0, 'l', 1, false, "lo world")] #[case("hello world", 0, 'l', 1, true, "llo world")] diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index 416e13fe..d2f57bf0 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -191,7 +191,18 @@ impl LineBuffer { } /// Cursor position *behind* the next word to the right + /// + /// Uses Unicode UAX #29 word boundaries. For Vi-style word boundaries, + /// use [`vi_word_right_index`](Self::vi_word_right_index). + #[deprecated(note = "use emacs_word_right_index or vi_word_right_index for clarity")] pub fn word_right_index(&self) -> usize { + self.emacs_word_right_index() + } + + /// Cursor position *behind* the next Emacs word to the right + /// + /// Uses Unicode UAX #29 word boundaries. + pub fn emacs_word_right_index(&self) -> usize { self.lines[self.insertion_point..] .split_word_bound_indices() .find(|(_, word)| !is_whitespace_str(word)) @@ -199,6 +210,14 @@ impl LineBuffer { .unwrap_or_else(|| self.lines.len()) } + /// Cursor position *behind* the next Vi word to the right + pub fn vi_word_right_index(&self) -> usize { + vi_word_segments(&self.lines[self.insertion_point..]) + .find(|(_, word)| !is_whitespace_str(word)) + .map(|(i, word)| self.insertion_point + i + word.len()) + .unwrap_or_else(|| self.lines.len()) + } + /// Cursor position *behind* the next WORD to the right pub fn big_word_right_index(&self) -> usize { let mut found_ws = false; @@ -214,7 +233,18 @@ impl LineBuffer { } /// Cursor position *at end of* the next word to the right + /// + /// Uses Unicode UAX #29 word boundaries. For Vi-style word boundaries, + /// use [`vi_word_right_end_index`](Self::vi_word_right_end_index). + #[deprecated(note = "use emacs_word_right_end_index or vi_word_right_end_index for clarity")] pub fn word_right_end_index(&self) -> usize { + self.emacs_word_right_end_index() + } + + /// Cursor position *at end of* the next Emacs word to the right + /// + /// Uses Unicode UAX #29 word boundaries. + pub fn emacs_word_right_end_index(&self) -> usize { self.lines[self.insertion_point..] .split_word_bound_indices() .find_map(|(i, word)| { @@ -232,6 +262,24 @@ impl LineBuffer { }) } + /// Cursor position *at end of* the next Vi word to the right + pub fn vi_word_right_end_index(&self) -> usize { + vi_word_segments(&self.lines[self.insertion_point..]) + .find_map(|(i, word)| { + word.grapheme_indices(true) + .next_back() + .map(|x| self.insertion_point + x.0 + i) + .filter(|x| !is_whitespace_str(word) && *x != self.insertion_point) + }) + .unwrap_or_else(|| { + self.lines + .grapheme_indices(true) + .next_back() + .map(|x| x.0) + .unwrap_or(0) + }) + } + /// Cursor position *at end of* the next WORD to the right pub fn big_word_right_end_index(&self) -> usize { self.lines[self.insertion_point..] @@ -258,7 +306,20 @@ impl LineBuffer { } /// Cursor position *in front of* the next word to the right + /// + /// Uses Unicode UAX #29 word boundaries. For Vi-style word boundaries, + /// use [`vi_word_right_start_index`](Self::vi_word_right_start_index). + #[deprecated( + note = "use emacs_word_right_start_index or vi_word_right_start_index for clarity" + )] pub fn word_right_start_index(&self) -> usize { + self.emacs_word_right_start_index() + } + + /// Cursor position *in front of* the next Emacs word to the right + /// + /// Uses Unicode UAX #29 word boundaries. + pub fn emacs_word_right_start_index(&self) -> usize { self.lines[self.insertion_point..] .split_word_bound_indices() .find(|(i, word)| *i != 0 && !is_whitespace_str(word)) @@ -266,6 +327,14 @@ impl LineBuffer { .unwrap_or_else(|| self.lines.len()) } + /// Cursor position *in front of* the next Vi word to the right + pub fn vi_word_right_start_index(&self) -> usize { + vi_word_segments(&self.lines[self.insertion_point..]) + .find(|(i, word)| *i != 0 && !is_whitespace_str(word)) + .map(|(i, _)| self.insertion_point + i) + .unwrap_or_else(|| self.lines.len()) + } + /// Cursor position *in front of* the next WORD to the right pub fn big_word_right_start_index(&self) -> usize { let mut found_ws = false; @@ -281,7 +350,18 @@ impl LineBuffer { } /// Cursor position *in front of* the next word to the left + /// + /// Uses Unicode UAX #29 word boundaries. For Vi-style word boundaries, + /// use [`vi_word_left_index`](Self::vi_word_left_index). + #[deprecated(note = "use emacs_word_left_index or vi_word_left_index for clarity")] pub fn word_left_index(&self) -> usize { + self.emacs_word_left_index() + } + + /// Cursor position *in front of* the next Emacs word to the left + /// + /// Uses Unicode UAX #29 word boundaries. + pub fn emacs_word_left_index(&self) -> usize { self.lines[..self.insertion_point] .split_word_bound_indices() .rfind(|(_, word)| !is_whitespace_str(word)) @@ -289,6 +369,15 @@ impl LineBuffer { .unwrap_or(0) } + /// Cursor position *in front of* the next Vi word to the left + pub fn vi_word_left_index(&self) -> usize { + vi_word_segments(&self.lines[..self.insertion_point]) + .filter(|(_, word)| !is_whitespace_str(word)) + .last() + .map(|(i, _)| i) + .unwrap_or(0) + } + /// Cursor position *in front of* the next WORD to the left pub fn big_word_left_index(&self) -> usize { self.lines[..self.insertion_point] @@ -372,7 +461,7 @@ impl LineBuffer { /// Move cursor position *in front of* the next word to the left pub fn move_word_left(&mut self) { - self.insertion_point = self.word_left_index(); + self.insertion_point = self.emacs_word_left_index(); } /// Move cursor position *in front of* the next WORD to the left @@ -382,12 +471,12 @@ impl LineBuffer { /// Move cursor position *behind* the next word to the right pub fn move_word_right(&mut self) { - self.insertion_point = self.word_right_index(); + self.insertion_point = self.emacs_word_right_index(); } /// Move cursor position to the start of the next word pub fn move_word_right_start(&mut self) { - self.insertion_point = self.word_right_start_index(); + self.insertion_point = self.emacs_word_right_start_index(); } /// Move cursor position to the start of the next WORD @@ -397,7 +486,7 @@ impl LineBuffer { /// Move cursor position to the end of the next word pub fn move_word_right_end(&mut self) { - self.insertion_point = self.word_right_end_index(); + self.insertion_point = self.emacs_word_right_end_index(); } /// Move cursor position to the end of the next WORD @@ -518,8 +607,20 @@ impl LineBuffer { } /// Gets the range of the word the current edit position is pointing to + /// + /// Uses Unicode UAX #29 word boundaries. For Vi-style word boundaries, + /// use [`vi_current_word_range`](Self::vi_current_word_range). + #[deprecated(note = "use emacs_current_word_range or vi_current_word_range for clarity")] + #[allow(deprecated)] pub fn current_word_range(&self) -> Range { - let right_index = self.word_right_index(); + self.emacs_current_word_range() + } + + /// Gets the range of the Emacs word the current edit position is pointing to + /// + /// Uses Unicode UAX #29 word boundaries. + pub fn emacs_current_word_range(&self) -> Range { + let right_index = self.emacs_word_right_index(); let left_index = self.lines[..right_index] .split_word_bound_indices() .rfind(|(_, word)| !is_whitespace_str(word)) @@ -529,6 +630,18 @@ impl LineBuffer { left_index..right_index } + /// Gets the range of the Vi word the current edit position is pointing to + pub fn vi_current_word_range(&self) -> Range { + let right_index = self.vi_word_right_index(); + let left_index = vi_word_segments(&self.lines[..right_index]) + .filter(|(_, word)| !is_whitespace_str(word)) + .last() + .map(|(i, _)| i) + .unwrap_or(0); + + left_index..right_index + } + /// Range over the current line /// /// Starts on the first non-newline character and is an exclusive range @@ -547,7 +660,7 @@ impl LineBuffer { /// Uppercases the current word pub fn uppercase_word(&mut self) { - let change_range = self.current_word_range(); + let change_range = self.emacs_current_word_range(); let uppercased = self.get_buffer()[change_range.clone()].to_uppercase(); self.replace_range(change_range, &uppercased); self.move_word_right(); @@ -555,7 +668,7 @@ impl LineBuffer { /// Lowercases the current word pub fn lowercase_word(&mut self) { - let change_range = self.current_word_range(); + let change_range = self.emacs_current_word_range(); let uppercased = self.get_buffer()[change_range.clone()].to_lowercase(); self.replace_range(change_range, &uppercased); self.move_word_right(); @@ -623,22 +736,22 @@ impl LineBuffer { /// Deletes one word to the left pub fn delete_word_left(&mut self) { - let left_word_index = self.word_left_index(); + let left_word_index = self.emacs_word_left_index(); self.clear_range(left_word_index..self.insertion_point()); self.insertion_point = left_word_index; } /// Deletes one word to the right pub fn delete_word_right(&mut self) { - let right_word_index = self.word_right_index(); + let right_word_index = self.emacs_word_right_index(); self.clear_range(self.insertion_point()..right_word_index); } /// Swaps current word with word on right pub fn swap_words(&mut self) { - let word_1_range = self.current_word_range(); + let word_1_range = self.emacs_current_word_range(); self.move_word_right(); - let word_2_range = self.current_word_range(); + let word_2_range = self.emacs_current_word_range(); if word_1_range != word_2_range { self.move_word_left(); @@ -1065,6 +1178,56 @@ impl LineBuffer { } } +/// Character class for Vi-style word motions (w/e/b). +/// +/// Vi defines three classes of characters: +/// - **Keyword**: letters, digits, and underscores +/// - **Punctuation**: non-blank, non-keyword characters +/// - **Whitespace**: spaces, tabs, and other blanks +/// +/// A *word* is a maximal sequence of keyword characters OR a maximal sequence +/// of punctuation characters. Transitions between classes are word boundaries. +/// +/// Reference: Vim's `:help word`, GNU Readline's `_rl_isident()`, POSIX vi. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ViCharClass { + Keyword, + Punctuation, + Whitespace, +} + +impl ViCharClass { + fn of(c: char) -> Self { + if c.is_alphanumeric() || c == '_' { + Self::Keyword + } else if c.is_whitespace() { + Self::Whitespace + } else { + Self::Punctuation + } + } +} + +/// Iterate over Vi-style word segments in a string. +/// +/// Each yielded item is `(byte_offset, substring)` where all characters in +/// `substring` belong to the same [`ViCharClass`]. +fn vi_word_segments(s: &str) -> impl Iterator { + let mut chars = s.char_indices().peekable(); + std::iter::from_fn(move || { + let (first_idx, first_char) = chars.next()?; + let class = ViCharClass::of(first_char); + while let Some(&(_, next_char)) = chars.peek() { + if ViCharClass::of(next_char) != class { + break; + } + chars.next(); + } + let end = chars.peek().map_or(s.len(), |&(i, _)| i); + Some((first_idx, &s[first_idx..end])) + }) +} + /// Match any sequence of characters that are considered a word boundary fn is_whitespace_str(s: &str) -> bool { s.chars().all(char::is_whitespace) @@ -1807,7 +1970,7 @@ mod test { let mut line_buffer = buffer_with(input); line_buffer.set_insertion_point(position); - let index = line_buffer.word_left_index(); + let index = line_buffer.emacs_word_left_index(); assert_eq!(index, expected); } @@ -1842,7 +2005,7 @@ mod test { let mut line_buffer = buffer_with(input); line_buffer.set_insertion_point(position); - let index = line_buffer.word_right_start_index(); + let index = line_buffer.emacs_word_right_start_index(); assert_eq!(index, expected); } @@ -1879,7 +2042,7 @@ mod test { let mut line_buffer = buffer_with(input); line_buffer.set_insertion_point(position); - let index = line_buffer.word_right_end_index(); + let index = line_buffer.emacs_word_right_end_index(); assert_eq!(index, expected); } @@ -1907,6 +2070,200 @@ mod test { assert_eq!(index, expected); } + // ── Vi word segmentation tests ────────────────────────────────────── + + #[test] + fn test_vi_word_segments_basic() { + let segs: Vec<_> = vi_word_segments("abc def").collect(); + assert_eq!(segs, vec![(0, "abc"), (3, " "), (4, "def")]); + } + + #[test] + fn test_vi_word_segments_punctuation() { + // a.b.c → 5 segments: a, ., b, ., c + let segs: Vec<_> = vi_word_segments("a.b.c").collect(); + assert_eq!(segs, vec![(0, "a"), (1, "."), (2, "b"), (3, "."), (4, "c")]); + } + + #[test] + fn test_vi_word_segments_consecutive_punct() { + // "..." is a single punctuation word + let segs: Vec<_> = vi_word_segments("...").collect(); + assert_eq!(segs, vec![(0, "...")]); + } + + #[test] + fn test_vi_word_segments_mixed() { + let segs: Vec<_> = vi_word_segments("foo_bar.baz").collect(); + assert_eq!(segs, vec![(0, "foo_bar"), (7, "."), (8, "baz")]); + } + + #[test] + fn test_vi_word_segments_colons() { + let segs: Vec<_> = vi_word_segments("abc:def").collect(); + assert_eq!(segs, vec![(0, "abc"), (3, ":"), (4, "def")]); + } + + #[test] + fn test_vi_word_segments_mixed_punct() { + let segs: Vec<_> = vi_word_segments("abc...def").collect(); + assert_eq!(segs, vec![(0, "abc"), (3, "..."), (6, "def")]); + } + + #[test] + fn test_vi_word_segments_empty() { + let segs: Vec<_> = vi_word_segments("").collect(); + assert_eq!(segs, Vec::<(usize, &str)>::new()); + } + + // ── Additional Vi word motion tests (punctuation) ─────────────────── + + #[rstest] + // a.b.c: w from 'a' → position 1 (the dot) + #[case("a.b.c", 0, 1)] + // abc:def: w from 'a' → position 3 (the colon) + #[case("abc:def", 0, 3)] + // abc...def: w from 'a' → position 3 (first dot) + #[case("abc...def", 0, 3)] + // foo_bar.baz: w from 'f' → position 7 (the dot) + #[case("foo_bar.baz", 0, 7)] + // a.b.c: w from position 1 (dot) → position 2 ('b') + #[case("a.b.c", 1, 2)] + fn test_word_right_start_index_punct( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_right_start_index(), expected); + } + + #[rstest] + // a.b.c: e from 'a' → position 1 (the dot, next non-ws word end) + #[case("a.b.c", 0, 1)] + // abc:def: e from 'a' → position 2 (end of 'abc') + #[case("abc:def", 0, 2)] + // abc...def: e from 'a' → position 2 (end of 'abc') + #[case("abc...def", 0, 2)] + // foo_bar.baz: e from 'f' → position 6 (end of 'foo_bar') + #[case("foo_bar.baz", 0, 6)] + // a.b.c: e from 'b' (pos 2) → position 3 (the dot after b) + #[case("a.b.c", 2, 3)] + fn test_word_right_end_index_punct( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_right_end_index(), expected); + } + + #[rstest] + // a.b.c: b from 'c' (pos 4) → position 3 (the second dot) + #[case("a.b.c", 4, 3)] + // abc:def: b from end (pos 7) → position 4 ('d') + #[case("abc:def", 7, 4)] + // abc:def: b from 'd' (pos 4) → position 3 (':') + #[case("abc:def", 4, 3)] + // abc...def: b from end (pos 9) → position 6 ('d') + #[case("abc...def", 9, 6)] + // foo_bar.baz: b from end (pos 11) → position 8 ('b') + #[case("foo_bar.baz", 11, 8)] + // a.b.c.d: b from '.' between c and d (pos 5) → position 4 ('c') + #[case("a.b.c.d", 5, 4)] + fn test_word_left_index_punct( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_left_index(), expected); + } + + // ── Vi word motion tests (basic / spaces / edge cases) ───────────── + + #[rstest] + #[case("abc def ghi", 0, 4)] // basic: skip 'abc' + space + #[case("abc def ghi", 4, 8)] // from second word + #[case("abc", 0, 3)] // single word → end + #[case("", 0, 0)] // empty string + #[case(" abc", 0, 2)] // leading spaces + fn test_vi_word_right_start_index( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_right_start_index(), expected); + } + + #[rstest] + #[case("abc def ghi", 0, 2)] // end of 'abc' + #[case("abc", 1, 2)] // from middle of word + #[case("abc", 2, 2)] // already at end + #[case("abc def", 2, 6)] // skip to end of 'def' + fn test_vi_word_right_end_index( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_right_end_index(), expected); + } + + #[rstest] + #[case("abc def ghi", 10, 8)] // from last word → start of 'ghi' + #[case("abc def ghi", 8, 4)] // from 'ghi' → start of 'def' + #[case("abc", 3, 0)] // single word → start + #[case("abc", 0, 0)] // already at start + fn test_vi_word_left_index( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_left_index(), expected); + } + + #[rstest] + #[case("abc def", 0, 3)] // first word end + #[case("abc def", 4, 7)] // second word end + #[case("abc", 0, 3)] // single word + #[case("a.b", 0, 1)] // stops at punctuation boundary + #[case("abc...def", 0, 3)] // stops at punctuation boundary + fn test_vi_word_right_index( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_word_right_index(), expected); + } + + #[rstest] + #[case("abc def", 0, 0..3)] // cursor on 'a' → word "abc" + #[case("abc def", 4, 4..7)] // cursor on 'd' → word "def" + #[case("abc.def", 0, 0..3)] // cursor on 'a' → word "abc" (not "abc.def") + #[case("abc.def", 3, 3..4)] // cursor on '.' → word "." + #[case("abc.def", 4, 4..7)] // cursor on 'd' → word "def" + #[case("a.b.c", 2, 2..3)] // cursor on 'b' → word "b" + fn test_vi_current_word_range( + #[case] input: &str, + #[case] position: usize, + #[case] expected: Range, + ) { + let mut line_buffer = buffer_with(input); + line_buffer.set_insertion_point(position); + assert_eq!(line_buffer.vi_current_word_range(), expected); + } + #[rstest] #[case("abc def", 0, 3)] #[case("abc def ghi", 3, 7)] diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 18999e5c..2fbf5bdf 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -335,11 +335,15 @@ impl Command { Motion::NextBigWord => Some(vec![ReedlineOption::Edit( EditCommand::CutBigWordRightToNext, )]), - Motion::NextWordEnd => Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]), - Motion::NextBigWordEnd => { - Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordRight)]) + Motion::NextWordEnd => { + Some(vec![ReedlineOption::Edit(EditCommand::CutViWordRightEnd)]) + } + Motion::NextBigWordEnd => Some(vec![ReedlineOption::Edit( + EditCommand::CutViBigWordRightEnd, + )]), + Motion::PreviousWord => { + Some(vec![ReedlineOption::Edit(EditCommand::CutViWordLeft)]) } - Motion::PreviousWord => Some(vec![ReedlineOption::Edit(EditCommand::CutWordLeft)]), Motion::PreviousBigWord => { Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordLeft)]) } @@ -393,18 +397,20 @@ impl Command { ReedlineOption::Edit(EditCommand::MoveToLineStart { select: false }), ReedlineOption::Edit(EditCommand::CutToLineEnd), ]), - Motion::NextWord => Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]), - Motion::NextBigWord => { - Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordRight)]) + Motion::NextWord => { + Some(vec![ReedlineOption::Edit(EditCommand::CutViWordRightEnd)]) } + Motion::NextBigWord => Some(vec![ReedlineOption::Edit( + EditCommand::CutViBigWordRightEnd, + )]), Motion::NextWordEnd => { - Some(vec![ReedlineOption::Edit(EditCommand::CutWordRight)]) - } - Motion::NextBigWordEnd => { - Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordRight)]) + Some(vec![ReedlineOption::Edit(EditCommand::CutViWordRightEnd)]) } + Motion::NextBigWordEnd => Some(vec![ReedlineOption::Edit( + EditCommand::CutViBigWordRightEnd, + )]), Motion::PreviousWord => { - Some(vec![ReedlineOption::Edit(EditCommand::CutWordLeft)]) + Some(vec![ReedlineOption::Edit(EditCommand::CutViWordLeft)]) } Motion::PreviousBigWord => { Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordLeft)]) @@ -470,11 +476,15 @@ impl Command { Motion::NextBigWord => Some(vec![ReedlineOption::Edit( EditCommand::CopyBigWordRightToNext, )]), - Motion::NextWordEnd => Some(vec![ReedlineOption::Edit(EditCommand::CopyWordRight)]), - Motion::NextBigWordEnd => { - Some(vec![ReedlineOption::Edit(EditCommand::CopyBigWordRight)]) + Motion::NextWordEnd => { + Some(vec![ReedlineOption::Edit(EditCommand::CopyViWordRightEnd)]) + } + Motion::NextBigWordEnd => Some(vec![ReedlineOption::Edit( + EditCommand::CopyViBigWordRightEnd, + )]), + Motion::PreviousWord => { + Some(vec![ReedlineOption::Edit(EditCommand::CopyViWordLeft)]) } - Motion::PreviousWord => Some(vec![ReedlineOption::Edit(EditCommand::CopyWordLeft)]), Motion::PreviousBigWord => { Some(vec![ReedlineOption::Edit(EditCommand::CopyBigWordLeft)]) } diff --git a/src/edit_mode/vi/motion.rs b/src/edit_mode/vi/motion.rs index 6fac6ec7..d525cc57 100644 --- a/src/edit_mode/vi/motion.rs +++ b/src/edit_mode/vi/motion.rs @@ -195,13 +195,13 @@ impl Motion { ReedlineEvent::Down, ])) }], - Motion::NextWord => vec![ReedlineOption::Edit(EditCommand::MoveWordRightStart { + Motion::NextWord => vec![ReedlineOption::Edit(EditCommand::MoveViWordRightStart { select: select_mode, })], Motion::NextBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightStart { select: select_mode, })], - Motion::NextWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveWordRightEnd { + Motion::NextWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveViWordRightEnd { select: select_mode, })], Motion::NextBigWordEnd => { @@ -209,7 +209,7 @@ impl Motion { select: select_mode, })] } - Motion::PreviousWord => vec![ReedlineOption::Edit(EditCommand::MoveWordLeft { + Motion::PreviousWord => vec![ReedlineOption::Edit(EditCommand::MoveViWordLeft { select: select_mode, })], Motion::PreviousBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordLeft { diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index da0839cd..4fde9b41 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -491,7 +491,7 @@ mod tests { ReedlineEvent::Up, ])]))] #[case(&['w'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart{select:false}])]))] + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveViWordRightStart{select:false}])]))] #[case(&['W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart{select:false}])]))] #[case(&['2', 'l'], ReedlineEvent::Multiple(vec![ @@ -526,22 +526,22 @@ mod tests { ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] #[case(&['d', 'w'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRightToNext])]))] #[case(&['d', 'W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRightToNext])]))] - #[case(&['d', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight])]))] - #[case(&['d', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft])]))] + #[case(&['d', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViWordRightEnd])]))] + #[case(&['d', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViWordLeft])]))] #[case(&['d', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft])]))] #[case(&['c', 'c'], ReedlineEvent::Multiple(vec![ ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]), ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))] - #[case(&['c', 'w'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', 'W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordRight]), ReedlineEvent::Repaint]))] - #[case(&['c', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft]), ReedlineEvent::Repaint]))] + #[case(&['c', 'w'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViWordRightEnd]), ReedlineEvent::Repaint]))] + #[case(&['c', 'W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViBigWordRightEnd]), ReedlineEvent::Repaint]))] + #[case(&['c', 'e'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViWordRightEnd]), ReedlineEvent::Repaint]))] + #[case(&['c', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViWordLeft]), ReedlineEvent::Repaint]))] #[case(&['c', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft]), ReedlineEvent::Repaint]))] #[case(&['d', 'h'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Backspace])]))] #[case(&['d', 'l'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::Delete])]))] #[case(&['2', 'd', 'd'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] // #[case(&['d', 'j'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] // #[case(&['d', 'k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Up, ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine]), ReedlineEvent::Edit(vec![EditCommand::CutCurrentLine])]))] - #[case(&['d', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight])]))] + #[case(&['d', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViBigWordRightEnd])]))] #[case(&['d', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))] #[case(&['d', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart])]))] #[case(&['d', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd])]))] @@ -551,7 +551,7 @@ mod tests { #[case(&['d', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')])]))] #[case(&['d', 'g', 'g'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromStartLinewise { leave_blank_line: false }])]))] #[case(&['d', 'G'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToEndLinewise { leave_blank_line: false }])]))] - #[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] + #[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutViBigWordRightEnd]), ReedlineEvent::Repaint]))] #[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))] #[case(&['c', '^'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineNonBlankStart]), ReedlineEvent::Repaint]))] #[case(&['c', '$'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutToLineEnd]), ReedlineEvent::Repaint]))] @@ -622,7 +622,7 @@ mod tests { ])]))] #[case(&['k'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveLineUp { select: true }])]))] #[case(&['w'], - ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveWordRightStart{select:true}])]))] + ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveViWordRightStart{select:true}])]))] #[case(&['W'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::MoveBigWordRightStart{select:true}])]))] #[case(&['2', 'l'], ReedlineEvent::Multiple(vec![ diff --git a/src/enums.rs b/src/enums.rs index 46228e0b..07384fca 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -194,6 +194,24 @@ pub enum EditCommand { select: bool, }, + /// Move one Vi word to the left (uses Vi three-class word boundaries) + MoveViWordLeft { + /// Select the text between the current cursor position and destination + select: bool, + }, + + /// Move to the start of the next Vi word (uses Vi three-class word boundaries) + MoveViWordRightStart { + /// Select the text between the current cursor position and destination + select: bool, + }, + + /// Move to the end of the next Vi word (uses Vi three-class word boundaries) + MoveViWordRightEnd { + /// Select the text between the current cursor position and destination + select: bool, + }, + /// Move to position MoveToPosition { /// Position to move to @@ -302,12 +320,21 @@ pub enum EditCommand { /// Cut the word right of the insertion point CutBigWordRight, + /// Cut from the insertion point to the end of the next word (inclusive, for Vi `de`) + CutViWordRightEnd, + + /// Cut from the insertion point to the end of the next WORD (inclusive, for Vi `dE`) + CutViBigWordRightEnd, + /// Cut the word right of the insertion point and any following space CutWordRightToNext, /// Cut the WORD right of the insertion point and any following space CutBigWordRightToNext, + /// Cut the Vi word left of the insertion point (uses Vi three-class word boundaries) + CutViWordLeft, + /// Paste the cut buffer in front of the insertion point (Emacs, vi `P`) PasteCutBufferBefore, @@ -424,12 +451,21 @@ pub enum EditCommand { /// Copy the WORD left of the insertion point CopyBigWordLeft, + /// Copy the Vi word left of the insertion point (uses Vi three-class word boundaries) + CopyViWordLeft, + /// Copy the word right of the insertion point CopyWordRight, /// Copy the WORD right of the insertion point CopyBigWordRight, + /// Copy from the insertion point to the end of the next word (inclusive, for Vi `ye`) + CopyViWordRightEnd, + + /// Copy from the insertion point to the end of the next WORD (inclusive, for Vi `yE`) + CopyViBigWordRightEnd, + /// Copy the word right of the insertion point and any following space CopyWordRightToNext, @@ -545,6 +581,15 @@ impl Display for EditCommand { EditCommand::MoveBigWordRightEnd { .. } => { write!(f, "MoveBigWordRightEnd Optional[select: ]") } + EditCommand::MoveViWordLeft { .. } => { + write!(f, "MoveViWordLeft Optional[select: ]") + } + EditCommand::MoveViWordRightStart { .. } => { + write!(f, "MoveViWordRightStart Optional[select: ]") + } + EditCommand::MoveViWordRightEnd { .. } => { + write!(f, "MoveViWordRightEnd Optional[select: ]") + } EditCommand::MoveWordRightStart { .. } => { write!(f, "MoveWordRightStart Optional[select: ]") } @@ -592,8 +637,11 @@ impl Display for EditCommand { EditCommand::CutBigWordLeft => write!(f, "CutBigWordLeft"), EditCommand::CutWordRight => write!(f, "CutWordRight"), EditCommand::CutBigWordRight => write!(f, "CutBigWordRight"), + EditCommand::CutViWordRightEnd => write!(f, "CutViWordRightEnd"), + EditCommand::CutViBigWordRightEnd => write!(f, "CutViBigWordRightEnd"), EditCommand::CutWordRightToNext => write!(f, "CutWordRightToNext"), EditCommand::CutBigWordRightToNext => write!(f, "CutBigWordRightToNext"), + EditCommand::CutViWordLeft => write!(f, "CutViWordLeft"), EditCommand::PasteCutBufferBefore => write!(f, "PasteCutBufferBefore"), EditCommand::PasteCutBufferAfter => write!(f, "PasteCutBufferAfter"), EditCommand::UppercaseWord => write!(f, "UppercaseWord"), @@ -624,8 +672,11 @@ impl Display for EditCommand { EditCommand::CopyCurrentLine => write!(f, "CopyCurrentLine"), EditCommand::CopyWordLeft => write!(f, "CopyWordLeft"), EditCommand::CopyBigWordLeft => write!(f, "CopyBigWordLeft"), + EditCommand::CopyViWordLeft => write!(f, "CopyViWordLeft"), EditCommand::CopyWordRight => write!(f, "CopyWordRight"), EditCommand::CopyBigWordRight => write!(f, "CopyBigWordRight"), + EditCommand::CopyViWordRightEnd => write!(f, "CopyViWordRightEnd"), + EditCommand::CopyViBigWordRightEnd => write!(f, "CopyViBigWordRightEnd"), EditCommand::CopyWordRightToNext => write!(f, "CopyWordRightToNext"), EditCommand::CopyBigWordRightToNext => write!(f, "CopyBigWordRightToNext"), EditCommand::CopyLeft => write!(f, "CopyLeft"), @@ -674,6 +725,9 @@ impl EditCommand { | EditCommand::MoveBigWordRightStart { select, .. } | EditCommand::MoveWordRightEnd { select, .. } | EditCommand::MoveBigWordRightEnd { select, .. } + | EditCommand::MoveViWordLeft { select, .. } + | EditCommand::MoveViWordRightStart { select, .. } + | EditCommand::MoveViWordRightEnd { select, .. } | EditCommand::MoveRightUntil { select, .. } | EditCommand::MoveRightBefore { select, .. } | EditCommand::MoveLeftUntil { select, .. } @@ -712,8 +766,11 @@ impl EditCommand { | EditCommand::CutBigWordLeft | EditCommand::CutWordRight | EditCommand::CutBigWordRight + | EditCommand::CutViWordRightEnd + | EditCommand::CutViBigWordRightEnd | EditCommand::CutWordRightToNext | EditCommand::CutBigWordRightToNext + | EditCommand::CutViWordLeft | EditCommand::PasteCutBufferBefore | EditCommand::PasteCutBufferAfter | EditCommand::UppercaseWord @@ -750,8 +807,11 @@ impl EditCommand { | EditCommand::CopyCurrentLine | EditCommand::CopyWordLeft | EditCommand::CopyBigWordLeft + | EditCommand::CopyViWordLeft | EditCommand::CopyWordRight | EditCommand::CopyBigWordRight + | EditCommand::CopyViWordRightEnd + | EditCommand::CopyViBigWordRightEnd | EditCommand::CopyWordRightToNext | EditCommand::CopyBigWordRightToNext | EditCommand::CopyLeft