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