1use std::num::NonZeroUsize;
4
5use super::{
6 key::KeyToken,
7 motion::{
8 CharSearch, CharSearchDirection, CharSearchPlacement, ColumnMotion, LineAddress, Motion,
9 PageDirection, ParagraphDirection, WordEndMotion, WordKind,
10 },
11 search::{SearchDirection, SearchRepeatDirection},
12};
13
14#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
16pub struct Count(NonZeroUsize);
17
18impl Count {
19 #[must_use]
21 pub const fn new(value: NonZeroUsize) -> Self {
22 Self(value)
23 }
24
25 #[must_use]
27 pub const fn get(self) -> usize {
28 self.0.get()
29 }
30}
31
32impl Default for Count {
33 fn default() -> Self {
34 Self(NonZeroUsize::MIN)
35 }
36}
37
38impl From<NonZeroUsize> for Count {
39 fn from(value: NonZeroUsize) -> Self {
40 Self::new(value)
41 }
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub struct Counted<T> {
47 pub count: Count,
49 pub item: T,
51}
52
53impl<T> Counted<T> {
54 #[must_use]
56 pub fn once(item: T) -> Self {
57 Self {
58 count: Count::default(),
59 item,
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum NormalCommand {
67 Motion(Counted<Motion>),
69 Operator {
71 count: Count,
73 operator: Operator,
75 motion: Counted<Motion>,
77 },
78 ModeSwitch(ModeSwitch),
80 ExCommandStart,
82 SearchStart(SearchDirection),
84 SearchRepeat(SearchRepeatDirection),
86 ViewportPosition(ViewportPosition),
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub enum Operator {
93 Delete,
95 Yank,
97 Change,
99}
100
101#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum ModeSwitch {
104 VisualCharacterwise,
106 VisualLinewise,
108}
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum ViewportPosition {
113 Top,
115 Center,
117 Bottom,
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123pub enum NormalGrammarOutput {
124 Pending,
126 Command(NormalCommand),
128 Unmatched,
130}
131
132#[derive(Clone, Copy, Debug, Eq, PartialEq)]
134enum PendingPrefix {
135 G,
137 Z,
139 CharSearch {
141 direction: CharSearchDirection,
143 placement: CharSearchPlacement,
145 },
146}
147
148#[derive(Clone, Debug, Default)]
150pub struct NormalGrammar {
151 count: Option<NonZeroUsize>,
153 pending: Option<PendingPrefix>,
155}
156
157impl NormalGrammar {
158 pub const fn reset(&mut self) {
160 self.count = None;
161 self.pending = None;
162 }
163
164 pub fn feed(&mut self, token: KeyToken) -> NormalGrammarOutput {
166 if let Some(pending) = self.pending {
167 return self.finish_pending(pending, token);
168 }
169
170 if let Some(digit) = token.count_digit()
171 && (digit != 0 || self.count.is_some())
172 {
173 self.push_count_digit(digit);
174 return NormalGrammarOutput::Pending;
175 }
176
177 match token {
178 KeyToken::Char('h') => self.motion(Motion::Left),
179 KeyToken::Char('j') => self.motion(Motion::Down),
180 KeyToken::Char('k') => self.motion(Motion::Up),
181 KeyToken::Char('l') => self.motion(Motion::Right),
182 KeyToken::Ctrl('f' | 'F') => self.motion(Motion::Page(PageDirection::Forward)),
183 KeyToken::Ctrl('b' | 'B') => self.motion(Motion::Page(PageDirection::Backward)),
184 KeyToken::Char('w') => self.motion(Motion::WordForward(WordKind::Normal)),
185 KeyToken::Char('W') => self.motion(Motion::WordForward(WordKind::Big)),
186 KeyToken::Char('b') => self.motion(Motion::WordBackward(WordKind::Normal)),
187 KeyToken::Char('B') => self.motion(Motion::WordBackward(WordKind::Big)),
188 KeyToken::Char('e') => {
189 self.motion(Motion::WordEnd(WordEndMotion::Forward(WordKind::Normal)))
190 }
191 KeyToken::Char('E') => {
192 self.motion(Motion::WordEnd(WordEndMotion::Forward(WordKind::Big)))
193 }
194 KeyToken::Char('0') => self.motion(Motion::Column(ColumnMotion::LineStart)),
195 KeyToken::Char('^') => self.motion(Motion::Column(ColumnMotion::FirstNonBlank)),
196 KeyToken::Char('$') => self.motion(Motion::Column(ColumnMotion::LineEnd)),
197 KeyToken::Char('|') => self.motion(Motion::Column(ColumnMotion::ScreenColumn)),
198 KeyToken::Char('G') => {
199 let address = self
200 .take_count()
201 .map_or(LineAddress::LastNonBlank, |count| {
202 LineAddress::Number(count.0)
203 });
204 NormalGrammarOutput::Command(NormalCommand::Motion(Counted {
205 count: Count::default(),
206 item: Motion::LineAddress(address),
207 }))
208 }
209 KeyToken::Char('g') => {
210 self.pending = Some(PendingPrefix::G);
211 NormalGrammarOutput::Pending
212 }
213 KeyToken::Char('z') => {
214 self.pending = Some(PendingPrefix::Z);
215 NormalGrammarOutput::Pending
216 }
217 KeyToken::Char('f') => {
218 self.pending_char_search(CharSearchDirection::Forward, CharSearchPlacement::OnMatch)
219 }
220 KeyToken::Char('F') => self
221 .pending_char_search(CharSearchDirection::Backward, CharSearchPlacement::OnMatch),
222 KeyToken::Char('t') => self.pending_char_search(
223 CharSearchDirection::Forward,
224 CharSearchPlacement::BeforeMatch,
225 ),
226 KeyToken::Char('T') => self.pending_char_search(
227 CharSearchDirection::Backward,
228 CharSearchPlacement::BeforeMatch,
229 ),
230 KeyToken::Char(';') => self.motion(Motion::RepeatCharSearch),
231 KeyToken::Char(',') => self.motion(Motion::RepeatCharSearchReversed),
232 KeyToken::Char('}') => self.motion(Motion::Paragraph(ParagraphDirection::Forward)),
233 KeyToken::Char('{') => self.motion(Motion::Paragraph(ParagraphDirection::Backward)),
234 KeyToken::Char(':') => self.command(NormalCommand::ExCommandStart),
235 KeyToken::Char('/') => {
236 self.command(NormalCommand::SearchStart(SearchDirection::Forward))
237 }
238 KeyToken::Char('?') => {
239 self.command(NormalCommand::SearchStart(SearchDirection::Backward))
240 }
241 KeyToken::Char('n') => {
242 self.command(NormalCommand::SearchRepeat(SearchRepeatDirection::Next))
243 }
244 KeyToken::Char('N') => {
245 self.command(NormalCommand::SearchRepeat(SearchRepeatDirection::Previous))
246 }
247 KeyToken::Char('v') => {
248 self.command(NormalCommand::ModeSwitch(ModeSwitch::VisualCharacterwise))
249 }
250 KeyToken::Char('V') => {
251 self.command(NormalCommand::ModeSwitch(ModeSwitch::VisualLinewise))
252 }
253 _ => {
254 self.reset();
255 NormalGrammarOutput::Unmatched
256 }
257 }
258 }
259
260 fn finish_pending(&mut self, pending: PendingPrefix, token: KeyToken) -> NormalGrammarOutput {
262 self.pending = None;
263
264 match pending {
265 PendingPrefix::G if token == KeyToken::Char('g') => {
266 let address = self
267 .take_count()
268 .map_or(LineAddress::FirstNonBlank, |count| {
269 LineAddress::Number(count.0)
270 });
271 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
272 Motion::LineAddress(address),
273 )))
274 }
275 PendingPrefix::G if token == KeyToken::Char('e') => {
276 self.command(NormalCommand::Motion(Counted::once(Motion::WordEnd(
277 WordEndMotion::Backward(WordKind::Normal),
278 ))))
279 }
280 PendingPrefix::G if token == KeyToken::Char('E') => {
281 self.command(NormalCommand::Motion(Counted::once(Motion::WordEnd(
282 WordEndMotion::Backward(WordKind::Big),
283 ))))
284 }
285 PendingPrefix::CharSearch {
286 direction,
287 placement,
288 } => {
289 if let KeyToken::Char(target) = token {
290 let count = self.take_count().unwrap_or_default();
291 self.command(NormalCommand::Motion(Counted {
292 count,
293 item: Motion::CharSearch(CharSearch {
294 target,
295 direction,
296 placement,
297 }),
298 }))
299 } else {
300 self.reset();
301 NormalGrammarOutput::Unmatched
302 }
303 }
304 PendingPrefix::G => {
305 self.reset();
306 NormalGrammarOutput::Unmatched
307 }
308 PendingPrefix::Z => match token {
309 KeyToken::Char('t') => {
310 self.command(NormalCommand::ViewportPosition(ViewportPosition::Top))
311 }
312 KeyToken::Char('z') => {
313 self.command(NormalCommand::ViewportPosition(ViewportPosition::Center))
314 }
315 KeyToken::Char('b') => {
316 self.command(NormalCommand::ViewportPosition(ViewportPosition::Bottom))
317 }
318 _ => {
319 self.reset();
320 NormalGrammarOutput::Unmatched
321 }
322 },
323 }
324 }
325
326 fn motion(&mut self, motion: Motion) -> NormalGrammarOutput {
328 let count = self.take_count().unwrap_or_default();
329 self.command(NormalCommand::Motion(Counted {
330 count,
331 item: motion,
332 }))
333 }
334
335 const fn command(&mut self, command: NormalCommand) -> NormalGrammarOutput {
337 self.reset();
338 NormalGrammarOutput::Command(command)
339 }
340
341 const fn pending_char_search(
343 &mut self,
344 direction: CharSearchDirection,
345 placement: CharSearchPlacement,
346 ) -> NormalGrammarOutput {
347 self.pending = Some(PendingPrefix::CharSearch {
348 direction,
349 placement,
350 });
351 NormalGrammarOutput::Pending
352 }
353
354 fn push_count_digit(&mut self, digit: usize) {
356 let next = self.count.map_or(digit, |count| {
357 count.get().saturating_mul(10).saturating_add(digit)
358 });
359 self.count = NonZeroUsize::new(next);
360 }
361
362 fn take_count(&mut self) -> Option<Count> {
364 self.count.take().map(Count::from)
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::{Counted, NormalCommand, NormalGrammar, NormalGrammarOutput};
371 use crate::vim::{
372 CharSearch, CharSearchDirection, CharSearchPlacement, ColumnMotion, KeyToken, LineAddress,
373 Motion, PageDirection, ViewportPosition, WordKind, motion::WordEndMotion,
374 search::SearchRepeatDirection,
375 };
376 use proptest::prelude::*;
377
378 fn feed_chars(grammar: &mut NormalGrammar, keys: &str) -> NormalGrammarOutput {
379 let mut output = NormalGrammarOutput::Pending;
380 for character in keys.chars() {
381 output = grammar.feed(KeyToken::Char(character));
382 }
383 output
384 }
385
386 #[test]
387 fn parses_counts_outside_relative_motions() {
388 let mut grammar = NormalGrammar::default();
389
390 assert_eq!(
391 feed_chars(&mut grammar, "10j"),
392 NormalGrammarOutput::Command(NormalCommand::Motion(Counted {
393 count: std::num::NonZeroUsize::new(10).unwrap().into(),
394 item: Motion::Down,
395 }))
396 );
397 }
398
399 #[test]
400 fn parses_line_addresses_with_count_aware_targets() {
401 let mut grammar = NormalGrammar::default();
402
403 assert_eq!(
404 feed_chars(&mut grammar, "100gg"),
405 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
406 Motion::LineAddress(LineAddress::Number(
407 std::num::NonZeroUsize::new(100).unwrap()
408 ))
409 )))
410 );
411 assert_eq!(
412 feed_chars(&mut grammar, "G"),
413 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
414 Motion::LineAddress(LineAddress::LastNonBlank)
415 )))
416 );
417 }
418
419 #[test]
420 fn parses_character_search_target_as_motion_payload() {
421 let mut grammar = NormalGrammar::default();
422
423 assert_eq!(
424 grammar.feed(KeyToken::Char('f')),
425 NormalGrammarOutput::Pending
426 );
427 assert_eq!(
428 grammar.feed(KeyToken::Char('b')),
429 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::CharSearch(
430 CharSearch {
431 target: 'b',
432 direction: CharSearchDirection::Forward,
433 placement: CharSearchPlacement::OnMatch,
434 }
435 ))))
436 );
437 }
438
439 #[test]
440 fn parses_control_page_motions() {
441 let mut grammar = NormalGrammar::default();
442
443 assert_eq!(
444 grammar.feed(KeyToken::Ctrl('f')),
445 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::Page(
446 PageDirection::Forward
447 ))))
448 );
449 assert_eq!(
450 grammar.feed(KeyToken::Ctrl('b')),
451 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::Page(
452 PageDirection::Backward
453 ))))
454 );
455 }
456
457 #[test]
458 fn parses_viewport_position_commands() {
459 let mut grammar = NormalGrammar::default();
460
461 assert_eq!(
462 feed_chars(&mut grammar, "zt"),
463 NormalGrammarOutput::Command(NormalCommand::ViewportPosition(ViewportPosition::Top))
464 );
465 assert_eq!(
466 feed_chars(&mut grammar, "zz"),
467 NormalGrammarOutput::Command(NormalCommand::ViewportPosition(ViewportPosition::Center))
468 );
469 assert_eq!(
470 feed_chars(&mut grammar, "zb"),
471 NormalGrammarOutput::Command(NormalCommand::ViewportPosition(ViewportPosition::Bottom))
472 );
473 }
474
475 #[test]
476 fn parses_search_repeat_commands() {
477 let mut grammar = NormalGrammar::default();
478
479 assert_eq!(
480 grammar.feed(KeyToken::Char('n')),
481 NormalGrammarOutput::Command(NormalCommand::SearchRepeat(SearchRepeatDirection::Next))
482 );
483 assert_eq!(
484 grammar.feed(KeyToken::Char('N')),
485 NormalGrammarOutput::Command(NormalCommand::SearchRepeat(
486 SearchRepeatDirection::Previous
487 ))
488 );
489 }
490
491 #[test]
492 fn unsupported_viewport_position_resets_pending_prefix() {
493 let mut grammar = NormalGrammar::default();
494
495 assert_eq!(
496 grammar.feed(KeyToken::Char('z')),
497 NormalGrammarOutput::Pending
498 );
499 assert_eq!(
500 grammar.feed(KeyToken::Char('x')),
501 NormalGrammarOutput::Unmatched
502 );
503 assert_eq!(
504 grammar.feed(KeyToken::Char('j')),
505 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(Motion::Down)))
506 );
507 }
508
509 proptest! {
510 #[test]
511 fn parses_relative_motion_counts_generically(count in 1usize..10_000, key in prop_oneof![
512 Just('h'),
513 Just('j'),
514 Just('k'),
515 Just('l'),
516 Just('w'),
517 Just('W'),
518 Just('b'),
519 Just('B'),
520 Just('e'),
521 Just('E'),
522 Just('$'),
523 Just('^'),
524 ]) {
525 let mut grammar = NormalGrammar::default();
526 let input = format!("{count}{key}");
527 let output = feed_chars(&mut grammar, &input);
528 let motion = match key {
529 'h' => Motion::Left,
530 'j' => Motion::Down,
531 'k' => Motion::Up,
532 'l' => Motion::Right,
533 'w' => Motion::WordForward(WordKind::Normal),
534 'W' => Motion::WordForward(WordKind::Big),
535 'b' => Motion::WordBackward(WordKind::Normal),
536 'B' => Motion::WordBackward(WordKind::Big),
537 'e' => Motion::WordEnd(WordEndMotion::Forward(WordKind::Normal)),
538 'E' => Motion::WordEnd(WordEndMotion::Forward(WordKind::Big)),
539 '$' => Motion::Column(ColumnMotion::LineEnd),
540 '^' => Motion::Column(ColumnMotion::FirstNonBlank),
541 _ => unreachable!("strategy only emits supported keys"),
542 };
543
544 prop_assert_eq!(
545 output,
546 NormalGrammarOutput::Command(NormalCommand::Motion(Counted {
547 count: std::num::NonZeroUsize::new(count).unwrap().into(),
548 item: motion,
549 }))
550 );
551 }
552
553 #[test]
554 fn char_search_target_is_not_reinterpreted_as_a_followup_motion(target in any::<char>()) {
555 let mut grammar = NormalGrammar::default();
556
557 prop_assert_eq!(grammar.feed(KeyToken::Char('f')), NormalGrammarOutput::Pending);
558 prop_assert_eq!(
559 grammar.feed(KeyToken::Char(target)),
560 NormalGrammarOutput::Command(NormalCommand::Motion(Counted::once(
561 Motion::CharSearch(CharSearch {
562 target,
563 direction: CharSearchDirection::Forward,
564 placement: CharSearchPlacement::OnMatch,
565 })
566 )))
567 );
568 }
569 }
570}