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