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`
This commit is contained in:
sholderbach 2022-09-25 02:27:46 +02:00 committed by Stefan Holderbach
parent 07a9a7463f
commit 3e92f97da2
6 changed files with 379 additions and 407 deletions

View File

@ -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<I>) -> Option<Command>
pub fn parse_command<'iter, I>(input: &mut Peekable<I>) -> Option<Command>
where
I: Iterator<Item = &'iter char>,
{
@ -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<ReedlineOption> {
pub fn whole_line_char(&self) -> Option<char> {
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<ReedlineOption> {
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<Vec<ReedlineOption>> {
pub fn to_reedline_with_motion(
&self,
motion: &Motion,
vi_state: &mut Vi,
) -> Option<Vec<ReedlineOption>> {
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<ViToTill> 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<ViToTill>,
#[case] expected: Option<Command>,
) {
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);
}
}

View File

@ -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<ReedlineEvent>,
// last f, F, t, T motion for ; and ,
last_to_till: Option<ViToTill>,
last_char_search: Option<ViCharSearch>,
}
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));
}
}

View File

@ -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<I>) -> Option<Motion>
use super::parser::{ParseResult, ReedlineOption};
pub fn parse_motion<'iter, I>(
input: &mut Peekable<I>,
command_char: Option<char>,
) -> ParseResult<Motion>
where
I: Iterator<Item = &'iter char>,
{
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<ReedlineOption> {
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<EditCommand> for Option<ViToTill> {
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),
}
}
}

View File

@ -21,17 +21,44 @@ impl ReedlineOption {
}
#[derive(Debug, PartialEq, Eq)]
pub struct ParseResult {
pub enum ParseResult<T> {
Valid(T),
Incomplete,
Invalid,
}
impl<T> ParseResult<T> {
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<usize>,
command: Option<Command>,
count: Option<usize>,
motion: Option<Motion>,
valid: bool,
motion: ParseResult<Motion>,
}
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<I>) -> ParseResult
pub fn parse<'iter, I>(input: &mut Peekable<I>) -> ParsedViSequence
where
I: Iterator<Item = &'iter char>,
{
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);
}

View File

@ -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),
}
}

View File

@ -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"),
}
}
}