1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2
3use super::{key_to_action, Action};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum VimMode {
7 Normal,
8 Insert,
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12enum PendingKey {
13 None,
14 G,
15 D,
16 C,
17}
18
19#[derive(Debug, Clone)]
20pub struct VimState {
21 pub mode: VimMode,
22 pending: PendingKey,
23}
24
25impl VimState {
26 pub fn new() -> Self {
27 Self {
28 mode: VimMode::Normal,
29 pending: PendingKey::None,
30 }
31 }
32
33 pub fn cancel_insert(&mut self) {
36 self.mode = VimMode::Normal;
37 }
38}
39
40impl Default for VimState {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46fn is_global_shortcut(key: &KeyEvent) -> bool {
48 let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
49 let alt = key.modifiers.contains(KeyModifiers::ALT);
50
51 if ctrl {
52 return matches!(
53 key.code,
54 KeyCode::Char('e')
55 | KeyCode::Char('z')
56 | KeyCode::Char('Z')
57 | KeyCode::Char('y')
58 | KeyCode::Char('w')
59 | KeyCode::Char('o')
60 | KeyCode::Char('s')
61 | KeyCode::Char('r')
62 | KeyCode::Char('b')
63 | KeyCode::Char('u')
64 | KeyCode::Char('c')
65 | KeyCode::Char('q')
66 | KeyCode::Left
67 | KeyCode::Right
68 );
69 }
70 if alt {
71 return matches!(
72 key.code,
73 KeyCode::Char('i')
74 | KeyCode::Char('m')
75 | KeyCode::Char('s')
76 | KeyCode::Char('u')
77 | KeyCode::Char('x')
78 | KeyCode::Up
79 | KeyCode::Down
80 );
81 }
82 matches!(key.code, KeyCode::F(1) | KeyCode::Tab | KeyCode::BackTab)
83}
84
85pub fn vim_key_to_action(key: KeyEvent, state: &mut VimState) -> Action {
87 if is_global_shortcut(&key) {
88 state.pending = PendingKey::None;
89 return key_to_action(key);
90 }
91
92 match state.mode {
93 VimMode::Insert => vim_insert_action(key, state),
94 VimMode::Normal => vim_normal_action(key, state),
95 }
96}
97
98fn vim_insert_action(key: KeyEvent, state: &mut VimState) -> Action {
99 if key.code == KeyCode::Esc {
100 state.mode = VimMode::Normal;
101 return Action::EnterNormalMode;
102 }
103 key_to_action(key)
104}
105
106fn vim_normal_action(key: KeyEvent, state: &mut VimState) -> Action {
107 match state.pending {
109 PendingKey::G => {
110 state.pending = PendingKey::None;
111 return match key.code {
112 KeyCode::Char('g') => Action::MoveToFirstLine,
113 _ => Action::None,
114 };
115 }
116 PendingKey::D => {
117 state.pending = PendingKey::None;
118 return match key.code {
119 KeyCode::Char('d') => Action::DeleteLine,
120 _ => Action::None,
121 };
122 }
123 PendingKey::C => {
124 state.pending = PendingKey::None;
125 return match key.code {
126 KeyCode::Char('c') => {
127 state.mode = VimMode::Insert;
128 Action::ChangeLine
129 }
130 _ => Action::None,
131 };
132 }
133 PendingKey::None => {}
134 }
135
136 match key.code {
137 KeyCode::Char('i') => {
139 state.mode = VimMode::Insert;
140 Action::EnterInsertMode
141 }
142 KeyCode::Char('a') => {
143 state.mode = VimMode::Insert;
144 Action::EnterInsertModeAppend
145 }
146 KeyCode::Char('I') => {
147 state.mode = VimMode::Insert;
148 Action::EnterInsertModeLineStart
149 }
150 KeyCode::Char('A') => {
151 state.mode = VimMode::Insert;
152 Action::EnterInsertModeLineEnd
153 }
154 KeyCode::Char('o') => {
155 state.mode = VimMode::Insert;
156 Action::OpenLineBelow
157 }
158 KeyCode::Char('O') => {
159 state.mode = VimMode::Insert;
160 Action::OpenLineAbove
161 }
162
163 KeyCode::Char('h') | KeyCode::Left => Action::MoveCursorLeft,
165 KeyCode::Char('l') | KeyCode::Right => Action::MoveCursorRight,
166 KeyCode::Char('j') | KeyCode::Down => Action::ScrollDown,
167 KeyCode::Char('k') | KeyCode::Up => Action::ScrollUp,
168 KeyCode::Char('w') => Action::MoveCursorWordRight,
169 KeyCode::Char('b') => Action::MoveCursorWordLeft,
170 KeyCode::Char('e') => Action::MoveCursorWordForwardEnd,
171 KeyCode::Char('0') => Action::MoveCursorHome,
172 KeyCode::Char('^') => Action::MoveToFirstNonBlank,
173 KeyCode::Char('$') => Action::MoveCursorEnd,
174 KeyCode::Char('G') => Action::MoveToLastLine,
175 KeyCode::Char('g') => {
176 state.pending = PendingKey::G;
177 Action::None
178 }
179 KeyCode::Home => Action::MoveCursorHome,
180 KeyCode::End => Action::MoveCursorEnd,
181
182 KeyCode::Char('x') => Action::DeleteCharAtCursor,
184 KeyCode::Char('d') => {
185 state.pending = PendingKey::D;
186 Action::None
187 }
188 KeyCode::Char('c') => {
189 state.pending = PendingKey::C;
190 Action::None
191 }
192 KeyCode::Char('u') => Action::Undo,
193 KeyCode::Char('p') => Action::PasteClipboard,
194
195 KeyCode::Esc => Action::Quit,
197
198 _ => Action::None,
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205 use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
206
207 fn key(code: KeyCode) -> KeyEvent {
208 KeyEvent {
209 code,
210 modifiers: KeyModifiers::NONE,
211 kind: KeyEventKind::Press,
212 state: KeyEventState::NONE,
213 }
214 }
215
216 fn key_ctrl(code: KeyCode) -> KeyEvent {
217 KeyEvent {
218 code,
219 modifiers: KeyModifiers::CONTROL,
220 kind: KeyEventKind::Press,
221 state: KeyEventState::NONE,
222 }
223 }
224
225 #[test]
226 fn test_starts_in_normal_mode() {
227 let state = VimState::new();
228 assert_eq!(state.mode, VimMode::Normal);
229 }
230
231 #[test]
232 fn test_i_enters_insert_mode() {
233 let mut state = VimState::new();
234 let action = vim_key_to_action(key(KeyCode::Char('i')), &mut state);
235 assert_eq!(action, Action::EnterInsertMode);
236 assert_eq!(state.mode, VimMode::Insert);
237 }
238
239 #[test]
240 fn test_esc_in_insert_returns_to_normal() {
241 let mut state = VimState::new();
242 state.mode = VimMode::Insert;
243 let action = vim_key_to_action(key(KeyCode::Esc), &mut state);
244 assert_eq!(action, Action::EnterNormalMode);
245 assert_eq!(state.mode, VimMode::Normal);
246 }
247
248 #[test]
249 fn test_esc_in_normal_quits() {
250 let mut state = VimState::new();
251 let action = vim_key_to_action(key(KeyCode::Esc), &mut state);
252 assert_eq!(action, Action::Quit);
253 }
254
255 #[test]
256 fn test_hjkl_motions() {
257 let mut state = VimState::new();
258 assert_eq!(
259 vim_key_to_action(key(KeyCode::Char('h')), &mut state),
260 Action::MoveCursorLeft
261 );
262 assert_eq!(
263 vim_key_to_action(key(KeyCode::Char('j')), &mut state),
264 Action::ScrollDown
265 );
266 assert_eq!(
267 vim_key_to_action(key(KeyCode::Char('k')), &mut state),
268 Action::ScrollUp
269 );
270 assert_eq!(
271 vim_key_to_action(key(KeyCode::Char('l')), &mut state),
272 Action::MoveCursorRight
273 );
274 }
275
276 #[test]
277 fn test_word_motions() {
278 let mut state = VimState::new();
279 assert_eq!(
280 vim_key_to_action(key(KeyCode::Char('w')), &mut state),
281 Action::MoveCursorWordRight
282 );
283 assert_eq!(
284 vim_key_to_action(key(KeyCode::Char('b')), &mut state),
285 Action::MoveCursorWordLeft
286 );
287 assert_eq!(
288 vim_key_to_action(key(KeyCode::Char('e')), &mut state),
289 Action::MoveCursorWordForwardEnd
290 );
291 }
292
293 #[test]
294 fn test_gg_goes_to_first_line() {
295 let mut state = VimState::new();
296 let a1 = vim_key_to_action(key(KeyCode::Char('g')), &mut state);
297 assert_eq!(a1, Action::None);
298 let a2 = vim_key_to_action(key(KeyCode::Char('g')), &mut state);
299 assert_eq!(a2, Action::MoveToFirstLine);
300 }
301
302 #[test]
303 fn test_g_then_non_g_cancels() {
304 let mut state = VimState::new();
305 vim_key_to_action(key(KeyCode::Char('g')), &mut state);
306 let action = vim_key_to_action(key(KeyCode::Char('x')), &mut state);
307 assert_eq!(action, Action::None);
308 }
309
310 #[test]
311 fn test_dd_deletes_line() {
312 let mut state = VimState::new();
313 let a1 = vim_key_to_action(key(KeyCode::Char('d')), &mut state);
314 assert_eq!(a1, Action::None);
315 let a2 = vim_key_to_action(key(KeyCode::Char('d')), &mut state);
316 assert_eq!(a2, Action::DeleteLine);
317 }
318
319 #[test]
320 fn test_d_then_non_d_cancels() {
321 let mut state = VimState::new();
322 vim_key_to_action(key(KeyCode::Char('d')), &mut state);
323 let action = vim_key_to_action(key(KeyCode::Char('j')), &mut state);
324 assert_eq!(action, Action::None);
325 }
326
327 #[test]
328 fn test_cc_changes_line() {
329 let mut state = VimState::new();
330 let a1 = vim_key_to_action(key(KeyCode::Char('c')), &mut state);
331 assert_eq!(a1, Action::None);
332 let a2 = vim_key_to_action(key(KeyCode::Char('c')), &mut state);
333 assert_eq!(a2, Action::ChangeLine);
334 assert_eq!(state.mode, VimMode::Insert);
335 }
336
337 #[test]
338 fn test_x_deletes_char() {
339 let mut state = VimState::new();
340 assert_eq!(
341 vim_key_to_action(key(KeyCode::Char('x')), &mut state),
342 Action::DeleteCharAtCursor
343 );
344 }
345
346 #[test]
347 fn test_global_shortcuts_bypass_vim() {
348 let mut state = VimState::new();
349 let action = vim_key_to_action(key_ctrl(KeyCode::Char('e')), &mut state);
350 assert_eq!(action, Action::SwitchEngine);
351 assert_eq!(state.mode, VimMode::Normal);
352 }
353
354 #[test]
355 fn test_global_shortcut_clears_pending() {
356 let mut state = VimState::new();
357 vim_key_to_action(key(KeyCode::Char('d')), &mut state);
358 let action = vim_key_to_action(key_ctrl(KeyCode::Char('e')), &mut state);
359 assert_eq!(action, Action::SwitchEngine);
360 }
361
362 #[test]
363 fn test_insert_mode_types_chars() {
364 let mut state = VimState::new();
365 state.mode = VimMode::Insert;
366 let action = vim_key_to_action(key(KeyCode::Char('h')), &mut state);
367 assert_eq!(action, Action::InsertChar('h'));
368 }
369
370 #[test]
371 fn test_a_enters_insert_append() {
372 let mut state = VimState::new();
373 let action = vim_key_to_action(key(KeyCode::Char('a')), &mut state);
374 assert_eq!(action, Action::EnterInsertModeAppend);
375 assert_eq!(state.mode, VimMode::Insert);
376 }
377
378 #[test]
379 fn test_tab_bypasses_vim() {
380 let mut state = VimState::new();
381 let action = vim_key_to_action(key(KeyCode::Tab), &mut state);
382 assert_eq!(action, Action::SwitchPanel);
383 }
384
385 #[test]
386 fn test_u_is_undo_in_normal() {
387 let mut state = VimState::new();
388 assert_eq!(
389 vim_key_to_action(key(KeyCode::Char('u')), &mut state),
390 Action::Undo
391 );
392 }
393
394 #[test]
395 fn test_o_opens_line_and_enters_insert() {
396 let mut state = VimState::new();
397 let action = vim_key_to_action(key(KeyCode::Char('o')), &mut state);
398 assert_eq!(action, Action::OpenLineBelow);
399 assert_eq!(state.mode, VimMode::Insert);
400 }
401}