1#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
3pub enum VimMode {
4 #[default]
6 Normal,
7 Insert,
9 Visual,
11 VisualLine,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub(crate) enum VimMotion {
18 Left,
20 Down,
22 Up,
24 Right,
26 WordForward,
28 WordBackward,
30 WordEnd,
32 LineStart,
34 FirstNonBlank,
36 LineEnd,
38 DocumentStart,
40 DocumentEnd,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub(crate) enum VimOperator {
48 Delete,
50 Change,
52 Yank,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub(crate) enum VimInsertPosition {
59 BeforeCursor,
61 AfterCursor,
63 FirstNonBlank,
65 EndOfLine,
67 NewLineBelow,
69 NewLineAbove,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub(crate) enum VimPastePosition {
76 AfterCursor,
78 BeforeCursor,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub(crate) enum VimAction {
85 Mode(VimMode),
87 Motion { motion: VimMotion, count: usize, explicit_count: bool },
92 Insert { position: VimInsertPosition, count: usize },
95 Operator {
98 operator: VimOperator,
99 motion: VimMotion,
100 count: usize,
101 explicit_count: bool,
102 },
103 LineOperator { operator: VimOperator, count: usize },
105 VisualOperator(VimOperator),
107 DeleteCharacters { count: usize },
109 Paste { position: VimPastePosition, count: usize },
111 Undo { count: usize },
113 Redo { count: usize },
115 RepeatSearch { reverse: bool },
118 CommandLineChanged,
120 SubmitSearch(String),
122 SubmitGotoLine(usize),
125 WriteFile { exit_vim: bool },
128 ExitVimMode,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133enum VimCommandLineKind {
134 Search,
135 Command,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139struct VimCommandLine {
140 kind: VimCommandLineKind,
141 input: String,
142}
143
144#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
146pub(crate) enum VimRegisterKind {
147 #[default]
148 Characterwise,
149 Linewise,
150}
151
152#[derive(Debug, Default, Clone, PartialEq, Eq)]
154pub(crate) struct VimRegister {
155 pub(crate) text: String,
156 pub(crate) kind: VimRegisterKind,
157}
158
159#[derive(Debug, Default)]
161pub(crate) struct VimState {
162 mode: VimMode,
163 count: Option<usize>,
164 g_prefix: bool,
165 pending_operator: Option<VimOperator>,
166 pending_operator_count: usize,
167 visual_anchor: Option<(usize, usize)>,
168 visual_active: Option<(usize, usize)>,
169 command_line: Option<VimCommandLine>,
170 last_search: Option<String>,
171 pub(crate) register: VimRegister,
172}
173
174impl VimState {
175 pub(crate) fn mode(&self) -> VimMode {
176 self.mode
177 }
178
179 pub(crate) fn reset(&mut self) {
180 *self = Self::default();
181 }
182
183 pub(crate) fn enter_clean_normal_mode(&mut self) {
184 self.mode = VimMode::Normal;
185 self.clear_visual();
186 self.command_line = None;
187 self.clear_pending();
188 }
189
190 pub(crate) fn command_line_active(&self) -> bool {
191 self.command_line.is_some()
192 }
193
194 pub(crate) fn last_search(&self) -> Option<&str> {
195 self.last_search.as_deref()
196 }
197
198 pub(crate) fn command_line_text(&self) -> Option<String> {
199 self.command_line.as_ref().map(|command_line| {
200 let prefix = match command_line.kind {
201 VimCommandLineKind::Search => '/',
202 VimCommandLineKind::Command => ':',
203 };
204 format!("{prefix}{}", command_line.input)
205 })
206 }
207
208 pub(crate) fn pending_keys(&self) -> String {
209 let mut pending = String::new();
210 if let Some(operator) = self.pending_operator {
211 if self.pending_operator_count > 1 {
212 pending.push_str(&self.pending_operator_count.to_string());
213 }
214 pending.push(match operator {
215 VimOperator::Delete => 'd',
216 VimOperator::Change => 'c',
217 VimOperator::Yank => 'y',
218 });
219 if let Some(count) = self.count {
220 pending.push_str(&count.to_string());
221 }
222 } else if let Some(count) = self.count {
223 pending.push_str(&count.to_string());
224 }
225 if self.g_prefix {
226 pending.push('g');
227 }
228 pending
229 }
230
231 pub(crate) fn status_line_text(&self) -> (String, String) {
232 if let Some(command_line) = self.command_line_text() {
233 return (command_line, String::new());
234 }
235
236 let mode = match self.mode {
237 VimMode::Normal => "NORMAL",
238 VimMode::Insert => "INSERT",
239 VimMode::Visual => "VISUAL",
240 VimMode::VisualLine => "VISUAL LINE",
241 };
242
243 (format!("-- {mode} --"), self.pending_keys())
244 }
245
246 pub(crate) fn begin_visual(&mut self, position: (usize, usize)) {
247 self.visual_anchor = Some(position);
248 self.visual_active = Some(position);
249 }
250
251 pub(crate) fn visual_positions(
252 &self,
253 ) -> Option<((usize, usize), (usize, usize))> {
254 Some((self.visual_anchor?, self.visual_active?))
255 }
256
257 pub(crate) fn set_visual_active(&mut self, position: (usize, usize)) {
258 self.visual_active = Some(position);
259 }
260
261 pub(crate) fn clear_visual(&mut self) {
262 self.visual_anchor = None;
263 self.visual_active = None;
264 }
265
266 pub(crate) fn set_mode_from_mouse(&mut self, mode: VimMode) {
267 self.mode = mode;
268 self.clear_pending();
269 }
270
271 pub(crate) fn enter_insert_mode(&mut self) {
272 self.mode = VimMode::Insert;
273 self.clear_visual();
274 self.command_line = None;
275 self.clear_pending();
276 }
277
278 pub(crate) fn parse_key(&mut self, key: char) -> Option<VimAction> {
279 if self.command_line.is_some() {
280 return self.parse_command_line_key(key);
281 }
282
283 if self.mode == VimMode::Insert {
284 return if key == '\u{1b}' {
285 Some(self.set_mode(VimMode::Normal))
286 } else {
287 None
288 };
289 }
290
291 if key == '\u{1b}' {
292 return Some(self.set_mode(VimMode::Normal));
293 }
294
295 if key.is_ascii_digit() && (key != '0' || self.count.is_some()) {
296 self.push_count_digit(key);
297 return None;
298 }
299
300 if self.g_prefix {
301 if key == 'g' {
302 self.g_prefix = false;
303 return Some(self.finish_motion(VimMotion::DocumentStart));
304 }
305 self.clear_pending();
306 return None;
307 }
308
309 if key == 'g' {
310 self.g_prefix = true;
311 return None;
312 }
313
314 if let Some(operator) = operator_for_key(key) {
315 return self.parse_operator(operator);
316 }
317
318 if let Some(motion) = motion_for_key(key) {
319 return Some(self.finish_motion(motion));
320 }
321
322 let action = match key {
323 'i' => Some(self.insert(VimInsertPosition::BeforeCursor)),
324 'a' => Some(self.insert(VimInsertPosition::AfterCursor)),
325 'I' => Some(self.insert(VimInsertPosition::FirstNonBlank)),
326 'A' => Some(self.insert(VimInsertPosition::EndOfLine)),
327 'o' => Some(self.insert(VimInsertPosition::NewLineBelow)),
328 'O' => Some(self.insert(VimInsertPosition::NewLineAbove)),
329 'v' => Some(self.set_mode(VimMode::Visual)),
330 'V' => Some(self.set_mode(VimMode::VisualLine)),
331 'x' => {
332 Some(VimAction::DeleteCharacters { count: self.take_count() })
333 }
334 'p' => Some(VimAction::Paste {
335 position: VimPastePosition::AfterCursor,
336 count: self.take_count(),
337 }),
338 'P' => Some(VimAction::Paste {
339 position: VimPastePosition::BeforeCursor,
340 count: self.take_count(),
341 }),
342 'u' => Some(VimAction::Undo { count: self.take_count() }),
343 '\u{12}' => Some(VimAction::Redo { count: self.take_count() }),
344 'n' => Some(VimAction::RepeatSearch { reverse: false }),
345 'N' => Some(VimAction::RepeatSearch { reverse: true }),
346 '/' => Some(self.open_command_line(VimCommandLineKind::Search)),
347 ':' => Some(self.open_command_line(VimCommandLineKind::Command)),
348 _ => None,
349 };
350
351 if action.is_none() {
352 self.clear_pending();
353 }
354 action
355 }
356
357 fn open_command_line(&mut self, kind: VimCommandLineKind) -> VimAction {
358 self.clear_pending();
359 self.command_line = Some(VimCommandLine { kind, input: String::new() });
360 VimAction::CommandLineChanged
361 }
362
363 fn parse_command_line_key(&mut self, key: char) -> Option<VimAction> {
364 match key {
365 '\u{1b}' => {
366 self.command_line = None;
367 Some(VimAction::CommandLineChanged)
368 }
369 '\u{8}' => {
370 if let Some(command_line) = self.command_line.as_mut()
371 && command_line.input.pop().is_none()
372 {
373 self.command_line = None;
374 }
375 Some(VimAction::CommandLineChanged)
376 }
377 '\n' | '\r' => {
378 let command_line = self.command_line.take()?;
379 if command_line.input.is_empty() {
380 return Some(VimAction::CommandLineChanged);
381 }
382 match command_line.kind {
383 VimCommandLineKind::Search => {
384 self.last_search = Some(command_line.input.clone());
385 Some(VimAction::SubmitSearch(command_line.input))
386 }
387 VimCommandLineKind::Command => {
388 match command_line.input.as_str() {
389 "q" => Some(VimAction::ExitVimMode),
390 "w" => {
391 Some(VimAction::WriteFile { exit_vim: false })
392 }
393 "wq" => {
394 Some(VimAction::WriteFile { exit_vim: true })
395 }
396 _ => command_line
397 .input
398 .parse::<usize>()
399 .ok()
400 .filter(|line| *line > 0)
401 .map(VimAction::SubmitGotoLine)
402 .or(Some(VimAction::CommandLineChanged)),
403 }
404 }
405 }
406 }
407 key if !key.is_control() => {
408 if let Some(command_line) = self.command_line.as_mut() {
409 command_line.input.push(key);
410 }
411 Some(VimAction::CommandLineChanged)
412 }
413 _ => None,
414 }
415 }
416
417 fn set_mode(&mut self, mode: VimMode) -> VimAction {
418 self.mode = mode;
419 self.clear_pending();
420 VimAction::Mode(mode)
421 }
422
423 fn insert(&mut self, position: VimInsertPosition) -> VimAction {
424 let count = self.take_count();
425 self.mode = VimMode::Insert;
426 self.clear_pending();
427 VimAction::Insert { position, count }
428 }
429
430 fn parse_operator(&mut self, operator: VimOperator) -> Option<VimAction> {
431 if self.mode != VimMode::Normal {
432 self.clear_pending();
433 return Some(VimAction::VisualOperator(operator));
434 }
435
436 if let Some(pending) = self.pending_operator {
437 if pending == operator {
438 let count = self
439 .pending_operator_count
440 .saturating_mul(self.take_count());
441 self.clear_pending();
442 return Some(VimAction::LineOperator { operator, count });
443 }
444 self.clear_pending();
445 return None;
446 }
447
448 self.pending_operator_count = self.take_count();
449 self.pending_operator = Some(operator);
450 None
451 }
452
453 fn finish_motion(&mut self, motion: VimMotion) -> VimAction {
454 let motion_count_explicit = self.count.is_some();
455 let motion_count = self.take_count();
456 if let Some(operator) = self.pending_operator {
457 let count =
458 self.pending_operator_count.saturating_mul(motion_count);
459 let explicit_count =
463 motion_count_explicit || self.pending_operator_count > 1;
464 self.clear_pending();
465 VimAction::Operator { operator, motion, count, explicit_count }
466 } else {
467 self.g_prefix = false;
468 VimAction::Motion {
469 motion,
470 count: motion_count,
471 explicit_count: motion_count_explicit,
472 }
473 }
474 }
475
476 fn push_count_digit(&mut self, key: char) {
477 let digit = key.to_digit(10).unwrap_or_default() as usize;
478 self.count = Some(
479 self.count
480 .unwrap_or_default()
481 .saturating_mul(10)
482 .saturating_add(digit),
483 );
484 }
485
486 fn take_count(&mut self) -> usize {
487 self.count.take().unwrap_or(1).max(1)
488 }
489
490 fn clear_pending(&mut self) {
491 self.count = None;
492 self.g_prefix = false;
493 self.pending_operator = None;
494 self.pending_operator_count = 1;
495 }
496}
497
498fn motion_for_key(key: char) -> Option<VimMotion> {
499 match key {
500 'h' => Some(VimMotion::Left),
501 'j' => Some(VimMotion::Down),
502 'k' => Some(VimMotion::Up),
503 'l' => Some(VimMotion::Right),
504 'w' => Some(VimMotion::WordForward),
505 'b' => Some(VimMotion::WordBackward),
506 'e' => Some(VimMotion::WordEnd),
507 '0' => Some(VimMotion::LineStart),
508 '^' => Some(VimMotion::FirstNonBlank),
509 '$' => Some(VimMotion::LineEnd),
510 'G' => Some(VimMotion::DocumentEnd),
511 _ => None,
512 }
513}
514
515fn operator_for_key(key: char) -> Option<VimOperator> {
516 match key {
517 'd' => Some(VimOperator::Delete),
518 'c' => Some(VimOperator::Change),
519 'y' => Some(VimOperator::Yank),
520 _ => None,
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::{
527 VimAction, VimMotion, VimOperator, VimRegister, VimRegisterKind,
528 VimState,
529 };
530
531 #[test]
532 fn vim_parser_accumulates_count_and_operator() {
533 let mut state = VimState::default();
534
535 assert_eq!(state.parse_key('2'), None);
536 assert_eq!(state.parse_key('d'), None);
537 assert_eq!(state.parse_key('3'), None);
538 assert_eq!(
539 state.parse_key('w'),
540 Some(VimAction::Operator {
541 operator: VimOperator::Delete,
542 motion: VimMotion::WordForward,
543 count: 6,
544 explicit_count: true,
545 })
546 );
547 }
548
549 #[test]
550 fn vim_parser_zero_is_motion_without_leading_count() {
551 let mut state = VimState::default();
552
553 assert_eq!(
554 state.parse_key('0'),
555 Some(VimAction::Motion {
556 motion: VimMotion::LineStart,
557 count: 1,
558 explicit_count: false,
559 })
560 );
561
562 assert_eq!(state.parse_key('1'), None);
563 assert_eq!(state.parse_key('0'), None);
564 assert_eq!(
565 state.parse_key('l'),
566 Some(VimAction::Motion {
567 motion: VimMotion::Right,
568 count: 10,
569 explicit_count: true,
570 })
571 );
572 }
573
574 #[test]
575 fn vim_parser_distinguishes_explicit_count_for_document_end() {
576 let mut state = VimState::default();
577
578 assert_eq!(
579 state.parse_key('G'),
580 Some(VimAction::Motion {
581 motion: VimMotion::DocumentEnd,
582 count: 1,
583 explicit_count: false,
584 })
585 );
586
587 assert_eq!(state.parse_key('1'), None);
588 assert_eq!(
589 state.parse_key('G'),
590 Some(VimAction::Motion {
591 motion: VimMotion::DocumentEnd,
592 count: 1,
593 explicit_count: true,
594 })
595 );
596 }
597
598 #[test]
599 fn vim_state_resets_typed_unnamed_register() {
600 let mut state = VimState {
601 register: VimRegister {
602 text: "line\n".to_owned(),
603 kind: VimRegisterKind::Linewise,
604 },
605 ..VimState::default()
606 };
607
608 assert_eq!(state.register.text, "line\n");
609 assert_eq!(state.register.kind, VimRegisterKind::Linewise);
610
611 state.reset();
612
613 assert!(state.register.text.is_empty());
614 assert_eq!(state.register.kind, VimRegisterKind::Characterwise);
615 }
616
617 #[test]
618 fn vim_command_line_parser_accepts_edit_submit_and_cancel() {
619 let mut state = VimState::default();
620
621 assert_eq!(state.parse_key('/'), Some(VimAction::CommandLineChanged));
622 assert_eq!(state.command_line_text().as_deref(), Some("/"));
623
624 for key in "foo".chars() {
625 assert_eq!(
626 state.parse_key(key),
627 Some(VimAction::CommandLineChanged)
628 );
629 }
630 assert_eq!(state.command_line_text().as_deref(), Some("/foo"));
631
632 assert_eq!(
633 state.parse_key('\u{8}'),
634 Some(VimAction::CommandLineChanged)
635 );
636 assert_eq!(state.command_line_text().as_deref(), Some("/fo"));
637 assert_eq!(
638 state.parse_key('\n'),
639 Some(VimAction::SubmitSearch("fo".to_owned()))
640 );
641 assert_eq!(state.command_line_text(), None);
642
643 assert_eq!(state.parse_key(':'), Some(VimAction::CommandLineChanged));
644 for key in "12".chars() {
645 let _ = state.parse_key(key);
646 }
647 assert_eq!(state.parse_key('\n'), Some(VimAction::SubmitGotoLine(12)));
648
649 let _ = state.parse_key(':');
650 let _ = state.parse_key('q');
651 assert_eq!(state.parse_key('\n'), Some(VimAction::ExitVimMode));
652
653 let _ = state.parse_key(':');
654 let _ = state.parse_key('w');
655 assert_eq!(
656 state.parse_key('\n'),
657 Some(VimAction::WriteFile { exit_vim: false })
658 );
659
660 let _ = state.parse_key(':');
661 let _ = state.parse_key('w');
662 let _ = state.parse_key('q');
663 assert_eq!(
664 state.parse_key('\n'),
665 Some(VimAction::WriteFile { exit_vim: true })
666 );
667
668 let _ = state.parse_key('/');
669 let _ = state.parse_key('x');
670 assert_eq!(
671 state.parse_key('\u{1b}'),
672 Some(VimAction::CommandLineChanged)
673 );
674 assert_eq!(state.command_line_text(), None);
675 }
676
677 #[test]
678 fn vim_pending_keys_formats_counts_prefixes_and_operators() {
679 let mut state = VimState::default();
680
681 assert_eq!(state.parse_key('5'), None);
682 assert_eq!(state.pending_keys(), "5");
683 assert_eq!(state.parse_key('d'), None);
684 assert_eq!(state.pending_keys(), "5d");
685 assert_eq!(state.parse_key('2'), None);
686 assert_eq!(state.pending_keys(), "5d2");
687
688 state.enter_clean_normal_mode();
689 assert_eq!(state.parse_key('3'), None);
690 assert_eq!(state.parse_key('g'), None);
691 assert_eq!(state.pending_keys(), "3g");
692 }
693
694 #[test]
695 fn vim_status_line_formats_mode_command_and_pending_input() {
696 let mut state = VimState::default();
697 assert_eq!(
698 state.status_line_text(),
699 ("-- NORMAL --".to_owned(), String::new())
700 );
701
702 let _ = state.parse_key('i');
703 assert_eq!(
704 state.status_line_text(),
705 ("-- INSERT --".to_owned(), String::new())
706 );
707 let _ = state.parse_key('\u{1b}');
708
709 let _ = state.parse_key('5');
710 let _ = state.parse_key('d');
711 assert_eq!(
712 state.status_line_text(),
713 ("-- NORMAL --".to_owned(), "5d".to_owned())
714 );
715
716 state.enter_clean_normal_mode();
717 let _ = state.parse_key('/');
718 let _ = state.parse_key('f');
719 let _ = state.parse_key('o');
720 let _ = state.parse_key('o');
721 assert_eq!(
722 state.status_line_text(),
723 ("/foo".to_owned(), String::new())
724 );
725
726 let _ = state.parse_key('\u{1b}');
727 let _ = state.parse_key('v');
728 assert_eq!(
729 state.status_line_text(),
730 ("-- VISUAL --".to_owned(), String::new())
731 );
732
733 let _ = state.parse_key('V');
734 assert_eq!(
735 state.status_line_text(),
736 ("-- VISUAL LINE --".to_owned(), String::new())
737 );
738 }
739}