1use hjkl_keymap::{KeyCode, KeyEvent, KeyModifiers};
18
19use crate::Mode;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct VimDescriptor {
26 pub key: KeyEvent,
28 pub desc: Option<&'static str>,
31}
32
33impl VimDescriptor {
34 fn char(c: char, desc: &'static str) -> Self {
35 Self {
36 key: KeyEvent::char(c),
37 desc: Some(desc),
38 }
39 }
40
41 fn ctrl(c: char, desc: &'static str) -> Self {
42 Self {
43 key: KeyEvent::ctrl(c),
44 desc: Some(desc),
45 }
46 }
47
48 fn prefix(c: char) -> Self {
49 Self {
50 key: KeyEvent::char(c),
51 desc: None,
52 }
53 }
54}
55
56pub fn children_for(mode: Mode, prefix: &[KeyEvent]) -> Vec<VimDescriptor> {
70 match mode {
71 Mode::Normal => children_normal(prefix),
72 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => children_visual(prefix),
73 Mode::OpPending => children_op_pending(prefix),
74 Mode::Insert | Mode::CommandLine => vec![],
75 }
76}
77
78pub const COUNT_NORMAL_ROOT: usize = 84;
82pub const COUNT_G_PREFIX: usize = 23;
84pub const COUNT_Z_PREFIX: usize = 11;
86pub const COUNT_OP_PENDING_ROOT: usize = 25;
88
89fn children_normal(prefix: &[KeyEvent]) -> Vec<VimDescriptor> {
92 if prefix.is_empty() {
93 return normal_root();
94 }
95 if prefix.len() == 1 {
97 let k = prefix[0];
98 if k == KeyEvent::char('g') {
99 return g_prefix();
100 }
101 if k == KeyEvent::char('z') {
102 return z_prefix();
103 }
104 if k == KeyEvent::char('d')
106 || k == KeyEvent::char('c')
107 || k == KeyEvent::char('y')
108 || k == KeyEvent::char('>')
109 || k == KeyEvent::char('<')
110 || k == KeyEvent::char('=')
111 {
112 return op_pending_root();
113 }
114 }
115 vec![]
116}
117
118fn children_visual(prefix: &[KeyEvent]) -> Vec<VimDescriptor> {
119 if prefix.is_empty() {
120 return visual_root();
121 }
122 if prefix.len() == 1 && prefix[0] == KeyEvent::char('z') {
123 return z_prefix();
124 }
125 if prefix.len() == 1 && prefix[0] == KeyEvent::char('g') {
126 return g_prefix();
129 }
130 vec![]
131}
132
133fn children_op_pending(prefix: &[KeyEvent]) -> Vec<VimDescriptor> {
134 if prefix.is_empty() {
135 return op_pending_root();
136 }
137 vec![]
138}
139
140fn normal_root() -> Vec<VimDescriptor> {
143 vec![
147 VimDescriptor::char('i', "insert before cursor"),
149 VimDescriptor::char('I', "insert at line start"),
150 VimDescriptor::char('a', "append after cursor"),
151 VimDescriptor::char('A', "append at line end"),
152 VimDescriptor::char('o', "open line below"),
153 VimDescriptor::char('O', "open line above"),
154 VimDescriptor::char('R', "enter replace mode"),
155 VimDescriptor::char('s', "substitute char"),
156 VimDescriptor::char('S', "substitute line"),
157 VimDescriptor::prefix('d'),
159 VimDescriptor::prefix('c'),
160 VimDescriptor::prefix('y'),
161 VimDescriptor::char('x', "delete char forward"),
162 VimDescriptor::char('X', "delete char backward"),
163 VimDescriptor::char('D', "delete to end of line"),
164 VimDescriptor::char('C', "change to end of line"),
165 VimDescriptor::char('Y', "yank to end of line"),
166 VimDescriptor::char('p', "paste after"),
168 VimDescriptor::char('P', "paste before"),
169 VimDescriptor::char('u', "undo"),
170 VimDescriptor::ctrl('r', "redo"),
171 VimDescriptor::char('~', "toggle case at cursor"),
173 VimDescriptor::char('J', "join line below"),
174 VimDescriptor::char('r', "replace character"),
175 VimDescriptor::char('.', "repeat last change"),
176 VimDescriptor::prefix('>'),
178 VimDescriptor::prefix('<'),
179 VimDescriptor::prefix('='),
180 VimDescriptor::char('h', "left"),
182 VimDescriptor::char('j', "down"),
183 VimDescriptor::char('k', "up"),
184 VimDescriptor::char('l', "right"),
185 VimDescriptor::char('w', "word forward"),
186 VimDescriptor::char('W', "WORD forward"),
187 VimDescriptor::char('b', "word backward"),
188 VimDescriptor::char('B', "WORD backward"),
189 VimDescriptor::char('e', "word end"),
190 VimDescriptor::char('E', "WORD end"),
191 VimDescriptor::char('0', "line start"),
192 VimDescriptor::char('^', "first non-blank"),
193 VimDescriptor::char('$', "line end"),
194 VimDescriptor::char('G', "file bottom / go to line"),
195 VimDescriptor::char('%', "match bracket"),
196 VimDescriptor::char('H', "viewport top"),
197 VimDescriptor::char('M', "viewport middle"),
198 VimDescriptor::char('L', "viewport bottom"),
199 VimDescriptor::char('{', "paragraph prev"),
200 VimDescriptor::char('}', "paragraph next"),
201 VimDescriptor::char('(', "sentence prev"),
202 VimDescriptor::char(')', "sentence next"),
203 VimDescriptor::char('|', "goto column"),
204 VimDescriptor::char('n', "search next"),
205 VimDescriptor::char('N', "search prev"),
206 VimDescriptor::char('*', "search word forward"),
207 VimDescriptor::char('#', "search word backward"),
208 VimDescriptor::char(';', "repeat find"),
209 VimDescriptor::char(',', "repeat find reverse"),
210 VimDescriptor::char('f', "find char forward"),
212 VimDescriptor::char('F', "find char backward"),
213 VimDescriptor::char('t', "till char forward"),
214 VimDescriptor::char('T', "till char backward"),
215 VimDescriptor::prefix('g'),
217 VimDescriptor::prefix('z'),
218 VimDescriptor::char('m', "set mark"),
220 VimDescriptor::char('\'', "goto mark (linewise)"),
221 VimDescriptor::char('`', "goto mark (charwise)"),
222 VimDescriptor::char('"', "select register"),
224 VimDescriptor::char('@', "play macro"),
225 VimDescriptor::char('q', "record macro"),
226 VimDescriptor::ctrl('d', "scroll half-page down"),
228 VimDescriptor::ctrl('u', "scroll half-page up"),
229 VimDescriptor::ctrl('f', "scroll full-page down"),
230 VimDescriptor::ctrl('b', "scroll full-page up"),
231 VimDescriptor::ctrl('e', "scroll line down"),
232 VimDescriptor::ctrl('y', "scroll line up"),
233 VimDescriptor::ctrl('a', "increment number"),
235 VimDescriptor::ctrl('x', "decrement number"),
236 VimDescriptor::ctrl('o', "jump back"),
238 VimDescriptor::ctrl('i', "jump forward"),
239 VimDescriptor::char('v', "enter visual mode"),
241 VimDescriptor::char('V', "enter visual-line mode"),
242 VimDescriptor {
243 key: KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CTRL),
244 desc: Some("enter visual-block mode"),
245 },
246 VimDescriptor::char('/', "search forward"),
248 VimDescriptor::char('?', "search backward"),
249 ]
250}
251
252fn visual_root() -> Vec<VimDescriptor> {
253 vec![
254 VimDescriptor::char('h', "left"),
256 VimDescriptor::char('j', "down"),
257 VimDescriptor::char('k', "up"),
258 VimDescriptor::char('l', "right"),
259 VimDescriptor::char('w', "word forward"),
260 VimDescriptor::char('b', "word backward"),
261 VimDescriptor::char('e', "word end"),
262 VimDescriptor::char('0', "line start"),
263 VimDescriptor::char('$', "line end"),
264 VimDescriptor::char('G', "file bottom"),
265 VimDescriptor::char('%', "match bracket"),
266 VimDescriptor::char('n', "search next"),
267 VimDescriptor::char('N', "search prev"),
268 VimDescriptor::char('d', "delete selection"),
270 VimDescriptor::char('c', "change selection"),
271 VimDescriptor::char('y', "yank selection"),
272 VimDescriptor::char('x', "delete selection"),
273 VimDescriptor::char('s', "substitute selection"),
274 VimDescriptor::char('U', "uppercase selection"),
275 VimDescriptor::char('u', "lowercase selection"),
276 VimDescriptor::char('~', "toggle case selection"),
277 VimDescriptor::char('>', "indent selection"),
278 VimDescriptor::char('<', "outdent selection"),
279 VimDescriptor::char('=', "auto-indent selection"),
280 VimDescriptor::char('o', "swap anchor and cursor"),
281 VimDescriptor::char('i', "inner text object"),
283 VimDescriptor::char('a', "around text object"),
284 VimDescriptor::prefix('z'),
286 VimDescriptor::char('`', "goto mark (charwise)"),
288 VimDescriptor::char('g', "g-prefix (gc = toggle comment)"),
290 ]
291}
292
293fn g_prefix() -> Vec<VimDescriptor> {
294 vec![
296 VimDescriptor::char('g', "go to first line"),
297 VimDescriptor::char('e', "word end backward"),
298 VimDescriptor::char('E', "WORD end backward"),
299 VimDescriptor::char('_', "last non-blank on line"),
300 VimDescriptor::char('M', "middle of line"),
301 VimDescriptor::char('v', "reselect last visual"),
302 VimDescriptor::char('j', "display-line down"),
303 VimDescriptor::char('k', "display-line up"),
304 VimDescriptor::char('U', "uppercase operator"),
305 VimDescriptor::char('u', "lowercase operator"),
306 VimDescriptor::char('~', "toggle case operator"),
307 VimDescriptor::char('q', "reflow operator"),
308 VimDescriptor::char('w', "reflow operator (keep cursor)"),
309 VimDescriptor::char('J', "join without space"),
310 VimDescriptor::char('d', "goto definition"),
311 VimDescriptor::char('i', "goto last insert position"),
312 VimDescriptor::char(';', "goto older change"),
313 VimDescriptor::char(',', "goto newer change"),
314 VimDescriptor::char('*', "search word (partial) forward"),
315 VimDescriptor::char('#', "search word (partial) backward"),
316 VimDescriptor::char('c', "toggle comment operator"),
317 VimDescriptor::char('-', "undo time-travel (earlier)"),
318 VimDescriptor::char('+', "undo time-travel (later)"),
319 ]
320}
321
322fn z_prefix() -> Vec<VimDescriptor> {
323 vec![
325 VimDescriptor::char('z', "center cursor line"),
326 VimDescriptor::char('t', "cursor line to top"),
327 VimDescriptor::char('b', "cursor line to bottom"),
328 VimDescriptor::char('o', "open fold"),
329 VimDescriptor::char('c', "close fold"),
330 VimDescriptor::char('a', "toggle fold"),
331 VimDescriptor::char('R', "open all folds"),
332 VimDescriptor::char('M', "close all folds"),
333 VimDescriptor::char('E', "clear all folds"),
334 VimDescriptor::char('d', "delete fold at cursor"),
335 VimDescriptor::char('f', "create fold (visual/motion)"),
336 ]
337}
338
339fn op_pending_root() -> Vec<VimDescriptor> {
340 vec![
343 VimDescriptor::char('h', "left"),
344 VimDescriptor::char('j', "down"),
345 VimDescriptor::char('k', "up"),
346 VimDescriptor::char('l', "right"),
347 VimDescriptor::char('w', "word forward"),
348 VimDescriptor::char('W', "WORD forward"),
349 VimDescriptor::char('b', "word backward"),
350 VimDescriptor::char('B', "WORD backward"),
351 VimDescriptor::char('e', "word end"),
352 VimDescriptor::char('E', "WORD end"),
353 VimDescriptor::char('0', "line start"),
354 VimDescriptor::char('^', "first non-blank"),
355 VimDescriptor::char('$', "line end"),
356 VimDescriptor::char('G', "file bottom"),
357 VimDescriptor::char('%', "match bracket"),
358 VimDescriptor::char('n', "search next"),
359 VimDescriptor::char('N', "search prev"),
360 VimDescriptor::char('f', "find char forward"),
361 VimDescriptor::char('F', "find char backward"),
362 VimDescriptor::char('t', "till char forward"),
363 VimDescriptor::char('T', "till char backward"),
364 VimDescriptor::char('|', "goto column"),
365 VimDescriptor::char('i', "inner text object"),
367 VimDescriptor::char('a', "around text object"),
368 VimDescriptor::prefix('g'),
370 ]
371}
372
373#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
382 fn normal_root_count_matches_expected() {
383 let got = children_for(Mode::Normal, &[]);
384 assert_eq!(
385 got.len(),
386 COUNT_NORMAL_ROOT,
387 "normal root count drifted: got {}, expected {}",
388 got.len(),
389 COUNT_NORMAL_ROOT
390 );
391 }
392
393 #[test]
394 fn normal_root_includes_basic_motions() {
395 let got = children_for(Mode::Normal, &[]);
396 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
397 for ch in ['h', 'j', 'k', 'l'] {
398 assert!(
399 keys.contains(&KeyEvent::char(ch)),
400 "normal root missing '{ch}'"
401 );
402 }
403 }
404
405 #[test]
406 fn normal_root_includes_insert_entries() {
407 let got = children_for(Mode::Normal, &[]);
408 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
409 for ch in ['i', 'a', 'I', 'A', 'o', 'O'] {
410 assert!(
411 keys.contains(&KeyEvent::char(ch)),
412 "normal root missing insert entry '{ch}'"
413 );
414 }
415 }
416
417 #[test]
418 fn normal_root_g_and_z_are_prefix_nodes() {
419 let got = children_for(Mode::Normal, &[]);
420 let g = got.iter().find(|d| d.key == KeyEvent::char('g')).unwrap();
421 let z = got.iter().find(|d| d.key == KeyEvent::char('z')).unwrap();
422 assert_eq!(g.desc, None, "g should be a prefix node (desc = None)");
423 assert_eq!(z.desc, None, "z should be a prefix node (desc = None)");
424 }
425
426 #[test]
427 fn normal_root_operator_prefixes_are_prefix_nodes() {
428 let got = children_for(Mode::Normal, &[]);
429 for ch in ['d', 'c', 'y'] {
430 let entry = got
431 .iter()
432 .find(|d| d.key == KeyEvent::char(ch))
433 .unwrap_or_else(|| panic!("normal root missing operator prefix '{ch}'"));
434 assert_eq!(
435 entry.desc, None,
436 "operator '{ch}' should be a prefix node (desc = None)"
437 );
438 }
439 }
440
441 #[test]
442 fn normal_root_has_ctrl_scroll_keys() {
443 let got = children_for(Mode::Normal, &[]);
444 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
445 for ch in ['d', 'u', 'f', 'b', 'e', 'y'] {
446 assert!(
447 keys.contains(&KeyEvent::ctrl(ch)),
448 "normal root missing <C-{ch}>"
449 );
450 }
451 }
452
453 #[test]
456 fn g_prefix_count_matches_expected() {
457 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
458 assert_eq!(
459 got.len(),
460 COUNT_G_PREFIX,
461 "g-prefix count drifted: got {}, expected {}",
462 got.len(),
463 COUNT_G_PREFIX
464 );
465 }
466
467 #[test]
468 fn g_prefix_includes_gg() {
469 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
470 let found = got
471 .iter()
472 .any(|d| d.key == KeyEvent::char('g') && d.desc.is_some());
473 assert!(found, "g-prefix missing gg");
474 }
475
476 #[test]
477 fn g_prefix_includes_gj_gk() {
478 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
479 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
480 assert!(keys.contains(&KeyEvent::char('j')), "g-prefix missing gj");
481 assert!(keys.contains(&KeyEvent::char('k')), "g-prefix missing gk");
482 }
483
484 #[test]
485 fn g_prefix_includes_gd() {
486 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
487 let found = got.iter().any(|d| d.key == KeyEvent::char('d'));
488 assert!(found, "g-prefix missing gd (goto definition)");
489 }
490
491 #[test]
492 fn g_prefix_includes_case_operators() {
493 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
494 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
495 for ch in ['U', 'u', '~', 'q'] {
496 assert!(keys.contains(&KeyEvent::char(ch)), "g-prefix missing g{ch}");
497 }
498 }
499
500 #[test]
503 fn z_prefix_count_matches_expected() {
504 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
505 assert_eq!(
506 got.len(),
507 COUNT_Z_PREFIX,
508 "z-prefix count drifted: got {}, expected {}",
509 got.len(),
510 COUNT_Z_PREFIX
511 );
512 }
513
514 #[test]
515 fn z_prefix_includes_zz() {
516 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
517 let found = got
518 .iter()
519 .any(|d| d.key == KeyEvent::char('z') && d.desc.is_some());
520 assert!(found, "z-prefix missing zz");
521 }
522
523 #[test]
524 fn z_prefix_includes_zt_zb() {
525 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
526 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
527 assert!(keys.contains(&KeyEvent::char('t')), "z-prefix missing zt");
528 assert!(keys.contains(&KeyEvent::char('b')), "z-prefix missing zb");
529 }
530
531 #[test]
532 fn z_prefix_includes_fold_ops() {
533 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
534 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
535 for ch in ['o', 'c', 'a', 'R', 'M', 'E', 'd', 'f'] {
536 assert!(keys.contains(&KeyEvent::char(ch)), "z-prefix missing z{ch}");
537 }
538 }
539
540 #[test]
543 fn op_pending_root_count_matches_expected() {
544 let got = children_for(Mode::Normal, &[KeyEvent::char('d')]);
545 assert_eq!(
546 got.len(),
547 COUNT_OP_PENDING_ROOT,
548 "op-pending root count drifted: got {}, expected {}",
549 got.len(),
550 COUNT_OP_PENDING_ROOT
551 );
552 }
553
554 #[test]
555 fn op_pending_same_for_d_c_y() {
556 let d = children_for(Mode::Normal, &[KeyEvent::char('d')]);
557 let c = children_for(Mode::Normal, &[KeyEvent::char('c')]);
558 let y = children_for(Mode::Normal, &[KeyEvent::char('y')]);
559 assert_eq!(d, c, "d and c op-pending children should match");
560 assert_eq!(d, y, "d and y op-pending children should match");
561 }
562
563 #[test]
564 fn op_pending_has_text_object_prefixes() {
565 let got = children_for(Mode::Normal, &[KeyEvent::char('d')]);
566 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
567 assert!(
568 keys.contains(&KeyEvent::char('i')),
569 "op-pending missing 'i' (inner text obj)"
570 );
571 assert!(
572 keys.contains(&KeyEvent::char('a')),
573 "op-pending missing 'a' (around text obj)"
574 );
575 }
576
577 #[test]
578 fn op_pending_has_g_sub_prefix() {
579 let got = children_for(Mode::Normal, &[KeyEvent::char('d')]);
580 let g = got
581 .iter()
582 .find(|d| d.key == KeyEvent::char('g'))
583 .expect("op-pending missing g sub-prefix");
584 assert_eq!(g.desc, None, "g in op-pending should be a prefix node");
585 }
586
587 #[test]
590 fn unknown_prefix_returns_empty() {
591 let got = children_for(Mode::Normal, &[KeyEvent::char('q')]);
594 assert!(got.is_empty(), "unknown prefix should return empty vec");
595 }
596
597 #[test]
598 fn insert_mode_always_empty() {
599 assert!(children_for(Mode::Insert, &[]).is_empty());
600 assert!(children_for(Mode::Insert, &[KeyEvent::char('g')]).is_empty());
601 }
602
603 #[test]
604 fn op_pending_mode_root_matches_normal_d_prefix() {
605 let via_normal = children_for(Mode::Normal, &[KeyEvent::char('d')]);
606 let via_op = children_for(Mode::OpPending, &[]);
607 assert_eq!(via_normal, via_op);
608 }
609
610 #[test]
613 fn visual_mode_root_non_empty() {
614 let got = children_for(Mode::Visual, &[]);
615 assert!(!got.is_empty(), "visual root should not be empty");
616 }
617
618 #[test]
619 fn visual_mode_z_prefix_same_as_normal() {
620 let vn = children_for(Mode::Visual, &[KeyEvent::char('z')]);
621 let nn = children_for(Mode::Normal, &[KeyEvent::char('z')]);
622 assert_eq!(vn, nn, "visual z-prefix should equal normal z-prefix");
623 }
624}