mirror of
https://github.com/nushell/reedline.git
synced 2024-08-17 11:50:42 +03:00
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:
parent
07a9a7463f
commit
3e92f97da2
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user