From b525e3de1c0695e1f542c300540ec096834b0e24 Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:32:46 +0800 Subject: [PATCH 1/8] fix Vi mode change: restore normal mode after cut operations in visual mode --- src/edit_mode/vi/parser.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index 0699dbc1..aebc7c8f 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -114,7 +114,12 @@ impl ParsedViSequence { { Some(ViMode::Insert) } - (Some(Command::Delete), ParseResult::Incomplete) => Some(ViMode::Normal), + (Some(Command::Delete), ParseResult::Incomplete) + | (Some(Command::DeleteChar), ParseResult::Incomplete) + | (Some(Command::DeleteToEnd), ParseResult::Incomplete) + | (Some(Command::Delete), ParseResult::Valid(_)) + | (Some(Command::DeleteChar), ParseResult::Valid(_)) + | (Some(Command::DeleteToEnd), ParseResult::Valid(_)) => Some(ViMode::Normal), _ => None, } } From 9ff187e4808bbb9ece810f68fa57a4eecc139140 Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Tue, 31 Dec 2024 11:45:43 +0800 Subject: [PATCH 2/8] fix Cut/Change/Delete under visual-mode: Align Vim standards --- src/core_editor/editor.rs | 11 +++++++++-- src/edit_mode/vi/command.rs | 18 +++++++++++++++--- src/edit_mode/vi/mod.rs | 3 +-- src/edit_mode/vi/parser.rs | 3 ++- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 227606eb..1e908561 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -564,10 +564,17 @@ impl Editor { /// The range is guaranteed to be ascending. pub fn get_selection(&self) -> Option<(usize, usize)> { self.selection_anchor.map(|selection_anchor| { + let buffer_len = self.line_buffer.len(); if self.insertion_point() > selection_anchor { - (selection_anchor, self.insertion_point()) + ( + selection_anchor, + (self.insertion_point() + 1).min(buffer_len), + ) } else { - (self.insertion_point(), selection_anchor) + ( + self.insertion_point(), + (selection_anchor + 1).min(buffer_len), + ) } }) } diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index f00b549b..6bbddcd9 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -1,4 +1,4 @@ -use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption}; +use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption, ViMode}; use crate::{EditCommand, ReedlineEvent, Vi}; use std::iter::Peekable; @@ -166,11 +166,23 @@ impl Command { select: false, })], Self::RewriteCurrentLine => vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)], - Self::DeleteChar => vec![ReedlineOption::Edit(EditCommand::CutChar)], + Self::DeleteChar => { + if vi_state.mode == ViMode::Visual { + vec![ReedlineOption::Edit(EditCommand::CutSelection)] + } else { + vec![ReedlineOption::Edit(EditCommand::CutChar)] + } + } Self::ReplaceChar(c) => { vec![ReedlineOption::Edit(EditCommand::ReplaceChar(*c))] } - Self::SubstituteCharWithInsert => vec![ReedlineOption::Edit(EditCommand::CutChar)], + Self::SubstituteCharWithInsert => { + if vi_state.mode == ViMode::Visual { + vec![ReedlineOption::Edit(EditCommand::CutSelection)] + } else { + vec![ReedlineOption::Edit(EditCommand::CutChar)] + } + } Self::HistorySearch => vec![ReedlineOption::Event(ReedlineEvent::SearchHistory)], Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)], // Whenever a motion is required to finish the command we must be in visual mode diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 81fa5b04..b2621aa6 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -89,11 +89,10 @@ impl EditMode for Vi { self.cache.clear(); ReedlineEvent::None } else if res.is_complete(self.mode) { + let event = res.to_reedline_event(self); if let Some(mode) = res.changes_mode() { self.mode = mode; } - - let event = res.to_reedline_event(self); self.cache.clear(); event } else { diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index aebc7c8f..9a9c79ba 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -108,7 +108,8 @@ impl ParsedViSequence { | (Some(Command::RewriteCurrentLine), ParseResult::Incomplete) | (Some(Command::SubstituteCharWithInsert), ParseResult::Incomplete) | (Some(Command::HistorySearch), ParseResult::Incomplete) - | (Some(Command::Change), ParseResult::Valid(_)) => Some(ViMode::Insert), + | (Some(Command::Change), ParseResult::Valid(_)) + | (Some(Command::Change), ParseResult::Incomplete) => Some(ViMode::Insert), (Some(Command::ChangeInside(char)), ParseResult::Incomplete) if is_valid_change_inside_left(char) || is_valid_change_inside_right(char) => { From 76a46e40c37c6e31924d437fa40e6bcbe7897943 Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Tue, 31 Dec 2024 16:25:50 +0800 Subject: [PATCH 3/8] fix ESC behavior under visual mode: clear selection when pressing ESC --- src/edit_mode/vi/mod.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index b2621aa6..efb6bcf9 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -142,7 +142,16 @@ impl EditMode for Vi { (_, KeyModifiers::NONE, KeyCode::Esc) => { self.cache.clear(); self.mode = ViMode::Normal; - ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) + ReedlineEvent::Multiple(vec![ + // Move left then right to clear the selection. + // Order matters here: this makes sure cursor does not move at end of line + ReedlineEvent::Edit(vec![ + EditCommand::MoveLeft { select: false }, + EditCommand::MoveRight { select: false }, + ]), + ReedlineEvent::Esc, + ReedlineEvent::Repaint, + ]) } (_, KeyModifiers::NONE, KeyCode::Enter) => { self.mode = ViMode::Insert; @@ -191,7 +200,14 @@ mod test { assert_eq!( result, - ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![ + EditCommand::MoveLeft { select: false }, + EditCommand::MoveRight { select: false } + ]), + ReedlineEvent::Esc, + ReedlineEvent::Repaint + ]) ); assert!(matches!(vi.mode, ViMode::Normal)); } From 980355db980a43435ca3199471e5a96dd9b07c82 Mon Sep 17 00:00:00 2001 From: WHOWHOWHOWHOWHOWHOWHOWHOWHOWHO Date: Wed, 1 Jan 2025 09:47:11 +0800 Subject: [PATCH 4/8] fix ESC behavior under visual mode: follow-up: Implement ReedlineEvent::ResetSelection --- src/core_editor/editor.rs | 4 ++++ src/edit_mode/vi/mod.rs | 12 ++---------- src/engine.rs | 8 ++++++++ src/enums.rs | 4 ++++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 1e908561..ea784c68 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -655,6 +655,10 @@ impl Editor { self.delete_selection(); insert_clipboard_content_before(&mut self.line_buffer, self.cut_buffer.deref_mut()); } + + pub(crate) fn reset_selection(&mut self) { + self.selection_anchor = None; + } } fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) { diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index efb6bcf9..806a19c6 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -143,12 +143,7 @@ impl EditMode for Vi { self.cache.clear(); self.mode = ViMode::Normal; ReedlineEvent::Multiple(vec![ - // Move left then right to clear the selection. - // Order matters here: this makes sure cursor does not move at end of line - ReedlineEvent::Edit(vec![ - EditCommand::MoveLeft { select: false }, - EditCommand::MoveRight { select: false }, - ]), + ReedlineEvent::ResetSelection, ReedlineEvent::Esc, ReedlineEvent::Repaint, ]) @@ -201,10 +196,7 @@ mod test { assert_eq!( result, ReedlineEvent::Multiple(vec![ - ReedlineEvent::Edit(vec![ - EditCommand::MoveLeft { select: false }, - EditCommand::MoveRight { select: false } - ]), + ReedlineEvent::ResetSelection, ReedlineEvent::Esc, ReedlineEvent::Repaint ]) diff --git a/src/engine.rs b/src/engine.rs index dd9b02f8..c08a27e5 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -909,6 +909,10 @@ impl Reedline { self.input_mode = InputMode::Regular; Ok(EventStatus::Handled) } + ReedlineEvent::ResetSelection => { + self.editor.reset_selection(); + Ok(EventStatus::Handled) + } // TODO: Check if events should be handled ReedlineEvent::Right | ReedlineEvent::Left @@ -1197,6 +1201,10 @@ impl Reedline { Ok(EventStatus::Handled) } ReedlineEvent::OpenEditor => self.open_editor().map(|_| EventStatus::Handled), + ReedlineEvent::ResetSelection => { + self.editor.reset_selection(); + Ok(EventStatus::Handled) + } ReedlineEvent::Resize(width, height) => { self.painter.handle_resize(width, height); Ok(EventStatus::Handled) diff --git a/src/enums.rs b/src/enums.rs index cde42b1a..3e4848e7 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -644,6 +644,9 @@ pub enum ReedlineEvent { /// Open text editor OpenEditor, + + /// Reset the current text selection + ResetSelection, } impl Display for ReedlineEvent { @@ -687,6 +690,7 @@ impl Display for ReedlineEvent { ReedlineEvent::MenuPagePrevious => write!(f, "MenuPagePrevious"), ReedlineEvent::ExecuteHostCommand(_) => write!(f, "ExecuteHostCommand"), ReedlineEvent::OpenEditor => write!(f, "OpenEditor"), + ReedlineEvent::ResetSelection => write!(f, "ResetSelection"), } } } From 57c2128ed1f8d97258fb9fa0de041d21c8ffc5d5 Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Wed, 1 Jan 2025 10:19:28 +0800 Subject: [PATCH 5/8] fix Vi mode change: follow-up: Change/Delete + Incomplete motion could yield mode change from visual to insert/normal --- src/edit_mode/vi/mod.rs | 2 +- src/edit_mode/vi/parser.rs | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 806a19c6..cc19cbd6 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -90,7 +90,7 @@ impl EditMode for Vi { ReedlineEvent::None } else if res.is_complete(self.mode) { let event = res.to_reedline_event(self); - if let Some(mode) = res.changes_mode() { + if let Some(mode) = res.changes_mode(self.mode) { self.mode = mode; } self.cache.clear(); diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index 9a9c79ba..71ce2b1a 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -98,7 +98,7 @@ impl ParsedViSequence { } } - pub fn changes_mode(&self) -> Option { + pub fn changes_mode(&self, mode: ViMode) -> Option { match (&self.command, &self.motion) { (Some(Command::EnterViInsert), ParseResult::Incomplete) | (Some(Command::EnterViAppend), ParseResult::Incomplete) @@ -108,19 +108,18 @@ impl ParsedViSequence { | (Some(Command::RewriteCurrentLine), ParseResult::Incomplete) | (Some(Command::SubstituteCharWithInsert), ParseResult::Incomplete) | (Some(Command::HistorySearch), ParseResult::Incomplete) - | (Some(Command::Change), ParseResult::Valid(_)) - | (Some(Command::Change), ParseResult::Incomplete) => Some(ViMode::Insert), - (Some(Command::ChangeInside(char)), ParseResult::Incomplete) + | (Some(Command::Change), ParseResult::Valid(_)) => Some(ViMode::Insert), + (Some(Command::Change), ParseResult::Incomplete) if mode == ViMode::Visual => { + Some(ViMode::Insert) + } + (Some(Command::Delete), ParseResult::Incomplete) if mode == ViMode::Visual => { + Some(ViMode::Normal) + } + (Some(Command::ChangeInside(char)), ParseResult::Valid(_)) if is_valid_change_inside_left(char) || is_valid_change_inside_right(char) => { Some(ViMode::Insert) } - (Some(Command::Delete), ParseResult::Incomplete) - | (Some(Command::DeleteChar), ParseResult::Incomplete) - | (Some(Command::DeleteToEnd), ParseResult::Incomplete) - | (Some(Command::Delete), ParseResult::Valid(_)) - | (Some(Command::DeleteChar), ParseResult::Valid(_)) - | (Some(Command::DeleteToEnd), ParseResult::Valid(_)) => Some(ViMode::Normal), _ => None, } } From a4784733f9bb6835b990952cf2ee70594b44d93d Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Sat, 18 Jan 2025 10:15:14 +0800 Subject: [PATCH 6/8] fix: Cut/Change/Delete under visual-mode (follow-up): selection range considers UTF8 --- src/core_editor/editor.rs | 6 ++++-- src/core_editor/line_buffer.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index ea784c68..bd4c03bb 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -568,12 +568,14 @@ impl Editor { if self.insertion_point() > selection_anchor { ( selection_anchor, - (self.insertion_point() + 1).min(buffer_len), + self.line_buffer.grapheme_right_index().min(buffer_len), ) } else { ( self.insertion_point(), - (selection_anchor + 1).min(buffer_len), + self.line_buffer + .grapheme_right_index_from_pos(selection_anchor) + .min(buffer_len), ) } }) diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index ce5dc889..f221b4ab 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -168,6 +168,15 @@ impl LineBuffer { .unwrap_or(0) } + /// Cursor position *behind* the next unicode grapheme to the right from the given position + pub fn grapheme_right_index_from_pos(&self, pos: usize) -> usize { + self.lines[pos..] + .grapheme_indices(true) + .nth(1) + .map(|(i, _)| pos + i) + .unwrap_or_else(|| self.lines.len()) + } + /// Cursor position *behind* the next word to the right pub fn word_right_index(&self) -> usize { self.lines[self.insertion_point..] @@ -1597,4 +1606,26 @@ mod test { assert_eq!(index, expected); } + + #[rstest] + #[case("abc", 0, 1)] // Basic ASCII + #[case("abc", 1, 2)] // From middle position + #[case("abc", 2, 3)] // From last char + #[case("abc", 3, 3)] // From end of string + #[case("🦀rust", 0, 4)] // Unicode emoji + #[case("🦀rust", 4, 5)] // After emoji + #[case("é́", 0, 4)] // Combining characters + fn test_grapheme_right_index_from_pos( + #[case] input: &str, + #[case] position: usize, + #[case] expected: usize, + ) { + let mut line = LineBuffer::new(); + line.insert_str(input); + assert_eq!( + line.grapheme_right_index_from_pos(position), + expected, + "input: {input:?}, pos: {position}" + ); + } } From 1490b4293a3424e4db1c12cef087e9646495dfe6 Mon Sep 17 00:00:00 2001 From: "TechnoHouse (deephbz)" <13776377+deephbz@users.noreply.github.com> Date: Sat, 18 Jan 2025 13:05:37 +0800 Subject: [PATCH 7/8] Vi-Mode Feature: Atomic unified commands for ChangeInside/DeleteInside/YankInside(to be added): - added thorough unittests to emulate standard Vi behavior. - handle nested brackets but not nested quotes, alinged with Vi behavior --- src/core_editor/editor.rs | 100 +++++++++++++++++++++++++++++ src/core_editor/line_buffer.rs | 113 +++++++++++++++++++++++++++++++++ src/edit_mode/vi/command.rs | 102 ++++++++++++----------------- src/edit_mode/vi/parser.rs | 17 ++--- src/enums.rs | 10 +++ 5 files changed, 273 insertions(+), 69 deletions(-) diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index bd4c03bb..b79d8f00 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -126,6 +126,10 @@ impl Editor { EditCommand::CopySelectionSystem => self.copy_selection_to_system(), #[cfg(feature = "system_clipboard")] EditCommand::PasteSystem => self.paste_from_system(), + EditCommand::CutInside { + left_char, + right_char, + } => self.cut_inside(*left_char, *right_char), } if !matches!(command.edit_type(), EditType::MoveCursor { select: true }) { self.selection_anchor = None; @@ -661,6 +665,35 @@ impl Editor { pub(crate) fn reset_selection(&mut self) { self.selection_anchor = None; } + + /// Delete text strictly between matching `left_char` and `right_char`. + /// Places deleted text into the cut buffer. + /// Leaves the parentheses/quotes/etc. themselves. + /// On success, move the cursor just after the `left_char`. + /// If matching chars can't be found, restore the original cursor. + pub(crate) fn cut_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); + self.line_buffer.clear_range_safe(inside_start, rp); + } + self.line_buffer + .set_insertion_point(lp + left_char.len_utf8()); + return; + } + } + // If no valid pair was found, restore original cursor + self.line_buffer.set_insertion_point(old_pos); + } } fn insert_clipboard_content_before(line_buffer: &mut LineBuffer, clipboard: &mut dyn Clipboard) { @@ -911,4 +944,71 @@ mod test { pretty_assertions::assert_eq!(editor.line_buffer.len(), s.len() * 2); } } + + #[test] + fn test_cut_inside_brackets() { + let mut editor = editor_with("foo(bar)baz"); + editor.move_to_position(5, false); // Move inside brackets + editor.cut_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo()baz"); + assert_eq!(editor.insertion_point(), 4); + assert_eq!(editor.cut_buffer.get().0, "bar"); + + // Test with cursor outside brackets + let mut editor = editor_with("foo(bar)baz"); + editor.move_to_position(0, false); + editor.cut_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo(bar)baz"); + assert_eq!(editor.insertion_point(), 0); + assert_eq!(editor.cut_buffer.get().0, ""); + + // Test with no matching brackets + let mut editor = editor_with("foo bar baz"); + editor.move_to_position(4, false); + editor.cut_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_cut_inside_quotes() { + let mut editor = editor_with("foo\"bar\"baz"); + editor.move_to_position(5, false); // Move inside quotes + editor.cut_inside('"', '"'); + assert_eq!(editor.get_buffer(), "foo\"\"baz"); + assert_eq!(editor.insertion_point(), 4); + assert_eq!(editor.cut_buffer.get().0, "bar"); + + // Test with cursor outside quotes + let mut editor = editor_with("foo\"bar\"baz"); + editor.move_to_position(0, false); + editor.cut_inside('"', '"'); + assert_eq!(editor.get_buffer(), "foo\"bar\"baz"); + assert_eq!(editor.insertion_point(), 0); + assert_eq!(editor.cut_buffer.get().0, ""); + + // Test with no matching quotes + let mut editor = editor_with("foo bar baz"); + editor.move_to_position(4, false); + editor.cut_inside('"', '"'); + assert_eq!(editor.get_buffer(), "foo bar baz"); + assert_eq!(editor.insertion_point(), 4); + } + + #[test] + fn test_cut_inside_nested() { + let mut editor = editor_with("foo(bar(baz)qux)quux"); + editor.move_to_position(8, false); // Move inside inner brackets + editor.cut_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo(bar()qux)quux"); + assert_eq!(editor.insertion_point(), 8); + assert_eq!(editor.cut_buffer.get().0, "baz"); + + editor.move_to_position(4, false); // Move inside outer brackets + editor.cut_inside('(', ')'); + assert_eq!(editor.get_buffer(), "foo()quux"); + assert_eq!(editor.insertion_point(), 4); + assert_eq!(editor.cut_buffer.get().0, "bar()qux"); + } } diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index f221b4ab..0b3ae15f 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -776,6 +776,119 @@ impl LineBuffer { self.insertion_point = index + c.len_utf8(); } } + + /// Attempts to find the matching `(left_char, right_char)` pair *enclosing* + /// the cursor position, respecting nested pairs. + /// + /// Algorithm: + /// 1. Walk left from `cursor` until we find the "outermost" `left_char`, + /// ignoring any extra `right_char` we see (i.e., we keep a depth counter). + /// 2. Then from that left bracket, walk right to find the matching `right_char`, + /// also respecting nesting. + /// + /// Returns `Some((left_index, right_index))` if found, or `None` otherwise. + pub fn find_matching_pair( + &self, + left_char: char, + right_char: char, + cursor: usize, + ) -> Option<(usize, usize)> { + // Special case: quotes or the same char for left & right + // (Vi doesn't do nested quotes, so no depth counting). + if left_char == right_char { + // 1) Walk left to find the first matching quote + let mut scan_pos = cursor; + while scan_pos > 0 { + // Move left by one grapheme + let mut tmp = LineBuffer { + lines: self.lines.clone(), + insertion_point: scan_pos, + }; + tmp.move_left(); + scan_pos = tmp.insertion_point; + + if scan_pos >= self.lines.len() { + break; + } + let ch = self.lines[scan_pos..].chars().next().unwrap_or('\0'); + if ch == left_char { + // Found the "left quote" + let left_index = scan_pos; + // 2) Now walk right to find the next matching quote + let mut scan_pos_r = left_index + left_char.len_utf8(); + while scan_pos_r < self.lines.len() { + let next_ch = self.lines[scan_pos_r..].chars().next().unwrap(); + if next_ch == right_char { + return Some((left_index, scan_pos_r)); + } + scan_pos_r += next_ch.len_utf8(); + } + return None; // no right quote found + } + } + return None; // no left quote found + } + + // Step 1: search left + let mut scan_pos = cursor; + let mut depth = 0; + + while scan_pos > 0 { + // Move left by one grapheme + scan_pos = { + // a small helper to move left from an arbitrary position + let mut tmp = LineBuffer { + lines: self.lines.clone(), + insertion_point: scan_pos, + }; + tmp.move_left(); + tmp.insertion_point + }; + if scan_pos >= self.lines.len() { + break; + } + + let ch = self.lines[scan_pos..].chars().next().unwrap_or('\0'); + + if ch == left_char && depth == 0 { + // Found the "outermost" left bracket + let left_index = scan_pos; + // Step 2: search right from `left_index + left_char.len_utf8()` to find matching + let mut scan_pos_r = left_index + left_char.len_utf8(); + let mut depth_r = 0; + + while scan_pos_r < self.lines.len() { + let next_ch = self.lines[scan_pos_r..].chars().next().unwrap(); + if next_ch == left_char { + depth_r += 1; + } else if next_ch == right_char { + if depth_r == 0 { + // Found the matching close + let right_index = scan_pos_r; + return Some((left_index, right_index)); + } else { + depth_r -= 1; + } + } + scan_pos_r += next_ch.len_utf8(); + } + // Matching right bracket not found + return None; + } else if ch == right_char { + // This means we are "inside" nested parentheses, so increment nesting + depth += 1; + } else if ch == left_char { + // If we see another left_char while depth>0, it just closes one nesting level + if depth > 0 { + depth -= 1; + } else { + // That would be the outer bracket if depth==0, + // but we handle that in the `if ch == left_char && depth == 0` above + } + } + } + None + } } /// Match any sequence of characters that are considered a word boundary diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 6bbddcd9..9c191ddb 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -9,13 +9,16 @@ where match input.peek() { Some('d') => { let _ = input.next(); + // Checking for "di(" or "di)" etc. if let Some('i') = input.peek() { let _ = input.next(); match input.next() { - Some(c) - if is_valid_change_inside_left(c) || is_valid_change_inside_right(c) => - { - Some(Command::DeleteInside(*c)) + Some(&c) => { + if let Some((l, r)) = bracket_pair_for(c) { + Some(Command::DeleteInsidePair { left: l, right: r }) + } else { + None + } } _ => None, } @@ -43,15 +46,18 @@ where let _ = input.next(); Some(Command::Undo) } + // Checking for "ci(" or "ci)" etc. Some('c') => { let _ = input.next(); if let Some('i') = input.peek() { let _ = input.next(); match input.next() { - Some(c) - if is_valid_change_inside_left(c) || is_valid_change_inside_right(c) => - { - Some(Command::ChangeInside(*c)) + Some(&c) => { + if let Some((l, r)) = bracket_pair_for(c) { + Some(Command::ChangeInsidePair { left: l, right: r }) + } else { + None + } } _ => None, } @@ -131,8 +137,9 @@ pub enum Command { HistorySearch, Switchcase, RepeatLastAction, - ChangeInside(char), - DeleteInside(char), + // 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 }, } impl Command { @@ -192,39 +199,17 @@ impl Command { Some(event) => vec![ReedlineOption::Event(event.clone())], None => vec![], }, - Self::ChangeInside(left) if is_valid_change_inside_left(left) => { - let right = bracket_for(left); - vec![ - ReedlineOption::Edit(EditCommand::CutLeftBefore(*left)), - ReedlineOption::Edit(EditCommand::CutRightBefore(right)), - ] + Self::ChangeInsidePair { left, right } => { + vec![ReedlineOption::Edit(EditCommand::CutInside { + left_char: *left, + right_char: *right, + })] } - Self::ChangeInside(right) if is_valid_change_inside_right(right) => { - let left = bracket_for(right); - vec![ - ReedlineOption::Edit(EditCommand::CutLeftBefore(left)), - ReedlineOption::Edit(EditCommand::CutRightBefore(*right)), - ] - } - Self::ChangeInside(_) => { - vec![] - } - Self::DeleteInside(left) if is_valid_change_inside_left(left) => { - let right = bracket_for(left); - vec![ - ReedlineOption::Edit(EditCommand::CutLeftBefore(*left)), - ReedlineOption::Edit(EditCommand::CutRightBefore(right)), - ] - } - Self::DeleteInside(right) if is_valid_change_inside_right(right) => { - let left = bracket_for(right); - vec![ - ReedlineOption::Edit(EditCommand::CutLeftBefore(left)), - ReedlineOption::Edit(EditCommand::CutRightBefore(*right)), - ] - } - Self::DeleteInside(_) => { - vec![] + Self::DeleteInsidePair { left, right } => { + vec![ReedlineOption::Edit(EditCommand::CutInside { + left_char: *left, + right_char: *right, + })] } } } @@ -349,24 +334,19 @@ impl Command { } } -fn bracket_for(c: &char) -> char { - match *c { - '(' => ')', - '[' => ']', - '{' => '}', - '<' => '>', - ')' => '(', - ']' => '[', - '}' => '{', - '>' => '<', - _ => *c, +fn bracket_pair_for(c: char) -> Option<(char, char)> { + match c { + '(' => Some(('(', ')')), + '[' => Some(('[', ']')), + '{' => Some(('{', '}')), + '<' => Some(('<', '>')), + ')' => Some(('(', ')')), + ']' => Some(('[', ']')), + '}' => Some(('{', '}')), + '>' => Some(('<', '>')), + '"' => Some(('"', '"')), + '\'' => Some(('\'', '\'')), + '`' => Some(('`', '`')), + _ => None, } } - -pub(crate) fn is_valid_change_inside_left(c: &char) -> bool { - matches!(c, '(' | '[' | '{' | '"' | '\'' | '`' | '<') -} - -pub(crate) fn is_valid_change_inside_right(c: &char) -> bool { - matches!(c, ')' | ']' | '}' | '"' | '\'' | '`' | '>') -} diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index 71ce2b1a..00f78fc1 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -1,6 +1,4 @@ -use super::command::{ - is_valid_change_inside_left, is_valid_change_inside_right, parse_command, Command, -}; +use super::command::{parse_command, Command}; use super::motion::{parse_motion, Motion}; use crate::{edit_mode::vi::ViMode, EditCommand, ReedlineEvent, Vi}; use std::iter::Peekable; @@ -115,11 +113,14 @@ impl ParsedViSequence { (Some(Command::Delete), ParseResult::Incomplete) if mode == ViMode::Visual => { Some(ViMode::Normal) } - (Some(Command::ChangeInside(char)), ParseResult::Valid(_)) - if is_valid_change_inside_left(char) || is_valid_change_inside_right(char) => - { - Some(ViMode::Insert) - } + (Some(Command::ChangeInsidePair { .. }), _) => Some(ViMode::Insert), + (Some(Command::Delete), ParseResult::Incomplete) + | (Some(Command::DeleteChar), ParseResult::Incomplete) + | (Some(Command::DeleteToEnd), ParseResult::Incomplete) + | (Some(Command::Delete), ParseResult::Valid(_)) + | (Some(Command::DeleteChar), ParseResult::Valid(_)) + | (Some(Command::DeleteToEnd), ParseResult::Valid(_)) + | (Some(Command::DeleteInsidePair { .. }), _) => Some(ViMode::Normal), _ => None, } } diff --git a/src/enums.rs b/src/enums.rs index 3e4848e7..7f1f0655 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -278,6 +278,14 @@ pub enum EditCommand { /// Paste content from system clipboard at the current cursor position #[cfg(feature = "system_clipboard")] PasteSystem, + + /// Delete text between matching characters atomically + CutInside { + /// Left character of the pair + left_char: char, + /// Right character of the pair (usually matching bracket) + right_char: char, + }, } impl Display for EditCommand { @@ -371,6 +379,7 @@ impl Display for EditCommand { EditCommand::CopySelectionSystem => write!(f, "CopySelectionSystem"), #[cfg(feature = "system_clipboard")] EditCommand::PasteSystem => write!(f, "PasteSystem"), + EditCommand::CutInside { .. } => write!(f, "CutInside Value: "), } } } @@ -451,6 +460,7 @@ impl EditCommand { EditCommand::CopySelection => EditType::NoOp, #[cfg(feature = "system_clipboard")] EditCommand::CopySelectionSystem => EditType::NoOp, + EditCommand::CutInside { .. } => EditType::EditText, } } } 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 8/8] 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, } } }