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/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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}" + ); + } }