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