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