From 1f73f9a8017600218f6dfeeff31fbf01c058bb0d Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Tue, 31 Dec 2024 18:13:59 +0800 Subject: [PATCH] Implement vi-mode "Yank" (copy): - Vi Command; - Editor Command; - Editor methods; - Mode changes back to Normal after yank --- src/core_editor/editor.rs | 264 ++++++++++++++++++++++++++++++++++++ src/edit_mode/vi/command.rs | 78 ++++++++++- src/edit_mode/vi/motion.rs | 9 ++ src/edit_mode/vi/parser.rs | 5 +- src/enums.rs | 94 +++++++++++++ 5 files changed, 448 insertions(+), 2 deletions(-) diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index b79d8f00..8d7b724a 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -120,6 +120,49 @@ impl Editor { EditCommand::CutSelection => self.cut_selection_to_cut_buffer(), EditCommand::CopySelection => self.copy_selection_to_cut_buffer(), EditCommand::Paste => self.paste_cut_buffer(), + EditCommand::CopyFromStart => self.copy_from_start(), + EditCommand::CopyFromLineStart => self.copy_from_line_start(), + EditCommand::CopyToEnd => self.copy_from_end(), + EditCommand::CopyToLineEnd => self.copy_to_line_end(), + EditCommand::CopyWordLeft => self.copy_word_left(), + EditCommand::CopyBigWordLeft => self.copy_big_word_left(), + EditCommand::CopyWordRight => self.copy_word_right(), + EditCommand::CopyBigWordRight => self.copy_big_word_right(), + 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), + EditCommand::CopyRightBefore(c) => self.copy_right_until_char(*c, true, true), + EditCommand::CopyLeftUntil(c) => self.copy_left_until_char(*c, false, true), + EditCommand::CopyLeftBefore(c) => self.copy_left_until_char(*c, true, true), + EditCommand::CopyCurrentLine => { + let range = self.line_buffer.current_line_range(); + let copy_slice = &self.line_buffer.get_buffer()[range]; + if !copy_slice.is_empty() { + self.cut_buffer.set(copy_slice, ClipboardMode::Lines); + } + } + EditCommand::CopyLeft => { + let insertion_offset = self.line_buffer.insertion_point(); + if insertion_offset > 0 { + let left_index = self.line_buffer.grapheme_left_index(); + let copy_range = left_index..insertion_offset; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + EditCommand::CopyRight => { + let insertion_offset = self.line_buffer.insertion_point(); + let right_index = self.line_buffer.grapheme_right_index(); + if right_index > insertion_offset { + let copy_range = insertion_offset..right_index; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } #[cfg(feature = "system_clipboard")] EditCommand::CutSelectionSystem => self.cut_selection_to_system(), #[cfg(feature = "system_clipboard")] @@ -130,6 +173,10 @@ impl Editor { left_char, right_char, } => self.cut_inside(*left_char, *right_char), + EditCommand::YankInside { + left_char, + right_char, + } => self.yank_inside(*left_char, *right_char), } if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) { self.selection_anchor = None; @@ -694,6 +741,165 @@ impl Editor { // If no valid pair was found, restore original cursor self.line_buffer.set_insertion_point(old_pos); } + + pub(crate) fn copy_from_start(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + if insertion_offset > 0 { + self.cut_buffer.set( + &self.line_buffer.get_buffer()[..insertion_offset], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_from_line_start(&mut self) { + let previous_offset = self.line_buffer.insertion_point(); + let start_offset = { + let temp_pos = self.line_buffer.insertion_point(); + self.line_buffer.move_to_line_start(); + let start = self.line_buffer.insertion_point(); + self.line_buffer.set_insertion_point(temp_pos); + start + }; + let copy_range = start_offset..previous_offset; + let copy_slice = &self.line_buffer.get_buffer()[copy_range]; + if !copy_slice.is_empty() { + self.cut_buffer.set(copy_slice, ClipboardMode::Normal); + } + } + + pub(crate) fn copy_from_end(&mut self) { + let copy_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..]; + if !copy_slice.is_empty() { + self.cut_buffer.set(copy_slice, ClipboardMode::Normal); + } + } + + pub(crate) fn copy_to_line_end(&mut self) { + let copy_slice = &self.line_buffer.get_buffer() + [self.line_buffer.insertion_point()..self.line_buffer.find_current_line_end()]; + if !copy_slice.is_empty() { + self.cut_buffer.set(copy_slice, ClipboardMode::Normal); + } + } + + pub(crate) fn copy_word_left(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let left_index = self.line_buffer.word_left_index(); + if left_index < insertion_offset { + let copy_range = left_index..insertion_offset; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_big_word_left(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let left_index = self.line_buffer.big_word_left_index(); + if left_index < insertion_offset { + let copy_range = left_index..insertion_offset; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_word_right(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let right_index = self.line_buffer.word_right_index(); + if right_index > insertion_offset { + let copy_range = insertion_offset..right_index; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_big_word_right(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let right_index = self.line_buffer.next_whitespace(); + if right_index > insertion_offset { + let copy_range = insertion_offset..right_index; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_word_right_to_next(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let right_index = self.line_buffer.word_right_start_index(); + if right_index > insertion_offset { + let copy_range = insertion_offset..right_index; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_big_word_right_to_next(&mut self) { + let insertion_offset = self.line_buffer.insertion_point(); + let right_index = self.line_buffer.big_word_right_start_index(); + if right_index > insertion_offset { + let copy_range = insertion_offset..right_index; + self.cut_buffer.set( + &self.line_buffer.get_buffer()[copy_range], + ClipboardMode::Normal, + ); + } + } + + pub(crate) fn copy_right_until_char(&mut self, c: char, before_char: bool, current_line: bool) { + if let Some(index) = self.line_buffer.find_char_right(c, current_line) { + let extra = if before_char { 0 } else { c.len_utf8() }; + let copy_slice = + &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..index + extra]; + if !copy_slice.is_empty() { + self.cut_buffer.set(copy_slice, ClipboardMode::Normal); + } + } + } + + pub(crate) fn copy_left_until_char(&mut self, c: char, before_char: bool, current_line: bool) { + if let Some(index) = self.line_buffer.find_char_left(c, current_line) { + let extra = if before_char { c.len_utf8() } else { 0 }; + let copy_slice = + &self.line_buffer.get_buffer()[index + extra..self.line_buffer.insertion_point()]; + if !copy_slice.is_empty() { + self.cut_buffer.set(copy_slice, ClipboardMode::Normal); + } + } + } + + /// Yank text strictly between matching `left_char` and `right_char`. + /// Copies it into the cut buffer without removing anything. + /// Leaves the buffer unchanged and restores the original cursor. + pub(crate) fn yank_inside(&mut self, left_char: char, right_char: char) { + let old_pos = self.insertion_point(); + let buffer_len = self.line_buffer.len(); + + if let Some((lp, rp)) = + self.line_buffer + .find_matching_pair(left_char, right_char, self.insertion_point()) + { + let inside_start = lp + left_char.len_utf8(); + if inside_start < rp && rp <= buffer_len { + let inside_slice = &self.line_buffer.get_buffer()[inside_start..rp]; + if !inside_slice.is_empty() { + self.cut_buffer.set(inside_slice, ClipboardMode::Normal); + } + } + } + + // Always restore the cursor position + self.line_buffer.set_insertion_point(old_pos); + } } fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) { @@ -1011,4 +1217,62 @@ mod test { assert_eq!(editor.insertion_point(), 4); assert_eq!(editor.cut_buffer.get().0, "bar()qux"); } + + #[test] + fn test_yank_inside_brackets() { + let mut editor = editor_with("foo(bar)baz"); + editor.move_to_position(5, false); // Move inside brackets + editor.yank_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo(bar)baz"); // Buffer shouldn't change + assert_eq!(editor.insertion_point(), 5); // Cursor should return to original position + + // Test yanked content by pasting + editor.paste_cut_buffer(); + assert_eq!(editor.get_buffer(), "foo(bbarar)baz"); + + // Test with cursor outside brackets + let mut editor = editor_with("foo(bar)baz"); + editor.move_to_position(0, false); + editor.yank_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo(bar)baz"); + assert_eq!(editor.insertion_point(), 0); + } + + #[test] + fn test_yank_inside_quotes() { + let mut editor = editor_with("foo\"bar\"baz"); + editor.move_to_position(5, false); // Move inside quotes + editor.yank_inside('"', '"'); + assert_eq!(editor.get_buffer(), "foo\"bar\"baz"); // Buffer shouldn't change + assert_eq!(editor.insertion_point(), 5); // Cursor should return to original position + assert_eq!(editor.cut_buffer.get().0, "bar"); + + // Test with no matching quotes + let mut editor = editor_with("foo bar baz"); + editor.move_to_position(4, false); + editor.yank_inside('"', '"'); + assert_eq!(editor.get_buffer(), "foo bar baz"); + assert_eq!(editor.insertion_point(), 4); + assert_eq!(editor.cut_buffer.get().0, ""); + } + + #[test] + fn test_yank_inside_nested() { + let mut editor = editor_with("foo(bar(baz)qux)quux"); + editor.move_to_position(8, false); // Move inside inner brackets + editor.yank_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo(bar(baz)qux)quux"); // Buffer shouldn't change + assert_eq!(editor.insertion_point(), 8); + assert_eq!(editor.cut_buffer.get().0, "baz"); + + // Test yanked content by pasting + editor.paste_cut_buffer(); + assert_eq!(editor.get_buffer(), "foo(bar(bazbaz)qux)quux"); + + editor.move_to_position(4, false); // Move inside outer brackets + editor.yank_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo(bar(bazbaz)qux)quux"); + assert_eq!(editor.insertion_point(), 4); + assert_eq!(editor.cut_buffer.get().0, "bar(bazbaz)qux"); + } } diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 9c191ddb..faef45d9 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -26,6 +26,25 @@ where Some(Command::Delete) } } + // Checking for "yi(" or "yi)" etc. + Some('y') => { + let _ = input.next(); + if let Some('i') = input.peek() { + let _ = input.next(); + match input.next() { + Some(&c) => { + if let Some((l, r)) = bracket_pair_for(c) { + Some(Command::YankInsidePair { left: l, right: r }) + } else { + None + } + } + _ => None, + } + } else { + Some(Command::Yank) + } + } Some('p') => { let _ = input.next(); Some(Command::PasteAfter) @@ -137,9 +156,11 @@ pub enum Command { HistorySearch, Switchcase, RepeatLastAction, + Yank, // These DoSthInsidePair commands are agnostic to whether user pressed the left char or right char ChangeInsidePair { left: char, right: char }, DeleteInsidePair { left: char, right: char }, + YankInsidePair { left: char, right: char }, } impl Command { @@ -147,12 +168,13 @@ impl Command { match self { Command::Delete => Some('d'), Command::Change => Some('c'), + Command::Yank => Some('y'), _ => None, } } pub fn requires_motion(&self) -> bool { - matches!(self, Command::Delete | Command::Change) + matches!(self, Command::Delete | Command::Change | Command::Yank) } pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec { @@ -194,6 +216,7 @@ impl Command { Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)], // Whenever a motion is required to finish the command we must be in visual mode Self::Delete | Self::Change => vec![ReedlineOption::Edit(EditCommand::CutSelection)], + Self::Yank => vec![ReedlineOption::Edit(EditCommand::CopySelection)], Self::Incomplete => vec![ReedlineOption::Incomplete], Self::RepeatLastAction => match &vi_state.previous { Some(event) => vec![ReedlineOption::Event(event.clone())], @@ -211,6 +234,12 @@ impl Command { right_char: *right, })] } + Self::YankInsidePair { left, right } => { + vec![ReedlineOption::Edit(EditCommand::YankInside { + left_char: *left, + right_char: *right, + })] + } } } @@ -329,6 +358,53 @@ impl Command { vec }) } + Self::Yank => match motion { + Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::CopyToLineEnd)]), + Motion::Line => Some(vec![ReedlineOption::Edit(EditCommand::CopyCurrentLine)]), + Motion::NextWord => { + Some(vec![ReedlineOption::Edit(EditCommand::CopyWordRightToNext)]) + } + 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::PreviousWord => Some(vec![ReedlineOption::Edit(EditCommand::CopyWordLeft)]), + Motion::PreviousBigWord => { + Some(vec![ReedlineOption::Edit(EditCommand::CopyBigWordLeft)]) + } + Motion::RightUntil(c) => { + vi_state.last_char_search = Some(ViCharSearch::ToRight(*c)); + Some(vec![ReedlineOption::Edit(EditCommand::CopyRightUntil(*c))]) + } + Motion::RightBefore(c) => { + vi_state.last_char_search = Some(ViCharSearch::TillRight(*c)); + Some(vec![ReedlineOption::Edit(EditCommand::CopyRightBefore(*c))]) + } + Motion::LeftUntil(c) => { + vi_state.last_char_search = Some(ViCharSearch::ToLeft(*c)); + Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftUntil(*c))]) + } + Motion::LeftBefore(c) => { + vi_state.last_char_search = Some(ViCharSearch::TillLeft(*c)); + Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftBefore(*c))]) + } + Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CopyFromLineStart)]), + Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::CopyLeft)]), + Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::CopyRight)]), + Motion::Up => None, + Motion::Down => None, + Motion::ReplayCharSearch => vi_state + .last_char_search + .as_ref() + .map(|char_search| vec![ReedlineOption::Edit(char_search.to_copy())]), + Motion::ReverseCharSearch => vi_state + .last_char_search + .as_ref() + .map(|char_search| vec![ReedlineOption::Edit(char_search.reverse().to_copy())]), + }, _ => None, } } diff --git a/src/edit_mode/vi/motion.rs b/src/edit_mode/vi/motion.rs index a0e1ad3c..a59edf3e 100644 --- a/src/edit_mode/vi/motion.rs +++ b/src/edit_mode/vi/motion.rs @@ -295,4 +295,13 @@ impl ViCharSearch { ViCharSearch::TillLeft(c) => EditCommand::CutLeftBefore(*c), } } + + pub fn to_copy(&self) -> EditCommand { + match self { + ViCharSearch::ToRight(c) => EditCommand::CopyRightUntil(*c), + ViCharSearch::TillRight(c) => EditCommand::CopyRightBefore(*c), + ViCharSearch::ToLeft(c) => EditCommand::CopyLeftUntil(*c), + ViCharSearch::TillLeft(c) => EditCommand::CopyLeftBefore(*c), + } + } } diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index 00f78fc1..3c39a3ea 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -120,7 +120,10 @@ impl ParsedViSequence { | (Some(Command::Delete), ParseResult::Valid(_)) | (Some(Command::DeleteChar), ParseResult::Valid(_)) | (Some(Command::DeleteToEnd), ParseResult::Valid(_)) - | (Some(Command::DeleteInsidePair { .. }), _) => Some(ViMode::Normal), + | (Some(Command::Yank), ParseResult::Valid(_)) + | (Some(Command::Yank), ParseResult::Incomplete) + | (Some(Command::DeleteInsidePair { .. }), _) + | (Some(Command::YankInsidePair { .. }), _) => Some(ViMode::Normal), _ => None, } } diff --git a/src/enums.rs b/src/enums.rs index 7f1f0655..3b87b099 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -267,6 +267,57 @@ pub enum EditCommand { /// Paste content from local buffer at the current cursor position Paste, + /// Copy from the start of the buffer to the insertion point + CopyFromStart, + + /// Copy from the start of the current line to the insertion point + CopyFromLineStart, + + /// Copy from the insertion point to the end of the buffer + CopyToEnd, + + /// Copy from the insertion point to the end of the current line + CopyToLineEnd, + + /// Copy the current line + CopyCurrentLine, + + /// Copy the word left of the insertion point + CopyWordLeft, + + /// Copy the WORD left of the insertion point + CopyBigWordLeft, + + /// Copy the word right of the insertion point + CopyWordRight, + + /// Copy the WORD right of the insertion point + CopyBigWordRight, + + /// Copy the word right of the insertion point and any following space + CopyWordRightToNext, + + /// Copy the WORD right of the insertion point and any following space + CopyBigWordRightToNext, + + /// Copy one character to the left + CopyLeft, + + /// Copy one character to the right + CopyRight, + + /// Copy until right until char + CopyRightUntil(char), + + /// Copy right before char + CopyRightBefore(char), + + /// Copy left until char + CopyLeftUntil(char), + + /// Copy left before char + CopyLeftBefore(char), + /// Cut selection to system clipboard #[cfg(feature = "system_clipboard")] CutSelectionSystem, @@ -286,6 +337,13 @@ pub enum EditCommand { /// Right character of the pair (usually matching bracket) right_char: char, }, + /// Yank text between matching characters atomically + YankInside { + /// Left character of the pair + left_char: char, + /// Right character of the pair (usually matching bracket) + right_char: char, + }, } impl Display for EditCommand { @@ -373,6 +431,23 @@ impl Display for EditCommand { EditCommand::CutSelection => write!(f, "CutSelection"), EditCommand::CopySelection => write!(f, "CopySelection"), EditCommand::Paste => write!(f, "Paste"), + EditCommand::CopyFromStart => write!(f, "CopyFromStart"), + EditCommand::CopyFromLineStart => write!(f, "CopyFromLineStart"), + EditCommand::CopyToEnd => write!(f, "CopyToEnd"), + EditCommand::CopyToLineEnd => write!(f, "CopyToLineEnd"), + EditCommand::CopyCurrentLine => write!(f, "CopyCurrentLine"), + EditCommand::CopyWordLeft => write!(f, "CopyWordLeft"), + EditCommand::CopyBigWordLeft => write!(f, "CopyBigWordLeft"), + EditCommand::CopyWordRight => write!(f, "CopyWordRight"), + EditCommand::CopyBigWordRight => write!(f, "CopyBigWordRight"), + EditCommand::CopyWordRightToNext => write!(f, "CopyWordRightToNext"), + EditCommand::CopyBigWordRightToNext => write!(f, "CopyBigWordRightToNext"), + EditCommand::CopyLeft => write!(f, "CopyLeft"), + EditCommand::CopyRight => write!(f, "CopyRight"), + EditCommand::CopyRightUntil(_) => write!(f, "CopyRightUntil Value: "), + EditCommand::CopyRightBefore(_) => write!(f, "CopyRightBefore Value: "), + EditCommand::CopyLeftUntil(_) => write!(f, "CopyLeftUntil Value: "), + EditCommand::CopyLeftBefore(_) => write!(f, "CopyLeftBefore Value: "), #[cfg(feature = "system_clipboard")] EditCommand::CutSelectionSystem => write!(f, "CutSelectionSystem"), #[cfg(feature = "system_clipboard")] @@ -380,6 +455,7 @@ impl Display for EditCommand { #[cfg(feature = "system_clipboard")] EditCommand::PasteSystem => write!(f, "PasteSystem"), EditCommand::CutInside { .. } => write!(f, "CutInside Value: "), + EditCommand::YankInside { .. } => write!(f, "YankInside Value: "), } } } @@ -461,6 +537,24 @@ impl EditCommand { #[cfg(feature = "system_clipboard")] EditCommand::CopySelectionSystem => EditType::NoOp, EditCommand::CutInside { .. } => EditType::EditText, + EditCommand::YankInside { .. } => EditType::EditText, + EditCommand::CopyFromStart + | EditCommand::CopyFromLineStart + | EditCommand::CopyToEnd + | EditCommand::CopyToLineEnd + | EditCommand::CopyCurrentLine + | EditCommand::CopyWordLeft + | EditCommand::CopyBigWordLeft + | EditCommand::CopyWordRight + | EditCommand::CopyBigWordRight + | EditCommand::CopyWordRightToNext + | EditCommand::CopyBigWordRightToNext + | EditCommand::CopyLeft + | EditCommand::CopyRight + | EditCommand::CopyRightUntil(_) + | EditCommand::CopyRightBefore(_) + | EditCommand::CopyLeftUntil(_) + | EditCommand::CopyLeftBefore(_) => EditType::NoOp, } } }