use std::thread; use std::time::Duration; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}; use evil_keys::{DispatchResult, Dispatcher, KeyTrie}; fn press(code: KeyCode) -> KeyEvent { KeyEvent { code, modifiers: KeyModifiers::NONE, kind: KeyEventKind::Press, state: KeyEventState::NONE, } } fn press_char(c: char) -> KeyEvent { press(KeyCode::Char(c)) } fn press_spc() -> KeyEvent { press_char(' ') } fn press_esc() -> KeyEvent { press(KeyCode::Esc) } fn vim_dispatcher() -> Dispatcher<&'static str> { let mut trie = KeyTrie::new("normal"); trie.bind("j", "move_down").unwrap(); trie.bind("k", "move_up").unwrap(); trie.bind("G", "goto_last").unwrap(); trie.bind("Esc", "escape").unwrap(); trie.bind("0", "goto_start").unwrap(); trie.group("g", "+goto", |g| { g.bind("g", "goto_first")?; g.bind("t", "next_tab")?; g.bind("T", "prev_tab")?; Ok(()) }) .unwrap(); trie.group("SPC", "+leader", |g| { g.group("b", "+buffer", |b| { b.bind_desc("l", "goto_library", "library")?; b.bind_desc("w", "goto_wanted", "wanted")?; Ok(()) })?; g.bind_desc("h", "show_help", "help")?; g.bind_desc("q", "quit", "quit")?; g.bind_desc("n", "notifications", "notifications")?; Ok(()) }) .unwrap(); let insert = KeyTrie::new("insert"); let mut d = Dispatcher::new(); d.add_mode("normal", trie).unwrap(); d.add_mode("insert", insert).unwrap(); d.set_active("normal").unwrap(); d } #[test] fn vim_workflow_5gg() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_char('5')), DispatchResult::CountAccumulated ); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!( d.dispatch(press_char('g')), DispatchResult::Matched { action: "goto_first", count: 5 } ); } #[test] fn leader_spc_b_l() { let mut d = vim_dispatcher(); assert_eq!(d.dispatch(press_spc()), DispatchResult::Pending); assert_eq!(d.dispatch(press_char('b')), DispatchResult::Pending); assert_eq!( d.dispatch(press_char('l')), DispatchResult::Matched { action: "goto_library", count: 1 } ); } #[test] fn escape_progression() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_char('5')), DispatchResult::CountAccumulated ); assert_eq!(d.dispatch(press_esc()), DispatchResult::Cancelled); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!(d.dispatch(press_esc()), DispatchResult::Cancelled); assert_eq!( d.dispatch(press_char('3')), DispatchResult::CountAccumulated ); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!(d.dispatch(press_esc()), DispatchResult::Cancelled); assert_eq!( d.dispatch(press_char('j')), DispatchResult::Matched { action: "move_down", count: 1 } ); } #[test] fn timeout_resets_sequence() { let mut d = vim_dispatcher(); d.set_timeout(Duration::from_millis(50)); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); thread::sleep(Duration::from_millis(80)); assert!(d.check_timeout()); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!( d.dispatch(press_char('g')), DispatchResult::Matched { action: "goto_first", count: 1 } ); } #[test] fn timeout_clears_count_too() { let mut d = vim_dispatcher(); d.set_timeout(Duration::from_millis(50)); assert_eq!( d.dispatch(press_char('5')), DispatchResult::CountAccumulated ); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); thread::sleep(Duration::from_millis(80)); assert!(d.check_timeout()); assert_eq!( d.dispatch(press_char('j')), DispatchResult::Matched { action: "move_down", count: 1 } ); } #[test] fn mode_switch_mid_sequence() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_char('3')), DispatchResult::CountAccumulated ); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); d.set_active("insert").unwrap(); d.set_active("normal").unwrap(); assert_eq!( d.dispatch(press_char('j')), DispatchResult::Matched { action: "move_down", count: 1 } ); } #[test] fn which_key_after_leader() { let mut d = vim_dispatcher(); assert_eq!(d.dispatch(press_spc()), DispatchResult::Pending); let entries = d.which_key_entries().unwrap(); assert!(entries.iter().any(|e| e.key == "b" && e.is_group)); assert!(entries .iter() .any(|e| e.key == "h" && e.description == "help")); assert!(entries .iter() .any(|e| e.key == "q" && e.description == "quit")); assert!(entries .iter() .any(|e| e.key == "n" && e.description == "notifications")); assert_eq!(d.dispatch(press_char('b')), DispatchResult::Pending); let entries = d.which_key_entries().unwrap(); assert!(entries .iter() .any(|e| e.key == "l" && e.description == "library")); assert!(entries .iter() .any(|e| e.key == "w" && e.description == "wanted")); assert_eq!( d.dispatch(press_char('l')), DispatchResult::Matched { action: "goto_library", count: 1 } ); assert!(d.which_key_entries().is_none()); } #[test] fn stress_rapid_fire() { let mut d = vim_dispatcher(); for _ in 0..1000 { assert_eq!( d.dispatch(press_char('j')), DispatchResult::Matched { action: "move_down", count: 1 } ); } assert!(d.pending_keys().is_empty()); } #[test] fn stress_alternating_sequences() { let mut d = vim_dispatcher(); for _ in 0..100 { assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!( d.dispatch(press_char('g')), DispatchResult::Matched { action: "goto_first", count: 1 } ); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!( d.dispatch(press_char('t')), DispatchResult::Matched { action: "next_tab", count: 1 } ); } } #[test] fn count_123_j() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_char('1')), DispatchResult::CountAccumulated ); assert_eq!( d.dispatch(press_char('2')), DispatchResult::CountAccumulated ); assert_eq!( d.dispatch(press_char('3')), DispatchResult::CountAccumulated ); assert_eq!( d.dispatch(press_char('j')), DispatchResult::Matched { action: "move_down", count: 123 } ); } #[test] fn zero_as_binding_not_count() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_char('0')), DispatchResult::Matched { action: "goto_start", count: 1 } ); } #[test] fn zero_after_digit_is_count() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_char('1')), DispatchResult::CountAccumulated ); assert_eq!( d.dispatch(press_char('0')), DispatchResult::CountAccumulated ); assert_eq!( d.dispatch(press_char('j')), DispatchResult::Matched { action: "move_down", count: 10 } ); } #[test] fn count_overflow_saturates() { let mut d = vim_dispatcher(); for _ in 0..20 { assert_eq!( d.dispatch(press_char('9')), DispatchResult::CountAccumulated ); } let result = d.dispatch(press_char('j')); assert!(matches!( result, DispatchResult::Matched { action: "move_down", count } if count == usize::MAX )); } #[test] fn pending_display_shows_keys() { let mut d = vim_dispatcher(); assert_eq!(d.pending_display(), ""); d.dispatch(press_spc()); assert_eq!(d.pending_display(), "SPC"); d.dispatch(press_char('b')); assert_eq!(d.pending_display(), "SPC b"); } #[test] fn escape_with_nothing_pending_matches_binding() { let mut d = vim_dispatcher(); assert_eq!( d.dispatch(press_esc()), DispatchResult::Matched { action: "escape", count: 1 } ); } #[test] fn invalid_mode() { let mut d = vim_dispatcher(); assert!(d.set_active("nonexistent").is_err()); } #[test] fn dispatch_with_no_modes() { let mut d: Dispatcher<&str> = Dispatcher::new(); assert_eq!(d.dispatch(press_char('j')), DispatchResult::NotFound); } #[test] fn key_release_ignored() { let mut d = vim_dispatcher(); let release = KeyEvent { code: KeyCode::Char('j'), modifiers: KeyModifiers::NONE, kind: KeyEventKind::Release, state: KeyEventState::NONE, }; assert_eq!(d.dispatch(release), DispatchResult::Ignored); } #[test] fn key_repeat_ignored() { let mut d = vim_dispatcher(); let repeat = KeyEvent { code: KeyCode::Char('j'), modifiers: KeyModifiers::NONE, kind: KeyEventKind::Repeat, state: KeyEventState::NONE, }; assert_eq!(d.dispatch(repeat), DispatchResult::Ignored); } #[test] fn wrong_key_mid_sequence_resets() { let mut d = vim_dispatcher(); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); assert_eq!(d.dispatch(press_char('h')), DispatchResult::NotFound); assert_eq!(d.dispatch(press_char('g')), DispatchResult::Pending); }