From 3e92f97da21d62ded95ef9bd2c42386c8f701b99 Mon Sep 17 00:00:00 2001 From: sholderbach Date: Sun, 25 Sep 2022 02:27:46 +0200 Subject: [PATCH] Parse motions properly, fix #449, add combinations (#484) This change truly separates the actions and the motions on the parsing side. Bare motions are parsed by the motion parser instead of the command parser. (This requires a separate `ParseResult` to distinguish valid but incomplete motions like `f` from invalid motions.) Fixes #449 by making `.` its own command. Also introduces the possibility of multiplying the `.` repeat and resticts its effect to the action commands and ignores the bare moves. Simplify and make the `;` and `,` behavior more correct: - Place info in the `Vi` state instead of complicated in-band signal - Support using it with `d;` and `c;` - Store the character search when combined with an action - Rename `ViToTill` more explictly to `ViCharSearch` - **Missing:** the old tests were removed as they tested old quirks The above change also leads to proper separation of concern between parsing and translation to emacs commands (and subsequent execution). Add support for combining `d/c` with `h/l`. **Note:** `j/k` are still omitted as they require new behavior in the linebuffer/editor. Also `dj` has to add 1 to the multiplier to achieve the correct behavior as we do not model line ex-/inclusive behavior. Rename `ParseResult` to `ParsedViSequence` Simplify validity checking in `mod.rs` and have explicit check methods on the types. This fixes problems with `r` and `f/t/T/F` in their partial and complete state and adds a missing test. Rename `last_to_till` to `last_char_search` --- src/edit_mode/vi/command.rs | 278 +++++++++--------------------------- src/edit_mode/vi/mod.rs | 96 +++---------- src/edit_mode/vi/motion.rs | 168 +++++++++++++++++----- src/edit_mode/vi/parser.rs | 233 +++++++++++++++++++++--------- src/engine.rs | 7 +- src/enums.rs | 4 - 6 files changed, 379 insertions(+), 407 deletions(-) diff --git a/src/edit_mode/vi/command.rs b/src/edit_mode/vi/command.rs index 6faae7d..e86f9f3 100644 --- a/src/edit_mode/vi/command.rs +++ b/src/edit_mode/vi/command.rs @@ -1,8 +1,8 @@ -use super::{motion::Motion, motion::ViToTill, parser::ReedlineOption}; +use super::{motion::Motion, motion::ViCharSearch, parser::ReedlineOption}; use crate::{EditCommand, ReedlineEvent, Vi}; use std::iter::Peekable; -pub fn parse_command<'iter, I>(vi: &Vi, input: &mut Peekable) -> Option +pub fn parse_command<'iter, I>(input: &mut Peekable) -> Option where I: Iterator, { @@ -19,46 +19,6 @@ where let _ = input.next(); Some(Command::PasteBefore) } - Some('h') => { - let _ = input.next(); - Some(Command::MoveLeft) - } - Some('l') => { - let _ = input.next(); - Some(Command::MoveRight) - } - Some('j') => { - let _ = input.next(); - Some(Command::MoveDown) - } - Some('k') => { - let _ = input.next(); - Some(Command::MoveUp) - } - Some('w') => { - let _ = input.next(); - Some(Command::MoveWordRightStart) - } - Some('W') => { - let _ = input.next(); - Some(Command::MoveBigWordRightStart) - } - Some('e') => { - let _ = input.next(); - Some(Command::MoveWordRightEnd) - } - Some('E') => { - let _ = input.next(); - Some(Command::MoveBigWordRightEnd) - } - Some('b') => { - let _ = input.next(); - Some(Command::MoveWordLeft) - } - Some('B') => { - let _ = input.next(); - Some(Command::MoveBigWordLeft) - } Some('i') => { let _ = input.next(); Some(Command::EnterViInsert) @@ -67,14 +27,6 @@ where let _ = input.next(); Some(Command::EnterViAppend) } - Some('0' | '^') => { - let _ = input.next(); - Some(Command::MoveToLineStart) - } - Some('$') => { - let _ = input.next(); - Some(Command::MoveToLineEnd) - } Some('u') => { let _ = input.next(); Some(Command::Undo) @@ -89,8 +41,8 @@ where } Some('r') => { let _ = input.next(); - match input.peek() { - Some(c) => Some(Command::ReplaceChar(**c)), + match input.next() { + Some(c) => Some(Command::ReplaceChar(*c)), None => Some(Command::Incomplete), } } @@ -122,62 +74,14 @@ where let _ = input.next(); Some(Command::RewriteCurrentLine) } - Some('f') => { - let _ = input.next(); - match input.peek() { - Some(&c) => { - input.next(); - Some(Command::MoveRightUntil(*c)) - } - None => Some(Command::Incomplete), - } - } - Some('t') => { - let _ = input.next(); - match input.peek() { - Some(&c) => { - input.next(); - Some(Command::MoveRightBefore(*c)) - } - None => Some(Command::Incomplete), - } - } - Some('F') => { - let _ = input.next(); - match input.peek() { - Some(&c) => { - input.next(); - Some(Command::MoveLeftUntil(*c)) - } - None => Some(Command::Incomplete), - } - } - Some('T') => { - let _ = input.next(); - match input.peek() { - Some(&c) => { - input.next(); - Some(Command::MoveLeftBefore(*c)) - } - None => Some(Command::Incomplete), - } - } - Some(';') => { - let _ = input.next(); - vi.last_to_till - .as_ref() - .map(|to_till| Command::ReplayToTill(to_till.clone())) - } - Some(',') => { - let _ = input.next(); - vi.last_to_till - .as_ref() - .map(|to_till| Command::ReverseToTill(to_till.clone())) - } Some('~') => { let _ = input.next(); Some(Command::Switchcase) } + Some('.') => { + let _ = input.next(); + Some(Command::RepeatLastAction) + } _ => None, } } @@ -191,18 +95,6 @@ pub enum Command { SubstituteCharWithInsert, PasteAfter, PasteBefore, - MoveLeft, - MoveRight, - MoveUp, - MoveDown, - MoveWordRightStart, - MoveBigWordRightStart, - MoveWordRightEnd, - MoveBigWordRightEnd, - MoveWordLeft, - MoveBigWordLeft, - MoveToLineStart, - MoveToLineEnd, EnterViAppend, EnterViInsert, Undo, @@ -212,35 +104,26 @@ pub enum Command { PrependToStart, RewriteCurrentLine, Change, - MoveRightUntil(char), - MoveRightBefore(char), - MoveLeftUntil(char), - MoveLeftBefore(char), - ReplayToTill(ViToTill), - ReverseToTill(ViToTill), HistorySearch, Switchcase, + RepeatLastAction, } impl Command { - pub fn to_reedline(&self) -> Vec { + pub fn whole_line_char(&self) -> Option { + match self { + Command::Delete => Some('d'), + Command::Change => Some('c'), + _ => None, + } + } + + pub fn requires_motion(&self) -> bool { + matches!(self, Command::Delete | Command::Change) + } + + pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec { match self { - Self::MoveUp => vec![ReedlineOption::Event(ReedlineEvent::Up)], - Self::MoveDown => vec![ReedlineOption::Event(ReedlineEvent::Down)], - Self::MoveLeft => vec![ReedlineOption::Event(ReedlineEvent::Left)], - Self::MoveRight => vec![ReedlineOption::Event(ReedlineEvent::Right)], - Self::MoveToLineStart => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart)], - Self::MoveToLineEnd => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd)], - Self::MoveWordLeft => vec![ReedlineOption::Edit(EditCommand::MoveWordLeft)], - Self::MoveBigWordLeft => vec![ReedlineOption::Edit(EditCommand::MoveBigWordLeft)], - Self::MoveWordRightStart => vec![ReedlineOption::Edit(EditCommand::MoveWordRightStart)], - Self::MoveBigWordRightStart => { - vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightStart)] - } - Self::MoveWordRightEnd => vec![ReedlineOption::Edit(EditCommand::MoveWordRightEnd)], - Self::MoveBigWordRightEnd => { - vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightEnd)] - } Self::EnterViInsert => vec![ReedlineOption::Event(ReedlineEvent::Repaint)], Self::EnterViAppend => vec![ReedlineOption::Edit(EditCommand::MoveRight)], Self::PasteAfter => vec![ReedlineOption::Edit(EditCommand::PasteCutBufferAfter)], @@ -251,26 +134,6 @@ impl Command { Self::AppendToEnd => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd)], Self::PrependToStart => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart)], Self::RewriteCurrentLine => vec![ReedlineOption::Edit(EditCommand::CutCurrentLine)], - Self::MoveRightUntil(c) => vec![ - ReedlineOption::Event(ReedlineEvent::RecordToTill), - ReedlineOption::Edit(EditCommand::MoveRightUntil(*c)), - ], - Self::MoveRightBefore(c) => { - vec![ - ReedlineOption::Event(ReedlineEvent::RecordToTill), - ReedlineOption::Edit(EditCommand::MoveRightBefore(*c)), - ] - } - Self::MoveLeftUntil(c) => vec![ - ReedlineOption::Event(ReedlineEvent::RecordToTill), - ReedlineOption::Edit(EditCommand::MoveLeftUntil(*c)), - ], - Self::MoveLeftBefore(c) => vec![ - ReedlineOption::Event(ReedlineEvent::RecordToTill), - ReedlineOption::Edit(EditCommand::MoveLeftBefore(*c)), - ], - Self::ReplayToTill(to_till) => vec![ReedlineOption::Edit(to_till.into())], - Self::ReverseToTill(to_till) => vec![ReedlineOption::Edit(to_till.reverse().into())], Self::DeleteChar => vec![ReedlineOption::Edit(EditCommand::CutChar)], Self::ReplaceChar(c) => { vec![ReedlineOption::Edit(EditCommand::ReplaceChar(*c))] @@ -280,10 +143,18 @@ impl Command { Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)], // Mark a command as incomplete whenever a motion is required to finish the command Self::Delete | Self::Change | Self::Incomplete => vec![ReedlineOption::Incomplete], + Command::RepeatLastAction => match &vi_state.previous { + Some(event) => vec![ReedlineOption::Event(event.clone())], + None => vec![], + }, } } - pub fn to_reedline_with_motion(&self, motion: &Motion) -> Option> { + pub fn to_reedline_with_motion( + &self, + motion: &Motion, + vi_state: &mut Vi, + ) -> Option> { match self { Self::Delete => match motion { Motion::End => Some(vec![ReedlineOption::Edit(EditCommand::CutToLineEnd)]), @@ -303,18 +174,34 @@ impl Command { Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordLeft)]) } Motion::RightUntil(c) => { + vi_state.last_char_search = Some(ViCharSearch::ToRight(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutRightUntil(*c))]) } Motion::RightBefore(c) => { + vi_state.last_char_search = Some(ViCharSearch::TillRight(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutRightBefore(*c))]) } Motion::LeftUntil(c) => { + vi_state.last_char_search = Some(ViCharSearch::ToLeft(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutLeftUntil(*c))]) } Motion::LeftBefore(c) => { + vi_state.last_char_search = Some(ViCharSearch::TillLeft(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))]) } Motion::Start => Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]), + Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]), + Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]), + Motion::Up => None, + Motion::Down => None, + Motion::ReplayCharSearch => vi_state + .last_char_search + .as_ref() + .map(|char_search| vec![ReedlineOption::Edit(char_search.to_cut())]), + Motion::ReverseCharSearch => vi_state + .last_char_search + .as_ref() + .map(|char_search| vec![ReedlineOption::Edit(char_search.reverse().to_cut())]), }, Self::Change => { let op = match motion { @@ -342,20 +229,37 @@ impl Command { Some(vec![ReedlineOption::Edit(EditCommand::CutBigWordLeft)]) } Motion::RightUntil(c) => { + vi_state.last_char_search = Some(ViCharSearch::ToRight(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutRightUntil(*c))]) } Motion::RightBefore(c) => { + vi_state.last_char_search = Some(ViCharSearch::TillRight(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutRightBefore(*c))]) } Motion::LeftUntil(c) => { + vi_state.last_char_search = Some(ViCharSearch::ToLeft(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutLeftUntil(*c))]) } Motion::LeftBefore(c) => { + vi_state.last_char_search = Some(ViCharSearch::TillLeft(*c)); Some(vec![ReedlineOption::Edit(EditCommand::CutLeftBefore(*c))]) } Motion::Start => { Some(vec![ReedlineOption::Edit(EditCommand::CutFromLineStart)]) } + Motion::Left => Some(vec![ReedlineOption::Edit(EditCommand::Backspace)]), + Motion::Right => Some(vec![ReedlineOption::Edit(EditCommand::Delete)]), + Motion::Up => None, + Motion::Down => None, + Motion::ReplayCharSearch => vi_state + .last_char_search + .as_ref() + .map(|char_search| vec![ReedlineOption::Edit(char_search.to_cut())]), + Motion::ReverseCharSearch => { + vi_state.last_char_search.as_ref().map(|char_search| { + vec![ReedlineOption::Edit(char_search.reverse().to_cut())] + }) + } }; // Semihack: Append `Repaint` to ensure the mode change gets displayed op.map(|mut vec| { @@ -367,57 +271,3 @@ impl Command { } } } - -impl From for EditCommand { - fn from(val: ViToTill) -> Self { - EditCommand::from(&val) - } -} - -impl From<&ViToTill> for EditCommand { - fn from(val: &ViToTill) -> Self { - match val { - ViToTill::TillLeft(c) => EditCommand::MoveLeftBefore(*c), - ViToTill::ToLeft(c) => EditCommand::MoveLeftUntil(*c), - ViToTill::TillRight(c) => EditCommand::MoveRightBefore(*c), - ViToTill::ToRight(c) => EditCommand::MoveRightUntil(*c), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use rstest::rstest; - - #[rstest] - #[case(';', None, None)] - #[case(',', None, None)] - #[case( - ';', - Some(ViToTill::ToRight('X')), - Some(Command::ReplayToTill(ViToTill::ToRight('X'))) - )] - #[case( - ',', - Some(ViToTill::ToRight('X')), - Some(Command::ReverseToTill(ViToTill::ToRight('X'))) - )] - fn repeat_to_till( - #[case] input: char, - #[case] last_to_till: Option, - #[case] expected: Option, - ) { - let vi = Vi { - last_to_till, - ..Vi::default() - }; - - let input = vec![input]; - - let result = parse_command(&vi, &mut input.iter().peekable()); - - assert_eq!(result, expected); - } -} diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index b162196..88beff7 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -6,7 +6,7 @@ mod vi_keybindings; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; pub use vi_keybindings::{default_vi_insert_keybindings, default_vi_normal_keybindings}; -use self::motion::ViToTill; +use self::motion::ViCharSearch; use super::EditMode; use crate::{ @@ -29,7 +29,7 @@ pub struct Vi { mode: ViMode, previous: Option, // last f, F, t, T motion for ; and , - last_to_till: Option, + last_char_search: Option, } impl Default for Vi { @@ -40,7 +40,7 @@ impl Default for Vi { cache: Vec::new(), mode: ViMode::Insert, previous: None, - last_to_till: None, + last_char_search: None, } } } @@ -61,14 +61,6 @@ impl EditMode for Vi { match event { Event::Key(KeyEvent { code, modifiers }) => match (self.mode, modifiers, code) { (ViMode::Normal, modifier, KeyCode::Char(c)) => { - // The repeat character is the only character that is not managed - // by the parser since the last event is stored in the editor - if c == '.' { - if let Some(event) = &self.previous { - return event.clone(); - } - } - let c = c.to_ascii_lowercase(); if let Some(event) = self @@ -83,45 +75,22 @@ impl EditMode for Vi { c }); - let res = parse(self, &mut self.cache.iter().peekable()); + let res = parse(&mut self.cache.iter().peekable()); - if res.enter_insert_mode() { - self.mode = ViMode::Insert; + if !res.is_valid() { + self.cache.clear(); + ReedlineEvent::None + } else if res.is_complete() { + if res.enters_insert_mode() { + self.mode = ViMode::Insert; + } + + let event = res.to_reedline_event(self); + self.cache.clear(); + event + } else { + ReedlineEvent::None } - - let event = res.to_reedline_event(); - match event { - ReedlineEvent::None => { - if !res.is_valid() { - self.cache.clear(); - } - } - _ => { - self.cache.clear(); - } - }; - - // to_reedline_event() returned Multiple or None when this was written - if let ReedlineEvent::Multiple(ref events) = event { - let last_to_till = - if events.len() == 2 && events[0] == ReedlineEvent::RecordToTill { - if let ReedlineEvent::Edit(edit) = &events[1] { - edit[0].clone().into() - } else { - None - } - } else { - None - }; - - if last_to_till.is_some() { - self.last_to_till = last_to_till; - } - } - - self.previous = Some(event.clone()); - - event } else { ReedlineEvent::None } @@ -195,7 +164,6 @@ impl EditMode for Vi { mod test { use super::*; use pretty_assertions::assert_eq; - use rstest::rstest; #[test] fn esc_leads_to_normal_mode_test() { @@ -281,34 +249,4 @@ mod test { assert_eq!(result, ReedlineEvent::None); } - - #[rstest] - #[case('f', KeyModifiers::NONE, ViToTill::ToRight('X'))] - #[case('f', KeyModifiers::SHIFT, ViToTill::ToLeft('X'))] - #[case('t', KeyModifiers::NONE, ViToTill::TillRight('X'))] - #[case('t', KeyModifiers::SHIFT, ViToTill::TillLeft('X'))] - fn last_to_till( - #[case] code: char, - #[case] modifiers: KeyModifiers, - #[case] expected: ViToTill, - ) { - let mut vi = Vi { - mode: ViMode::Normal, - ..Vi::default() - }; - - let to_till = Event::Key(KeyEvent { - code: KeyCode::Char(code), - modifiers, - }); - vi.parse_event(to_till); - - let key_x = Event::Key(KeyEvent { - code: KeyCode::Char('x'), - modifiers: KeyModifiers::SHIFT, - }); - vi.parse_event(key_x); - - assert_eq!(vi.last_to_till, Some(expected)); - } } diff --git a/src/edit_mode/vi/motion.rs b/src/edit_mode/vi/motion.rs index 2482bbd..09ee9b2 100644 --- a/src/edit_mode/vi/motion.rs +++ b/src/edit_mode/vi/motion.rs @@ -1,56 +1,73 @@ use std::iter::Peekable; -use crate::EditCommand; +use crate::{EditCommand, ReedlineEvent, Vi}; -pub fn parse_motion<'iter, I>(input: &mut Peekable) -> Option +use super::parser::{ParseResult, ReedlineOption}; + +pub fn parse_motion<'iter, I>( + input: &mut Peekable, + command_char: Option, +) -> ParseResult where I: Iterator, { match input.peek() { + Some('h') => { + let _ = input.next(); + ParseResult::Valid(Motion::Left) + } + Some('l') => { + let _ = input.next(); + ParseResult::Valid(Motion::Right) + } + Some('j') => { + let _ = input.next(); + ParseResult::Valid(Motion::Down) + } + Some('k') => { + let _ = input.next(); + ParseResult::Valid(Motion::Up) + } Some('b') => { let _ = input.next(); - Some(Motion::PreviousWord) + ParseResult::Valid(Motion::PreviousWord) } Some('B') => { let _ = input.next(); - Some(Motion::PreviousBigWord) + ParseResult::Valid(Motion::PreviousBigWord) } Some('w') => { let _ = input.next(); - Some(Motion::NextWord) + ParseResult::Valid(Motion::NextWord) } Some('W') => { let _ = input.next(); - Some(Motion::NextBigWord) + ParseResult::Valid(Motion::NextBigWord) } Some('e') => { let _ = input.next(); - Some(Motion::NextWordEnd) + ParseResult::Valid(Motion::NextWordEnd) } Some('E') => { let _ = input.next(); - Some(Motion::NextBigWordEnd) - } - Some('d') => { - let _ = input.next(); - Some(Motion::Line) + ParseResult::Valid(Motion::NextBigWordEnd) } Some('0' | '^') => { let _ = input.next(); - Some(Motion::Start) + ParseResult::Valid(Motion::Start) } Some('$') => { let _ = input.next(); - Some(Motion::End) + ParseResult::Valid(Motion::End) } Some('f') => { let _ = input.next(); match input.peek() { Some(&x) => { input.next(); - Some(Motion::RightUntil(*x)) + ParseResult::Valid(Motion::RightUntil(*x)) } - None => None, + None => ParseResult::Incomplete, } } Some('t') => { @@ -58,9 +75,9 @@ where match input.peek() { Some(&x) => { input.next(); - Some(Motion::RightBefore(*x)) + ParseResult::Valid(Motion::RightBefore(*x)) } - None => None, + None => ParseResult::Incomplete, } } Some('F') => { @@ -68,9 +85,9 @@ where match input.peek() { Some(&x) => { input.next(); - Some(Motion::LeftUntil(*x)) + ParseResult::Valid(Motion::LeftUntil(*x)) } - None => None, + None => ParseResult::Incomplete, } } Some('T') => { @@ -78,17 +95,34 @@ where match input.peek() { Some(&x) => { input.next(); - Some(Motion::LeftBefore(*x)) + ParseResult::Valid(Motion::LeftBefore(*x)) } - None => None, + None => ParseResult::Incomplete, } } - _ => None, + Some(';') => { + let _ = input.next(); + ParseResult::Valid(Motion::ReplayCharSearch) + } + Some(',') => { + let _ = input.next(); + ParseResult::Valid(Motion::ReverseCharSearch) + } + ch if ch == command_char.as_ref().as_ref() && command_char.is_some() => { + let _ = input.next(); + ParseResult::Valid(Motion::Line) + } + None => ParseResult::Incomplete, + _ => ParseResult::Invalid, } } #[derive(Debug, PartialEq, Eq)] pub enum Motion { + Left, + Right, + Up, + Down, NextWord, NextBigWord, NextWordEnd, @@ -102,11 +136,63 @@ pub enum Motion { RightBefore(char), LeftUntil(char), LeftBefore(char), + ReplayCharSearch, + ReverseCharSearch, +} + +impl Motion { + pub fn to_reedline(&self, vi_state: &mut Vi) -> Vec { + match self { + Motion::Left => vec![ReedlineOption::Event(ReedlineEvent::Left)], + Motion::Right => vec![ReedlineOption::Event(ReedlineEvent::Right)], + Motion::Up => vec![ReedlineOption::Event(ReedlineEvent::Up)], + Motion::Down => vec![ReedlineOption::Event(ReedlineEvent::Down)], + Motion::NextWord => vec![ReedlineOption::Edit(EditCommand::MoveWordRightStart)], + Motion::NextBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightStart)], + Motion::NextWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveWordRightEnd)], + Motion::NextBigWordEnd => vec![ReedlineOption::Edit(EditCommand::MoveBigWordRightEnd)], + Motion::PreviousWord => vec![ReedlineOption::Edit(EditCommand::MoveWordLeft)], + Motion::PreviousBigWord => vec![ReedlineOption::Edit(EditCommand::MoveBigWordLeft)], + Motion::Line => vec![], // Placeholder as unusable standalone motion + Motion::Start => vec![ReedlineOption::Edit(EditCommand::MoveToLineStart)], + Motion::End => vec![ReedlineOption::Edit(EditCommand::MoveToLineEnd)], + Motion::RightUntil(ch) => { + vi_state.last_char_search = Some(ViCharSearch::ToRight(*ch)); + vec![ReedlineOption::Edit(EditCommand::MoveRightUntil(*ch))] + } + Motion::RightBefore(ch) => { + vi_state.last_char_search = Some(ViCharSearch::TillRight(*ch)); + vec![ReedlineOption::Edit(EditCommand::MoveRightBefore(*ch))] + } + Motion::LeftUntil(ch) => { + vi_state.last_char_search = Some(ViCharSearch::ToLeft(*ch)); + vec![ReedlineOption::Edit(EditCommand::MoveLeftUntil(*ch))] + } + Motion::LeftBefore(ch) => { + vi_state.last_char_search = Some(ViCharSearch::TillLeft(*ch)); + vec![ReedlineOption::Edit(EditCommand::MoveLeftBefore(*ch))] + } + Motion::ReplayCharSearch => { + if let Some(char_search) = vi_state.last_char_search.as_ref() { + vec![ReedlineOption::Edit(char_search.to_move())] + } else { + vec![] + } + } + Motion::ReverseCharSearch => { + if let Some(char_search) = vi_state.last_char_search.as_ref() { + vec![ReedlineOption::Edit(char_search.reverse().to_move())] + } else { + vec![] + } + } + } + } } /// Vi left-right motions to or till a character. #[derive(Debug, PartialEq, Eq, Clone)] -pub enum ViToTill { +pub enum ViCharSearch { /// f ToRight(char), /// F @@ -117,26 +203,32 @@ pub enum ViToTill { TillLeft(char), } -impl ViToTill { +impl ViCharSearch { /// Swap the direction of the to or till for ',' pub fn reverse(&self) -> Self { match self { - ViToTill::ToRight(c) => ViToTill::ToLeft(*c), - ViToTill::ToLeft(c) => ViToTill::ToRight(*c), - ViToTill::TillRight(c) => ViToTill::TillLeft(*c), - ViToTill::TillLeft(c) => ViToTill::TillRight(*c), + ViCharSearch::ToRight(c) => ViCharSearch::ToLeft(*c), + ViCharSearch::ToLeft(c) => ViCharSearch::ToRight(*c), + ViCharSearch::TillRight(c) => ViCharSearch::TillLeft(*c), + ViCharSearch::TillLeft(c) => ViCharSearch::TillRight(*c), } } -} -impl From for Option { - fn from(edit: EditCommand) -> Self { - match edit { - EditCommand::MoveLeftBefore(c) => Some(ViToTill::TillLeft(c)), - EditCommand::MoveLeftUntil(c) => Some(ViToTill::ToLeft(c)), - EditCommand::MoveRightBefore(c) => Some(ViToTill::TillRight(c)), - EditCommand::MoveRightUntil(c) => Some(ViToTill::ToRight(c)), - _ => None, + pub fn to_move(&self) -> EditCommand { + match self { + ViCharSearch::ToRight(c) => EditCommand::MoveRightUntil(*c), + ViCharSearch::ToLeft(c) => EditCommand::MoveLeftUntil(*c), + ViCharSearch::TillRight(c) => EditCommand::MoveRightBefore(*c), + ViCharSearch::TillLeft(c) => EditCommand::MoveLeftBefore(*c), + } + } + + pub fn to_cut(&self) -> EditCommand { + match self { + ViCharSearch::ToRight(c) => EditCommand::CutRightUntil(*c), + ViCharSearch::ToLeft(c) => EditCommand::CutLeftUntil(*c), + ViCharSearch::TillRight(c) => EditCommand::CutRightBefore(*c), + ViCharSearch::TillLeft(c) => EditCommand::CutLeftBefore(*c), } } } diff --git a/src/edit_mode/vi/parser.rs b/src/edit_mode/vi/parser.rs index c7e7b11..d533dca 100644 --- a/src/edit_mode/vi/parser.rs +++ b/src/edit_mode/vi/parser.rs @@ -21,17 +21,44 @@ impl ReedlineOption { } #[derive(Debug, PartialEq, Eq)] -pub struct ParseResult { +pub enum ParseResult { + Valid(T), + Incomplete, + Invalid, +} + +impl ParseResult { + fn is_invalid(&self) -> bool { + match self { + ParseResult::Valid(_) => false, + ParseResult::Incomplete => false, + ParseResult::Invalid => true, + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParsedViSequence { multiplier: Option, command: Option, count: Option, - motion: Option, - valid: bool, + motion: ParseResult, } -impl ParseResult { +impl ParsedViSequence { pub fn is_valid(&self) -> bool { - self.valid + !self.motion.is_invalid() + } + + pub fn is_complete(&self) -> bool { + match (&self.command, &self.motion) { + (None, ParseResult::Valid(_)) => true, + (Some(Command::Incomplete), _) => false, + (Some(cmd), ParseResult::Incomplete) if !cmd.requires_motion() => true, + (Some(_), ParseResult::Valid(_)) => true, + (Some(cmd), ParseResult::Incomplete) if cmd.requires_motion() => false, + _ => false, + } } /// Combine `multiplier` and `count` as vim only considers the product @@ -64,27 +91,46 @@ impl ParseResult { } } - pub fn enter_insert_mode(&self) -> bool { + pub fn enters_insert_mode(&self) -> bool { matches!( (&self.command, &self.motion), - (Some(Command::EnterViInsert), None) - | (Some(Command::EnterViAppend), None) - | (Some(Command::ChangeToLineEnd), None) - | (Some(Command::AppendToEnd), None) - | (Some(Command::PrependToStart), None) - | (Some(Command::RewriteCurrentLine), None) - | (Some(Command::SubstituteCharWithInsert), None) - | (Some(Command::HistorySearch), None) - | (Some(Command::Change), Some(_)) + (Some(Command::EnterViInsert), ParseResult::Incomplete) + | (Some(Command::EnterViAppend), ParseResult::Incomplete) + | (Some(Command::ChangeToLineEnd), ParseResult::Incomplete) + | (Some(Command::AppendToEnd), ParseResult::Incomplete) + | (Some(Command::PrependToStart), ParseResult::Incomplete) + | (Some(Command::RewriteCurrentLine), ParseResult::Incomplete) + | ( + Some(Command::SubstituteCharWithInsert), + ParseResult::Incomplete + ) + | (Some(Command::HistorySearch), ParseResult::Incomplete) + | (Some(Command::Change), ParseResult::Valid(_)) ) } - pub fn to_reedline_event(&self) -> ReedlineEvent { + pub fn to_reedline_event(&self, vi_state: &mut Vi) -> ReedlineEvent { match (&self.multiplier, &self.command, &self.count, &self.motion) { - (_, Some(command), None, None) => self.apply_multiplier(Some(command.to_reedline())), + (_, Some(command), None, ParseResult::Incomplete) => { + let events = self.apply_multiplier(Some(command.to_reedline(vi_state))); + match &events { + ReedlineEvent::None => {} + event => vi_state.previous = Some(event.clone()), + } + events + } // This case handles all combinations of commands and motions that could exist - (_, Some(command), _, Some(motion)) => { - self.apply_multiplier(command.to_reedline_with_motion(motion)) + (_, Some(command), _, ParseResult::Valid(motion)) => { + let events = + self.apply_multiplier(command.to_reedline_with_motion(motion, vi_state)); + match &events { + ReedlineEvent::None => {} + event => vi_state.previous = Some(event.clone()), + } + events + } + (_, None, _, ParseResult::Valid(motion)) => { + self.apply_multiplier(Some(motion.to_reedline(vi_state))) } _ => ReedlineEvent::None, } @@ -115,31 +161,20 @@ where } } -pub fn parse<'iter, I>(vi: &Vi, input: &mut Peekable) -> ParseResult +pub fn parse<'iter, I>(input: &mut Peekable) -> ParsedViSequence where I: Iterator, { let multiplier = parse_number(input); - let command = parse_command(vi, input); + let command = parse_command(input); let count = parse_number(input); - let motion = parse_motion(input); + let motion = parse_motion(input, command.as_ref().and_then(Command::whole_line_char)); - let valid = - { multiplier.is_some() || command.is_some() || count.is_some() || motion.is_some() }; - - // If after parsing all the input characters there is a remainder, - // then there is garbage in the input. Having unrecognized characters will get - // the user stuck in normal mode until the cache is clear, specially with - // commands that could be incomplete until a motion is introduced (e.g. delete or change) - // Better mark it as invalid for the cache to be cleared - let has_garbage = input.next().is_some(); - - ParseResult { + ParsedViSequence { multiplier, command, count, motion, - valid: valid && !has_garbage, } } @@ -149,9 +184,8 @@ mod tests { use pretty_assertions::assert_eq; use rstest::rstest; - fn vi_parse(input: &[char]) -> ParseResult { - let vi = Vi::default(); - parse(&vi, &mut input.iter().peekable()) + fn vi_parse(input: &[char]) -> ParsedViSequence { + parse(&mut input.iter().peekable()) } #[test] @@ -161,14 +195,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: None, command: Some(Command::Delete), count: None, - motion: Some(Motion::NextWord), - valid: true + motion: ParseResult::Valid(Motion::NextWord), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -178,14 +213,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), command: Some(Command::Delete), count: None, - motion: Some(Motion::NextWord), - valid: true + motion: ParseResult::Valid(Motion::NextWord), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -195,14 +231,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), command: Some(Command::Delete), count: Some(2), - motion: Some(Motion::NextWord), - valid: true + motion: ParseResult::Valid(Motion::NextWord), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -212,14 +249,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), command: Some(Command::Delete), count: Some(20), - motion: Some(Motion::NextWord), - valid: true + motion: ParseResult::Valid(Motion::NextWord), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -229,14 +267,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), command: Some(Command::Delete), count: None, - motion: Some(Motion::Line), - valid: true + motion: ParseResult::Valid(Motion::Line), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -246,14 +285,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: None, command: Some(Command::Delete), count: None, - motion: Some(Motion::RightBefore('d')), - valid: true + motion: ParseResult::Valid(Motion::RightBefore('d')), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -263,14 +303,70 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), command: Some(Command::Delete), count: None, - motion: None, - valid: false + motion: ParseResult::Invalid, } ); + assert_eq!(output.is_valid(), false); + } + + #[test] + fn test_partial_action() { + let input = ['r']; + let output = vi_parse(&input); + + assert_eq!( + output, + ParsedViSequence { + multiplier: None, + command: Some(Command::Incomplete), + count: None, + motion: ParseResult::Incomplete, + } + ); + + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), false); + } + + #[test] + fn test_partial_motion() { + let input = ['f']; + let output = vi_parse(&input); + + assert_eq!( + output, + ParsedViSequence { + multiplier: None, + command: None, + count: None, + motion: ParseResult::Incomplete, + } + ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), false); + } + + #[test] + fn test_two_char_action_replace() { + let input = ['r', 'k']; + let output = vi_parse(&input); + + assert_eq!( + output, + ParsedViSequence { + multiplier: None, + command: Some(Command::ReplaceChar('k')), + count: None, + motion: ParseResult::Incomplete, + } + ); + + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -280,14 +376,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), - command: Some(Command::MoveRightUntil('f')), + command: None, count: None, - motion: None, - valid: true + motion: ParseResult::Valid(Motion::RightUntil('f')), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[test] @@ -297,14 +394,15 @@ mod tests { assert_eq!( output, - ParseResult { + ParsedViSequence { multiplier: Some(2), - command: Some(Command::MoveUp), + command: None, count: None, - motion: None, - valid: true + motion: ParseResult::Valid(Motion::Up), } ); + assert_eq!(output.is_valid(), true); + assert_eq!(output.is_complete(), true); } #[rstest] @@ -347,8 +445,9 @@ mod tests { #[case(&['d', 'b'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutWordLeft])]))] #[case(&['d', 'B'], ReedlineEvent::Multiple(vec![ReedlineEvent::Edit(vec![EditCommand::CutBigWordLeft])]))] fn test_reedline_move(#[case] input: &[char], #[case] expected: ReedlineEvent) { + let mut vi = Vi::default(); let res = vi_parse(input); - let output = res.to_reedline_event(); + let output = res.to_reedline_event(&mut vi); assert_eq!(output, expected); } diff --git a/src/engine.rs b/src/engine.rs index c207d92..7c7fea9 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -692,8 +692,7 @@ impl Reedline { | ReedlineEvent::MenuLeft | ReedlineEvent::MenuRight | ReedlineEvent::MenuPageNext - | ReedlineEvent::MenuPagePrevious - | ReedlineEvent::RecordToTill => Ok(EventStatus::Inapplicable), + | ReedlineEvent::MenuPagePrevious => Ok(EventStatus::Inapplicable), } } @@ -990,9 +989,7 @@ impl Reedline { // Exhausting the event handlers is still considered handled Ok(EventStatus::Inapplicable) } - ReedlineEvent::None | ReedlineEvent::Mouse | ReedlineEvent::RecordToTill => { - Ok(EventStatus::Inapplicable) - } + ReedlineEvent::None | ReedlineEvent::Mouse => Ok(EventStatus::Inapplicable), } } diff --git a/src/enums.rs b/src/enums.rs index 15f4834..eed9f63 100644 --- a/src/enums.rs +++ b/src/enums.rs @@ -497,9 +497,6 @@ pub enum ReedlineEvent { /// Open text editor OpenEditor, - - /// Record vi to or till motion - RecordToTill, } impl Display for ReedlineEvent { @@ -541,7 +538,6 @@ impl Display for ReedlineEvent { ReedlineEvent::MenuPagePrevious => write!(f, "MenuPagePrevious"), ReedlineEvent::ExecuteHostCommand(_) => write!(f, "ExecuteHostCommand"), ReedlineEvent::OpenEditor => write!(f, "OpenEditor"), - ReedlineEvent::RecordToTill => write!(f, "RecordToTill"), } } }