use std::num::NonZeroUsize;
use super::{
InsertEntry, PastePlacement, TextObject, TextObjectKind, TextObjectScope,
key::KeyToken,
motion::{
CharSearch, CharSearchDirection, CharSearchPlacement, ColumnMotion, LineAddress, Motion,
PageDirection, ParagraphDirection, WordEndMotion, WordKind,
},
search::{SearchDirection, SearchRepeatDirection},
};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Count(NonZeroUsize);
impl Count {
#[must_use]
pub const fn new(value: NonZeroUsize) -> Self {
Self(value)
}
#[must_use]
pub const fn get(self) -> usize {
self.0.get()
}
}
impl Default for Count {
fn default() -> Self {
Self(NonZeroUsize::MIN)
}
}
impl From<NonZeroUsize> for Count {
fn from(value: NonZeroUsize) -> Self {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Counted<T> {
pub count: Count,
pub item: T,
}
impl<T> Counted<T> {
#[must_use]
pub fn once(item: T) -> Self {
Self {
count: Count::default(),
item,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NormalCommand {
Motion(Counted<Motion>),
Operator {
count: Count,
operator: Operator,
target: NormalOperatorTarget,
},
ModeSwitch(ModeSwitch),
Insert(InsertEntry),
Paste {
count: Count,
placement: PastePlacement,
},
Undo,
Redo,
RepeatLastChange,
ExCommandStart,
SearchStart(SearchDirection),
SearchRepeat(SearchRepeatDirection),
ViewportPosition(ViewportPosition),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Operator {
Delete,
Yank,
Change,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NormalOperatorTarget {
Motion(Counted<Motion>),
CurrentLine,
TextObject(Counted<TextObject>),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ModeSwitch {
VisualCharacterwise,
VisualLinewise,
VisualBlockwise,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ViewportPosition {
Top,
Center,
Bottom,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NormalGrammarOutput {
Pending,
Command(NormalCommand),
Unmatched,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PendingPrefix {
G,
Z,
CharSearch {
direction: CharSearchDirection,
placement: CharSearchPlacement,
},
Operator {
count: Count,
operator: Operator,
},
OperatorG {
count: Count,
operator: Operator,
},
OperatorCharSearch {
count: Count,
operator: Operator,
direction: CharSearchDirection,
placement: CharSearchPlacement,
},
OperatorTextObject {
count: Count,
operator: Operator,
scope: TextObjectScope,
},
}
#[derive(Clone, Debug, Default)]
pub struct NormalGrammar {
count: Option<NonZeroUsize>,
pending: Option<PendingPrefix>,
}
impl NormalGrammar {
pub const fn reset(&mut self) {
self.count = None;
self.pending = None;
}
pub fn feed(&mut self, token: KeyToken) -> NormalGrammarOutput {
if let Some(pending) = self.pending {
return self.finish_pending(pending, token);
}
if let Some(digit) = token.count_digit()
&& (digit != 0 || self.count.is_some())
{
self.push_count_digit(digit);
return NormalGrammarOutput::Pending;
}
self.feed_ready_token(token)
}
fn feed_ready_token(&mut self, token: KeyToken) -> NormalGrammarOutput {
match token {
KeyToken::Char('h') => self.motion(Motion::Left),
KeyToken::Char('j') => self.motion(Motion::Down),
KeyToken::Char('k') => self.motion(Motion::Up),
KeyToken::Char('l') => self.motion(Motion::Right),
KeyToken::Ctrl('f' | 'F') => self.motion(Motion::Page(PageDirection::Forward)),
KeyToken::Ctrl('b' | 'B') => self.motion(Motion::Page(PageDirection::Backward)),
KeyToken::Char('w') => self.motion(Motion::WordForward(WordKind::Normal)),
KeyToken::Char('W') => self.motion(Motion::WordForward(WordKind::Big)),
KeyToken::Char('b') => self.motion(Motion::WordBackward(WordKind::Normal)),
KeyToken::Char('B') => self.motion(Motion::WordBackward(WordKind::Big)),
KeyToken::Char('e') => {
self.motion(Motion::WordEnd(WordEndMotion::Forward(WordKind::Normal)))
}
KeyToken::Char('E') => {
self.motion(Motion::WordEnd(WordEndMotion::Forward(WordKind::Big)))
}
KeyToken::Char('0') => self.motion(Motion::Column(ColumnMotion::LineStart)),
KeyToken::Char('^') => self.motion(Motion::Column(ColumnMotion::FirstNonBlank)),
KeyToken::Char('$') => self.motion(Motion::Column(ColumnMotion::LineEnd)),
KeyToken::Char('|') => self.motion(Motion::Column(ColumnMotion::ScreenColumn)),
KeyToken::Char('G') => {
let address = self
.take_count()
.map_or(LineAddress::LastNonBlank, |count| {
LineAddress::Number(count.0)
});
NormalGrammarOutput::Command(NormalCommand::Motion(Counted {
count: Count::default(),
item: Motion::LineAddress(address),
}))
}
KeyToken::Char('g') => {
self.pending = Some(PendingPrefix::G);
NormalGrammarOutput::Pending
}
KeyToken::Char('z') => {
self.pending = Some(PendingPrefix::Z);
NormalGrammarOutput::Pending
}
KeyToken::Char('f') => {
self.pending_char_search(CharSearchDirection::Forward, CharSearchPlacement::OnMatch)
}
KeyToken::Char('F') => self
.pending_char_search(CharSearchDirection::Backward, CharSearchPlacement::OnMatch),
KeyToken::Char('t') => self.pending_char_search(
CharSearchDirection::Forward,
CharSearchPlacement::BeforeMatch,
),
KeyToken::Char('T') => self.pending_char_search(
CharSearchDirection::Backward,
CharSearchPlacement::BeforeMatch,
),
KeyToken::Char(';') => self.motion(Motion::RepeatCharSearch),
KeyToken::Char(',') => self.motion(Motion::RepeatCharSearchReversed),
KeyToken::Char('}') => self.motion(Motion::Paragraph(ParagraphDirection::Forward)),
KeyToken::Char('{') => self.motion(Motion::Paragraph(ParagraphDirection::Backward)),
KeyToken::Char(':') => self.command(NormalCommand::ExCommandStart),
KeyToken::Char('/') => {
self.command(NormalCommand::SearchStart(SearchDirection::Forward))
}
KeyToken::Char('?') => {
self.command(NormalCommand::SearchStart(SearchDirection::Backward))
}
KeyToken::Char('n') => {
self.command(NormalCommand::SearchRepeat(SearchRepeatDirection::Next))
}
KeyToken::Char('N') => {
self.command(NormalCommand::SearchRepeat(SearchRepeatDirection::Previous))
}
KeyToken::Char('i') => self.insert(InsertEntry::AtCursor),
KeyToken::Char('a') => self.insert(InsertEntry::AfterCursor),
KeyToken::Char('I') => self.insert(InsertEntry::FirstNonBlank),
KeyToken::Char('A') => self.insert(InsertEntry::LineEnd),
KeyToken::Char('o') => self.insert(InsertEntry::OpenLineBelow),
KeyToken::Char('O') => self.insert(InsertEntry::OpenLineAbove),
KeyToken::Char('p') => self.paste(PastePlacement::After),
KeyToken::Char('P') => self.paste(PastePlacement::Before),
KeyToken::Char('u') => self.command(NormalCommand::Undo),
KeyToken::Ctrl('r' | 'R') => self.command(NormalCommand::Redo),
KeyToken::Char('.') => self.command(NormalCommand::RepeatLastChange),
KeyToken::Char('v') => {
self.command(NormalCommand::ModeSwitch(ModeSwitch::VisualCharacterwise))
}
KeyToken::Char('V') => {
self.command(NormalCommand::ModeSwitch(ModeSwitch::VisualLinewise))
}
KeyToken::Ctrl('v' | 'V') => {
self.command(NormalCommand::ModeSwitch(ModeSwitch::VisualBlockwise))
}
KeyToken::Char('d') => self.pending_operator(Operator::Delete),
KeyToken::Char('y') => self.pending_operator(Operator::Yank),
KeyToken::Char('c') => self.pending_operator(Operator::Change),
_ => {
self.reset();
NormalGrammarOutput::Unmatched
}
}
}
fn finish_pending(&mut self, pending: PendingPrefix, token: KeyToken) -> NormalGrammarOutput {
self.pending = None;
match pending {
PendingPrefix::G if token == KeyToken::Char('g') => {
let address = self
.take_count()
.map_or(LineAddress::FirstNonBlank, |count| {
LineAddress::Number(count.0)
});
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
Motion::LineAddress(address),
)))
}
PendingPrefix::G if token == KeyToken::Char('e') => {
self.command(NormalCommand::Motion(Counted::once(Motion::WordEnd(
WordEndMotion::Backward(WordKind::Normal),
))))
}
PendingPrefix::G if token == KeyToken::Char('E') => {
self.command(NormalCommand::Motion(Counted::once(Motion::WordEnd(
WordEndMotion::Backward(WordKind::Big),
))))
}
PendingPrefix::CharSearch {
direction,
placement,
} => {
if let KeyToken::Char(target) = token {
let count = self.take_count().unwrap_or_default();
self.command(NormalCommand::Motion(Counted {
count,
item: Motion::CharSearch(CharSearch {
target,
direction,
placement,
}),
}))
} else {
self.reset();
NormalGrammarOutput::Unmatched
}
}
PendingPrefix::Operator { count, operator } => {
self.finish_pending_operator(count, operator, token)
}
PendingPrefix::OperatorG { count, operator } => {
self.finish_pending_operator_g(count, operator, token)
}
PendingPrefix::OperatorCharSearch {
count,
operator,
direction,
placement,
} => {
if let KeyToken::Char(target) = token {
let motion = Counted {
count: self.take_count().unwrap_or_default(),
item: Motion::CharSearch(CharSearch {
target,
direction,
placement,
}),
};
self.operator_command(count, operator, NormalOperatorTarget::Motion(motion))
} else {
self.reset();
NormalGrammarOutput::Unmatched
}
}
PendingPrefix::OperatorTextObject {
count,
operator,
scope,
} => self.finish_pending_operator_text_object(count, operator, scope, token),
PendingPrefix::G => {
self.reset();
NormalGrammarOutput::Unmatched
}
PendingPrefix::Z => match token {
KeyToken::Char('t') => {
self.command(NormalCommand::ViewportPosition(ViewportPosition::Top))
}
KeyToken::Char('z') => {
self.command(NormalCommand::ViewportPosition(ViewportPosition::Center))
}
KeyToken::Char('b') => {
self.command(NormalCommand::ViewportPosition(ViewportPosition::Bottom))
}
_ => {
self.reset();
NormalGrammarOutput::Unmatched
}
},
}
}
fn finish_pending_operator(
&mut self,
count: Count,
operator: Operator,
token: KeyToken,
) -> NormalGrammarOutput {
if let Some(digit) = token.count_digit()
&& (digit != 0 || self.count.is_some())
{
self.push_count_digit(digit);
self.pending = Some(PendingPrefix::Operator { count, operator });
return NormalGrammarOutput::Pending;
}
match token {
KeyToken::Char('d') if operator == Operator::Delete => {
self.operator_command(count, operator, NormalOperatorTarget::CurrentLine)
}
KeyToken::Char('y') if operator == Operator::Yank => {
self.operator_command(count, operator, NormalOperatorTarget::CurrentLine)
}
KeyToken::Char('c') if operator == Operator::Change => {
self.operator_command(count, operator, NormalOperatorTarget::CurrentLine)
}
KeyToken::Char('g') => {
self.pending = Some(PendingPrefix::OperatorG { count, operator });
NormalGrammarOutput::Pending
}
KeyToken::Char('f') => self.pending_operator_char_search(
count,
operator,
CharSearchDirection::Forward,
CharSearchPlacement::OnMatch,
),
KeyToken::Char('F') => self.pending_operator_char_search(
count,
operator,
CharSearchDirection::Backward,
CharSearchPlacement::OnMatch,
),
KeyToken::Char('t') => self.pending_operator_char_search(
count,
operator,
CharSearchDirection::Forward,
CharSearchPlacement::BeforeMatch,
),
KeyToken::Char('T') => self.pending_operator_char_search(
count,
operator,
CharSearchDirection::Backward,
CharSearchPlacement::BeforeMatch,
),
KeyToken::Char('i') => {
self.pending_operator_text_object(count, operator, TextObjectScope::Inner)
}
KeyToken::Char('a') => {
self.pending_operator_text_object(count, operator, TextObjectScope::Around)
}
token => {
if let Some(motion) = self.motion_from_token(token) {
self.operator_command(count, operator, NormalOperatorTarget::Motion(motion))
} else {
self.reset();
NormalGrammarOutput::Unmatched
}
}
}
}
fn finish_pending_operator_text_object(
&mut self,
count: Count,
operator: Operator,
scope: TextObjectScope,
token: KeyToken,
) -> NormalGrammarOutput {
let kind = match token {
KeyToken::Char('w') => TextObjectKind::Word,
KeyToken::Char('p') => TextObjectKind::Paragraph,
_ => {
self.reset();
return NormalGrammarOutput::Unmatched;
}
};
let object = Counted {
count: self.take_count().unwrap_or_default(),
item: TextObject::new(scope, kind),
};
self.operator_command(count, operator, NormalOperatorTarget::TextObject(object))
}
fn finish_pending_operator_g(
&mut self,
count: Count,
operator: Operator,
token: KeyToken,
) -> NormalGrammarOutput {
let motion = match token {
KeyToken::Char('g') => {
let address = self
.take_count()
.map_or(LineAddress::FirstNonBlank, |count| {
LineAddress::Number(count.0)
});
Counted::once(Motion::LineAddress(address))
}
KeyToken::Char('e') => {
Counted::once(Motion::WordEnd(WordEndMotion::Backward(WordKind::Normal)))
}
KeyToken::Char('E') => {
Counted::once(Motion::WordEnd(WordEndMotion::Backward(WordKind::Big)))
}
_ => {
self.reset();
return NormalGrammarOutput::Unmatched;
}
};
self.operator_command(count, operator, NormalOperatorTarget::Motion(motion))
}
fn motion(&mut self, motion: Motion) -> NormalGrammarOutput {
let count = self.take_count().unwrap_or_default();
self.command(NormalCommand::Motion(Counted {
count,
item: motion,
}))
}
fn motion_from_token(&mut self, token: KeyToken) -> Option<Counted<Motion>> {
let motion = match token {
KeyToken::Char('h') => Motion::Left,
KeyToken::Char('j') => Motion::Down,
KeyToken::Char('k') => Motion::Up,
KeyToken::Char('l') => Motion::Right,
KeyToken::Ctrl('f' | 'F') => Motion::Page(PageDirection::Forward),
KeyToken::Ctrl('b' | 'B') => Motion::Page(PageDirection::Backward),
KeyToken::Char('w') => Motion::WordForward(WordKind::Normal),
KeyToken::Char('W') => Motion::WordForward(WordKind::Big),
KeyToken::Char('b') => Motion::WordBackward(WordKind::Normal),
KeyToken::Char('B') => Motion::WordBackward(WordKind::Big),
KeyToken::Char('e') => Motion::WordEnd(WordEndMotion::Forward(WordKind::Normal)),
KeyToken::Char('E') => Motion::WordEnd(WordEndMotion::Forward(WordKind::Big)),
KeyToken::Char('0') => Motion::Column(ColumnMotion::LineStart),
KeyToken::Char('^') => Motion::Column(ColumnMotion::FirstNonBlank),
KeyToken::Char('$') => Motion::Column(ColumnMotion::LineEnd),
KeyToken::Char('|') => Motion::Column(ColumnMotion::ScreenColumn),
KeyToken::Char('G') => {
let address = self
.take_count()
.map_or(LineAddress::LastNonBlank, |count| {
LineAddress::Number(count.0)
});
return Some(Counted::once(Motion::LineAddress(address)));
}
KeyToken::Char(';') => Motion::RepeatCharSearch,
KeyToken::Char(',') => Motion::RepeatCharSearchReversed,
KeyToken::Char('}') => Motion::Paragraph(ParagraphDirection::Forward),
KeyToken::Char('{') => Motion::Paragraph(ParagraphDirection::Backward),
_ => return None,
};
Some(Counted {
count: self.take_count().unwrap_or_default(),
item: motion,
})
}
const fn command(&mut self, command: NormalCommand) -> NormalGrammarOutput {
self.reset();
NormalGrammarOutput::Command(command)
}
const fn operator_command(
&mut self,
count: Count,
operator: Operator,
target: NormalOperatorTarget,
) -> NormalGrammarOutput {
self.reset();
NormalGrammarOutput::Command(NormalCommand::Operator {
count,
operator,
target,
})
}
const fn insert(&mut self, entry: InsertEntry) -> NormalGrammarOutput {
self.reset();
NormalGrammarOutput::Command(NormalCommand::Insert(entry))
}
fn paste(&mut self, placement: PastePlacement) -> NormalGrammarOutput {
let count = self.take_count().unwrap_or_default();
self.command(NormalCommand::Paste { count, placement })
}
const fn pending_char_search(
&mut self,
direction: CharSearchDirection,
placement: CharSearchPlacement,
) -> NormalGrammarOutput {
self.pending = Some(PendingPrefix::CharSearch {
direction,
placement,
});
NormalGrammarOutput::Pending
}
fn pending_operator(&mut self, operator: Operator) -> NormalGrammarOutput {
let count = self.take_count().unwrap_or_default();
self.pending = Some(PendingPrefix::Operator { count, operator });
NormalGrammarOutput::Pending
}
const fn pending_operator_char_search(
&mut self,
count: Count,
operator: Operator,
direction: CharSearchDirection,
placement: CharSearchPlacement,
) -> NormalGrammarOutput {
self.pending = Some(PendingPrefix::OperatorCharSearch {
count,
operator,
direction,
placement,
});
NormalGrammarOutput::Pending
}
const fn pending_operator_text_object(
&mut self,
count: Count,
operator: Operator,
scope: TextObjectScope,
) -> NormalGrammarOutput {
self.pending = Some(PendingPrefix::OperatorTextObject {
count,
operator,
scope,
});
NormalGrammarOutput::Pending
}
fn push_count_digit(&mut self, digit: usize) {
let next = self.count.map_or(digit, |count| {
count.get().saturating_mul(10).saturating_add(digit)
});
self.count = NonZeroUsize::new(next);
}
fn take_count(&mut self) -> Option<Count> {
self.count.take().map(Count::from)
}
}
#[cfg(test)]
mod tests {
use super::{
Counted, ModeSwitch, NormalCommand, NormalGrammar, NormalGrammarOutput,
NormalOperatorTarget, Operator,
};
use crate::vim::{
CharSearch, CharSearchDirection, CharSearchPlacement, ColumnMotion, InsertEntry, KeyToken,
LineAddress, Motion, PageDirection, PastePlacement, TextObject, TextObjectKind,
TextObjectScope, ViewportPosition, WordKind, motion::WordEndMotion,
search::SearchRepeatDirection,
};
use proptest::prelude::*;
fn feed_chars(grammar: &mut NormalGrammar, keys: &str) -> NormalGrammarOutput {
let mut output = NormalGrammarOutput::Pending;
for character in keys.chars() {
output = grammar.feed(KeyToken::Char(character));
}
output
}
#[test]
fn parses_counts_outside_relative_motions() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "10j"),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted {
count: std::num::NonZeroUsize::new(10).unwrap().into(),
item: Motion::Down,
}))
);
}
#[test]
fn parses_line_addresses_with_count_aware_targets() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "100gg"),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
Motion::LineAddress(LineAddress::Number(
std::num::NonZeroUsize::new(100).unwrap()
))
)))
);
assert_eq!(
feed_chars(&mut grammar, "G"),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
Motion::LineAddress(LineAddress::LastNonBlank)
)))
);
}
#[test]
fn parses_character_search_target_as_motion_payload() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('f')),
NormalGrammarOutput::Pending
);
assert_eq!(
grammar.feed(KeyToken::Char('b')),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::CharSearch(
CharSearch {
target: 'b',
direction: CharSearchDirection::Forward,
placement: CharSearchPlacement::OnMatch,
}
))))
);
}
#[test]
fn parses_control_page_motions() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Ctrl('f')),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::Page(
PageDirection::Forward
))))
);
assert_eq!(
grammar.feed(KeyToken::Ctrl('b')),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::Page(
PageDirection::Backward
))))
);
}
#[test]
fn parses_control_v_as_blockwise_visual_mode() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Ctrl('v')),
NormalGrammarOutput::Command(NormalCommand::ModeSwitch(ModeSwitch::VisualBlockwise))
);
}
#[test]
fn parses_viewport_position_commands() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "zt"),
NormalGrammarOutput::Command(NormalCommand::ViewportPosition(ViewportPosition::Top))
);
assert_eq!(
feed_chars(&mut grammar, "zz"),
NormalGrammarOutput::Command(NormalCommand::ViewportPosition(ViewportPosition::Center))
);
assert_eq!(
feed_chars(&mut grammar, "zb"),
NormalGrammarOutput::Command(NormalCommand::ViewportPosition(ViewportPosition::Bottom))
);
}
#[test]
fn parses_search_repeat_commands() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('n')),
NormalGrammarOutput::Command(NormalCommand::SearchRepeat(SearchRepeatDirection::Next))
);
assert_eq!(
grammar.feed(KeyToken::Char('N')),
NormalGrammarOutput::Command(NormalCommand::SearchRepeat(
SearchRepeatDirection::Previous
))
);
}
#[test]
fn parses_dot_repeat_command() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('.')),
NormalGrammarOutput::Command(NormalCommand::RepeatLastChange)
);
}
#[test]
fn parses_insert_entry_commands() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('i')),
NormalGrammarOutput::Command(NormalCommand::Insert(InsertEntry::AtCursor))
);
assert_eq!(
grammar.feed(KeyToken::Char('a')),
NormalGrammarOutput::Command(NormalCommand::Insert(InsertEntry::AfterCursor))
);
assert_eq!(
grammar.feed(KeyToken::Char('I')),
NormalGrammarOutput::Command(NormalCommand::Insert(InsertEntry::FirstNonBlank))
);
assert_eq!(
grammar.feed(KeyToken::Char('A')),
NormalGrammarOutput::Command(NormalCommand::Insert(InsertEntry::LineEnd))
);
assert_eq!(
grammar.feed(KeyToken::Char('o')),
NormalGrammarOutput::Command(NormalCommand::Insert(InsertEntry::OpenLineBelow))
);
assert_eq!(
grammar.feed(KeyToken::Char('O')),
NormalGrammarOutput::Command(NormalCommand::Insert(InsertEntry::OpenLineAbove))
);
}
#[test]
fn parses_paste_commands_with_counts() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('p')),
NormalGrammarOutput::Command(NormalCommand::Paste {
count: super::Count::default(),
placement: PastePlacement::After,
})
);
assert_eq!(
feed_chars(&mut grammar, "3P"),
NormalGrammarOutput::Command(NormalCommand::Paste {
count: std::num::NonZeroUsize::new(3).unwrap().into(),
placement: PastePlacement::Before,
})
);
}
#[test]
fn parses_normal_operator_motion_targets() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "dw"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Delete,
target: NormalOperatorTarget::Motion(Counted::once(Motion::WordForward(
WordKind::Normal
))),
})
);
assert_eq!(
feed_chars(&mut grammar, "y$"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Yank,
target: NormalOperatorTarget::Motion(Counted::once(Motion::Column(
ColumnMotion::LineEnd
))),
})
);
}
#[test]
fn parses_doubled_operators_as_current_line_targets() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "dd"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Delete,
target: NormalOperatorTarget::CurrentLine,
})
);
assert_eq!(
feed_chars(&mut grammar, "yy"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Yank,
target: NormalOperatorTarget::CurrentLine,
})
);
}
#[test]
fn parses_operator_and_motion_counts_separately() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "2d3w"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: std::num::NonZeroUsize::new(2).unwrap().into(),
operator: Operator::Delete,
target: NormalOperatorTarget::Motion(Counted {
count: std::num::NonZeroUsize::new(3).unwrap().into(),
item: Motion::WordForward(WordKind::Normal),
}),
})
);
}
#[test]
fn parses_operator_text_object_targets() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "diw"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Delete,
target: NormalOperatorTarget::TextObject(Counted::once(TextObject::new(
TextObjectScope::Inner,
TextObjectKind::Word,
))),
})
);
assert_eq!(
feed_chars(&mut grammar, "yap"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Yank,
target: NormalOperatorTarget::TextObject(Counted::once(TextObject::new(
TextObjectScope::Around,
TextObjectKind::Paragraph,
))),
})
);
}
#[test]
fn parses_text_object_counts_inside_operator_targets() {
let mut grammar = NormalGrammar::default();
assert_eq!(
feed_chars(&mut grammar, "d2iw"),
NormalGrammarOutput::Command(NormalCommand::Operator {
count: super::Count::default(),
operator: Operator::Delete,
target: NormalOperatorTarget::TextObject(Counted {
count: std::num::NonZeroUsize::new(2).unwrap().into(),
item: TextObject::new(TextObjectScope::Inner, TextObjectKind::Word),
}),
})
);
}
#[test]
fn unsupported_viewport_position_resets_pending_prefix() {
let mut grammar = NormalGrammar::default();
assert_eq!(
grammar.feed(KeyToken::Char('z')),
NormalGrammarOutput::Pending
);
assert_eq!(
grammar.feed(KeyToken::Char('x')),
NormalGrammarOutput::Unmatched
);
assert_eq!(
grammar.feed(KeyToken::Char('j')),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::Down)))
);
}
proptest! {
#[test]
fn parses_relative_motion_counts_generically(count in 1usize..10_000, key in prop_oneof![
Just('h'),
Just('j'),
Just('k'),
Just('l'),
Just('w'),
Just('W'),
Just('b'),
Just('B'),
Just('e'),
Just('E'),
Just('$'),
Just('^'),
]) {
let mut grammar = NormalGrammar::default();
let input = format!("{count}{key}");
let output = feed_chars(&mut grammar, &input);
let motion = match key {
'h' => Motion::Left,
'j' => Motion::Down,
'k' => Motion::Up,
'l' => Motion::Right,
'w' => Motion::WordForward(WordKind::Normal),
'W' => Motion::WordForward(WordKind::Big),
'b' => Motion::WordBackward(WordKind::Normal),
'B' => Motion::WordBackward(WordKind::Big),
'e' => Motion::WordEnd(WordEndMotion::Forward(WordKind::Normal)),
'E' => Motion::WordEnd(WordEndMotion::Forward(WordKind::Big)),
'$' => Motion::Column(ColumnMotion::LineEnd),
'^' => Motion::Column(ColumnMotion::FirstNonBlank),
_ => unreachable!("strategy only emits supported keys"),
};
prop_assert_eq!(
output,
NormalGrammarOutput::Command(NormalCommand::Motion(Counted {
count: std::num::NonZeroUsize::new(count).unwrap().into(),
item: motion,
}))
);
}
#[test]
fn char_search_target_is_not_reinterpreted_as_a_followup_motion(target in any::<char>()) {
let mut grammar = NormalGrammar::default();
prop_assert_eq!(grammar.feed(KeyToken::Char('f')), NormalGrammarOutput::Pending);
prop_assert_eq!(
grammar.feed(KeyToken::Char(target)),
NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
Motion::CharSearch(CharSearch {
target,
direction: CharSearchDirection::Forward,
placement: CharSearchPlacement::OnMatch,
})
)))
);
}
}
}