diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 96e8e911..73425e7a 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -3,6 +3,7 @@ use super::{edit_stack::EditStack, Clipboard, ClipboardMode, LineBuffer}; use crate::core_editor::get_system_clipboard; use crate::enums::{EditType, TextObject, TextObjectScope, TextObjectType, UndoBehavior}; use crate::{core_editor::get_local_clipboard, EditCommand}; +use std::cmp::{max, min}; use std::ops::{DerefMut, Range}; /// Stateful editor executing changes to the underlying [`LineBuffer`] @@ -50,6 +51,9 @@ impl Editor { match command { EditCommand::MoveToStart { select } => self.move_to_start(*select), EditCommand::MoveToLineStart { select } => self.move_to_line_start(*select), + EditCommand::MoveToLineNonBlankStart { select } => { + self.move_to_line_non_blank_start(*select) + } EditCommand::MoveToEnd { select } => self.move_to_end(*select), EditCommand::MoveToLineEnd { select } => self.move_to_line_end(*select), EditCommand::MoveToPosition { position, select } => { @@ -82,6 +86,7 @@ impl Editor { EditCommand::CutCurrentLine => self.cut_current_line(), EditCommand::CutFromStart => self.cut_from_start(), EditCommand::CutFromLineStart => self.cut_from_line_start(), + EditCommand::CutFromLineNonBlankStart => self.cut_from_line_non_blank_start(), EditCommand::CutToEnd => self.cut_from_end(), EditCommand::CutToLineEnd => self.cut_to_line_end(), EditCommand::KillLine => self.kill_line(), @@ -123,6 +128,7 @@ impl Editor { EditCommand::Paste => self.paste_cut_buffer(), EditCommand::CopyFromStart => self.copy_from_start(), EditCommand::CopyFromLineStart => self.copy_from_line_start(), + EditCommand::CopyFromLineNonBlankStart => self.copy_from_line_non_blank_start(), EditCommand::CopyToEnd => self.copy_from_end(), EditCommand::CopyToLineEnd => self.copy_to_line_end(), EditCommand::CopyWordLeft => self.copy_word_left(), @@ -291,6 +297,11 @@ impl Editor { self.line_buffer.move_to_line_start(); } + pub(crate) fn move_to_line_non_blank_start(&mut self, select: bool) { + self.update_selection_anchor(select); + self.line_buffer.move_to_line_non_blank_start(); + } + pub(crate) fn move_to_line_end(&mut self, select: bool) { self.update_selection_anchor(select); self.line_buffer.move_to_line_end(); @@ -351,6 +362,14 @@ impl Editor { } } + fn cut_from_line_non_blank_start(&mut self) { + let offset_a = self.line_buffer.insertion_point(); + self.line_buffer.move_to_line_non_blank_start(); + let offset_b = self.line_buffer.insertion_point(); + let deletion_range = min(offset_a, offset_b)..max(offset_a, offset_b); + self.cut_range(deletion_range); + } + fn cut_from_end(&mut self) { let cut_slice = &self.line_buffer.get_buffer()[self.line_buffer.insertion_point()..]; if !cut_slice.is_empty() { @@ -852,6 +871,15 @@ impl Editor { self.copy_range(copy_range); } + pub(crate) fn copy_from_line_non_blank_start(&mut self) { + let offset_a = self.line_buffer.insertion_point(); + self.line_buffer.move_to_line_non_blank_start(); + let offset_b = self.line_buffer.insertion_point(); + self.line_buffer.set_insertion_point(offset_a); + let copy_range = min(offset_a, offset_b)..max(offset_a, offset_b); + self.copy_range(copy_range); + } + pub(crate) fn copy_from_end(&mut self) { let copy_range = self.line_buffer.insertion_point()..self.line_buffer.len(); self.copy_range(copy_range); diff --git a/src/core_editor/line_buffer.rs b/src/core_editor/line_buffer.rs index 57d79f27..7530ec50 100644 --- a/src/core_editor/line_buffer.rs +++ b/src/core_editor/line_buffer.rs @@ -113,6 +113,19 @@ impl LineBuffer { // str is guaranteed to be utf8, thus \n is safe to assume 1 byte long } + /// Move the cursor before the first non whitespace character of the line + pub fn move_to_line_non_blank_start(&mut self) { + let line_start = self.lines[..self.insertion_point] + .rfind('\n') + .map_or(0, |offset| offset + 1); + // str is guaranteed to be utf8, thus \n is safe to assume 1 byte long + + self.insertion_point = self.lines[line_start..] + .find(|c: char| !c.is_whitespace() || c == '\n') + .map(|offset| line_start + offset) + .unwrap_or(self.lines.len()); + } + /// Move cursor position to the end of the line /// /// Insertion will append to the line. diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 8c856418..1a0765f8 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -344,10 +344,25 @@ impl Command { Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))]) } Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]), + Motion::NonBlankStart => Some(vec![ReedlineOption::Edit( + EditCommand::CutFromLineNonBlankStart, + )]), Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]), Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]), Motion::Up => None, Motion::Down => None, + Motion::FirstLine => Some(vec![ + ReedlineOption::Edit(EditCommand::MoveToLineEnd { select: false }), + // `MoveRight` to include the new line character + ReedlineOption::Edit(EditCommand::MoveRight { select: false }), + ReedlineOption::Edit(EditCommand::CutFromStart), + ]), + Motion::LastLine => Some(vec![ + ReedlineOption::Edit(EditCommand::MoveToLineStart { select: false }), + // `MoveLeft` to include the new line character + ReedlineOption::Edit(EditCommand::MoveLeft { select: false }), + ReedlineOption::Edit(EditCommand::CutToEnd), + ]), Motion::ReplayCharSearch => vi_state .last_char_search .as_ref() @@ -399,10 +414,21 @@ impl Command { Motion::Start => { Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]) } + Motion::NonBlankStart => Some(vec![ReedlineOption::Edit( + EditCommand::CutFromLineNonBlankStart, + )]), Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]), Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]), Motion::Up => None, Motion::Down => None, + Motion::FirstLine => Some(vec![ + ReedlineOption::Edit(EditCommand::MoveToLineEnd { select: false }), + ReedlineOption::Edit(EditCommand::CutFromStart), + ]), + Motion::LastLine => Some(vec![ + ReedlineOption::Edit(EditCommand::MoveToLineStart { select: false }), + ReedlineOption::Edit(EditCommand::CutToEnd), + ]), Motion::ReplayCharSearch => vi_state .last_char_search .as_ref() @@ -453,10 +479,24 @@ impl Command { Some(vec![ReedlineOption::Edit(EditCommand::CopyLeftBefore(*c))]) } Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CopyFromLineStart)]), + Motion::NonBlankStart => Some(vec![ReedlineOption::Edit( + EditCommand::CopyFromLineNonBlankStart, + )]), Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::CopyLeft)]), Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::CopyRight)]), Motion::Up => None, Motion::Down => None, + Motion::FirstLine => Some(vec![ + ReedlineOption::Edit(EditCommand::MoveToLineEnd { select: false }), + // `MoveRight` to include the new line character + ReedlineOption::Edit(EditCommand::MoveRight { select: false }), + ReedlineOption::Edit(EditCommand::CopyFromStart), + ReedlineOption::Edit(EditCommand::MoveToStart { select: false }), + ]), + Motion::LastLine => Some(vec![ + ReedlineOption::Edit(EditCommand::MoveToLineStart { select: false }), + ReedlineOption::Edit(EditCommand::CopyToEnd), + ]), Motion::ReplayCharSearch => vi_state .last_char_search .as_ref() diff --git a/src/edit_mode/vi/motion.rs b/src/edit_mode/vi/motion.rs index a59edf3e..99ea54da 100644 --- a/src/edit_mode/vi/motion.rs +++ b/src/edit_mode/vi/motion.rs @@ -52,10 +52,14 @@ where let _ = input.next(); ParseResult::Valid(Motion::NextBigWordEnd) } - Some('0' | '^') => { + Some('0') => { let _ = input.next(); ParseResult::Valid(Motion::Start) } + Some('^') => { + let _ = input.next(); + ParseResult::Valid(Motion::NonBlankStart) + } Some('$') => { let _ = input.next(); ParseResult::Valid(Motion::End) @@ -108,6 +112,21 @@ where let _ = input.next(); ParseResult::Valid(Motion::ReverseCharSearch) } + Some('g') => { + let _ = input.next(); + match input.peek() { + Some('g') => { + input.next(); + ParseResult::Valid(Motion::FirstLine) + } + Some(_) => ParseResult::Invalid, + None => ParseResult::Incomplete, + } + } + Some('G') => { + let _ = input.next(); + ParseResult::Valid(Motion::LastLine) + } ch if ch == command_char.as_ref().as_ref() && command_char.is_some() => { let _ = input.next(); ParseResult::Valid(Motion::Line) @@ -131,7 +150,10 @@ pub enum Motion { PreviousBigWord, Line, Start, + NonBlankStart, End, + FirstLine, + LastLine, RightUntil(char), RightBefore(char), LeftUntil(char), @@ -191,9 +213,20 @@ impl Motion { Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart { select: select_mode, })], + Motion::NonBlankStart => { + vec![ReedlineOption::Edit(EditCommand::MoveToLineNonBlankStart { + select: select_mode, + })] + } Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd { select: select_mode, })], + Motion::FirstLine => vec![ReedlineOption::Edit(EditCommand::MoveToStart { + select: select_mode, + })], + Motion::LastLine => vec![ReedlineOption::Edit(EditCommand::MoveToEnd { + select: select_mode, + })], Motion::RightUntil(ch) => { vi_state.last_char_search = Some(ViCharSearch::ToRight(*ch)); vec![ReedlineOption::Edit(EditCommand::MoveRightUntil { diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index d7bebb8b..7c77fdd5 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -541,20 +541,40 @@ mod tests { // #[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', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart])]))] - #[case(&['d', '^'], 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])]))] #[case(&['d', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')])]))] #[case(&['d', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')])]))] #[case(&['d', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')])]))] #[case(&['d', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')])]))] + #[case(&['d', 'g', 'g'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::CutFromStart]), + ]))] + #[case(&['d', 'G'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::MoveLeft { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::CutToEnd]), + ]))] #[case(&['c', 'E'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordRight]), ReedlineEvent::Repaint]))] #[case(&['c', '0'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutFromLineStart]), ReedlineEvent::Repaint]))] - #[case(&['c', '^'], 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]))] #[case(&['c', 'f', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightUntil('a')]), ReedlineEvent::Repaint]))] #[case(&['c', 't', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutRightBefore('a')]), ReedlineEvent::Repaint]))] #[case(&['c', 'F', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftUntil('a')]), ReedlineEvent::Repaint]))] #[case(&['c', 'T', 'a'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutLeftBefore('a')]), ReedlineEvent::Repaint]))] + #[case(&['c', 'g', 'g'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveToLineEnd { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::CutFromStart]), + ReedlineEvent::Repaint, + ]))] + #[case(&['c', 'G'], ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveToLineStart { select: false }]), + ReedlineEvent::Edit(vec![EditCommand::CutToEnd]), + ReedlineEvent::Repaint, + ]))] fn test_reedline_move(#[case] input: &[char], #[case] expected: ReedlineEvent) { let mut vi = Vi::default(); let res = vi_parse(input); diff --git a/src/enums.rs b/src/enums.rs index 41531ba2..0f6c1b7c 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -72,6 +72,12 @@ pub enum EditCommand { select: bool, }, + /// Move to the start of the current line skipping any whitespace + MoveToLineNonBlankStart { + /// Select the text between the current cursor position and destination + select: bool, + }, + /// Move to the end of the buffer MoveToEnd { /// Select the text between the current cursor position and destination @@ -197,6 +203,9 @@ pub enum EditCommand { /// Cut from the start of the current line to the insertion point CutFromLineStart, + /// Cut from the first non whitespace character of the current line to the insertion point + CutFromLineNonBlankStart, + /// Cut from the insertion point to the end of the buffer CutToEnd, @@ -317,6 +326,9 @@ pub enum EditCommand { /// Copy from the start of the current line to the insertion point CopyFromLineStart, + /// Copy from the first non whitespace character of the current line to the insertion point + CopyFromLineNonBlankStart, + /// Copy from the insertion point to the end of the buffer CopyToEnd, @@ -423,6 +435,9 @@ impl Display for EditCommand { EditCommand::MoveToLineStart { .. } => { write!(f, "MoveToLineStart Optional[select: ]") } + EditCommand::MoveToLineNonBlankStart { .. } => { + write!(f, "MoveToLineNonBlankStart Optional[select: ]") + } EditCommand::MoveToEnd { .. } => write!(f, "MoveToEnd Optional[select: ]"), EditCommand::MoveToLineEnd { .. } => { write!(f, "MoveToLineEnd Optional[select: ]") @@ -473,6 +488,7 @@ impl Display for EditCommand { EditCommand::CutCurrentLine => write!(f, "CutCurrentLine"), EditCommand::CutFromStart => write!(f, "CutFromStart"), EditCommand::CutFromLineStart => write!(f, "CutFromLineStart"), + EditCommand::CutFromLineNonBlankStart => write!(f, "CutFromLineNonBlankStart"), EditCommand::CutToEnd => write!(f, "CutToEnd"), EditCommand::CutToLineEnd => write!(f, "CutToLineEnd"), EditCommand::KillLine => write!(f, "KillLine"), @@ -504,6 +520,7 @@ impl Display for EditCommand { EditCommand::Paste => write!(f, "Paste"), EditCommand::CopyFromStart => write!(f, "CopyFromStart"), EditCommand::CopyFromLineStart => write!(f, "CopyFromLineStart"), + EditCommand::CopyFromLineNonBlankStart => write!(f, "CopyFromLineNonBlankStart"), EditCommand::CopyToEnd => write!(f, "CopyToEnd"), EditCommand::CopyToLineEnd => write!(f, "CopyToLineEnd"), EditCommand::CopyCurrentLine => write!(f, "CopyCurrentLine"), @@ -546,6 +563,7 @@ impl EditCommand { | EditCommand::MoveToEnd { select, .. } | EditCommand::MoveToLineStart { select, .. } | EditCommand::MoveToLineEnd { select, .. } + | EditCommand::MoveToLineNonBlankStart { select, .. } | EditCommand::MoveToPosition { select, .. } | EditCommand::MoveLeft { select, .. } | EditCommand::MoveRight { select, .. } @@ -582,6 +600,7 @@ impl EditCommand { | EditCommand::CutCurrentLine | EditCommand::CutFromStart | EditCommand::CutFromLineStart + | EditCommand::CutFromLineNonBlankStart | EditCommand::CutToLineEnd | EditCommand::KillLine | EditCommand::CutToEnd @@ -622,6 +641,7 @@ impl EditCommand { EditCommand::CopyTextObject { .. } => EditType::NoOp, EditCommand::CopyFromStart | EditCommand::CopyFromLineStart + | EditCommand::CopyFromLineNonBlankStart | EditCommand::CopyToEnd | EditCommand::CopyToLineEnd | EditCommand::CopyCurrentLine