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 = 21;
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 ]
318}
319
320fn z_prefix() -> Vec<VimDescriptor> {
321 vec![
323 VimDescriptor::char('z', "center cursor line"),
324 VimDescriptor::char('t', "cursor line to top"),
325 VimDescriptor::char('b', "cursor line to bottom"),
326 VimDescriptor::char('o', "open fold"),
327 VimDescriptor::char('c', "close fold"),
328 VimDescriptor::char('a', "toggle fold"),
329 VimDescriptor::char('R', "open all folds"),
330 VimDescriptor::char('M', "close all folds"),
331 VimDescriptor::char('E', "clear all folds"),
332 VimDescriptor::char('d', "delete fold at cursor"),
333 VimDescriptor::char('f', "create fold (visual/motion)"),
334 ]
335}
336
337fn op_pending_root() -> Vec<VimDescriptor> {
338 vec![
341 VimDescriptor::char('h', "left"),
342 VimDescriptor::char('j', "down"),
343 VimDescriptor::char('k', "up"),
344 VimDescriptor::char('l', "right"),
345 VimDescriptor::char('w', "word forward"),
346 VimDescriptor::char('W', "WORD forward"),
347 VimDescriptor::char('b', "word backward"),
348 VimDescriptor::char('B', "WORD backward"),
349 VimDescriptor::char('e', "word end"),
350 VimDescriptor::char('E', "WORD end"),
351 VimDescriptor::char('0', "line start"),
352 VimDescriptor::char('^', "first non-blank"),
353 VimDescriptor::char('$', "line end"),
354 VimDescriptor::char('G', "file bottom"),
355 VimDescriptor::char('%', "match bracket"),
356 VimDescriptor::char('n', "search next"),
357 VimDescriptor::char('N', "search prev"),
358 VimDescriptor::char('f', "find char forward"),
359 VimDescriptor::char('F', "find char backward"),
360 VimDescriptor::char('t', "till char forward"),
361 VimDescriptor::char('T', "till char backward"),
362 VimDescriptor::char('|', "goto column"),
363 VimDescriptor::char('i', "inner text object"),
365 VimDescriptor::char('a', "around text object"),
366 VimDescriptor::prefix('g'),
368 ]
369}
370
371#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
380 fn normal_root_count_matches_expected() {
381 let got = children_for(Mode::Normal, &[]);
382 assert_eq!(
383 got.len(),
384 COUNT_NORMAL_ROOT,
385 "normal root count drifted: got {}, expected {}",
386 got.len(),
387 COUNT_NORMAL_ROOT
388 );
389 }
390
391 #[test]
392 fn normal_root_includes_basic_motions() {
393 let got = children_for(Mode::Normal, &[]);
394 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
395 for ch in ['h', 'j', 'k', 'l'] {
396 assert!(
397 keys.contains(&KeyEvent::char(ch)),
398 "normal root missing '{ch}'"
399 );
400 }
401 }
402
403 #[test]
404 fn normal_root_includes_insert_entries() {
405 let got = children_for(Mode::Normal, &[]);
406 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
407 for ch in ['i', 'a', 'I', 'A', 'o', 'O'] {
408 assert!(
409 keys.contains(&KeyEvent::char(ch)),
410 "normal root missing insert entry '{ch}'"
411 );
412 }
413 }
414
415 #[test]
416 fn normal_root_g_and_z_are_prefix_nodes() {
417 let got = children_for(Mode::Normal, &[]);
418 let g = got.iter().find(|d| d.key == KeyEvent::char('g')).unwrap();
419 let z = got.iter().find(|d| d.key == KeyEvent::char('z')).unwrap();
420 assert_eq!(g.desc, None, "g should be a prefix node (desc = None)");
421 assert_eq!(z.desc, None, "z should be a prefix node (desc = None)");
422 }
423
424 #[test]
425 fn normal_root_operator_prefixes_are_prefix_nodes() {
426 let got = children_for(Mode::Normal, &[]);
427 for ch in ['d', 'c', 'y'] {
428 let entry = got
429 .iter()
430 .find(|d| d.key == KeyEvent::char(ch))
431 .unwrap_or_else(|| panic!("normal root missing operator prefix '{ch}'"));
432 assert_eq!(
433 entry.desc, None,
434 "operator '{ch}' should be a prefix node (desc = None)"
435 );
436 }
437 }
438
439 #[test]
440 fn normal_root_has_ctrl_scroll_keys() {
441 let got = children_for(Mode::Normal, &[]);
442 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
443 for ch in ['d', 'u', 'f', 'b', 'e', 'y'] {
444 assert!(
445 keys.contains(&KeyEvent::ctrl(ch)),
446 "normal root missing <C-{ch}>"
447 );
448 }
449 }
450
451 #[test]
454 fn g_prefix_count_matches_expected() {
455 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
456 assert_eq!(
457 got.len(),
458 COUNT_G_PREFIX,
459 "g-prefix count drifted: got {}, expected {}",
460 got.len(),
461 COUNT_G_PREFIX
462 );
463 }
464
465 #[test]
466 fn g_prefix_includes_gg() {
467 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
468 let found = got
469 .iter()
470 .any(|d| d.key == KeyEvent::char('g') && d.desc.is_some());
471 assert!(found, "g-prefix missing gg");
472 }
473
474 #[test]
475 fn g_prefix_includes_gj_gk() {
476 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
477 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
478 assert!(keys.contains(&KeyEvent::char('j')), "g-prefix missing gj");
479 assert!(keys.contains(&KeyEvent::char('k')), "g-prefix missing gk");
480 }
481
482 #[test]
483 fn g_prefix_includes_gd() {
484 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
485 let found = got.iter().any(|d| d.key == KeyEvent::char('d'));
486 assert!(found, "g-prefix missing gd (goto definition)");
487 }
488
489 #[test]
490 fn g_prefix_includes_case_operators() {
491 let got = children_for(Mode::Normal, &[KeyEvent::char('g')]);
492 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
493 for ch in ['U', 'u', '~', 'q'] {
494 assert!(keys.contains(&KeyEvent::char(ch)), "g-prefix missing g{ch}");
495 }
496 }
497
498 #[test]
501 fn z_prefix_count_matches_expected() {
502 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
503 assert_eq!(
504 got.len(),
505 COUNT_Z_PREFIX,
506 "z-prefix count drifted: got {}, expected {}",
507 got.len(),
508 COUNT_Z_PREFIX
509 );
510 }
511
512 #[test]
513 fn z_prefix_includes_zz() {
514 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
515 let found = got
516 .iter()
517 .any(|d| d.key == KeyEvent::char('z') && d.desc.is_some());
518 assert!(found, "z-prefix missing zz");
519 }
520
521 #[test]
522 fn z_prefix_includes_zt_zb() {
523 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
524 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
525 assert!(keys.contains(&KeyEvent::char('t')), "z-prefix missing zt");
526 assert!(keys.contains(&KeyEvent::char('b')), "z-prefix missing zb");
527 }
528
529 #[test]
530 fn z_prefix_includes_fold_ops() {
531 let got = children_for(Mode::Normal, &[KeyEvent::char('z')]);
532 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
533 for ch in ['o', 'c', 'a', 'R', 'M', 'E', 'd', 'f'] {
534 assert!(keys.contains(&KeyEvent::char(ch)), "z-prefix missing z{ch}");
535 }
536 }
537
538 #[test]
541 fn op_pending_root_count_matches_expected() {
542 let got = children_for(Mode::Normal, &[KeyEvent::char('d')]);
543 assert_eq!(
544 got.len(),
545 COUNT_OP_PENDING_ROOT,
546 "op-pending root count drifted: got {}, expected {}",
547 got.len(),
548 COUNT_OP_PENDING_ROOT
549 );
550 }
551
552 #[test]
553 fn op_pending_same_for_d_c_y() {
554 let d = children_for(Mode::Normal, &[KeyEvent::char('d')]);
555 let c = children_for(Mode::Normal, &[KeyEvent::char('c')]);
556 let y = children_for(Mode::Normal, &[KeyEvent::char('y')]);
557 assert_eq!(d, c, "d and c op-pending children should match");
558 assert_eq!(d, y, "d and y op-pending children should match");
559 }
560
561 #[test]
562 fn op_pending_has_text_object_prefixes() {
563 let got = children_for(Mode::Normal, &[KeyEvent::char('d')]);
564 let keys: Vec<_> = got.iter().map(|d| d.key).collect();
565 assert!(
566 keys.contains(&KeyEvent::char('i')),
567 "op-pending missing 'i' (inner text obj)"
568 );
569 assert!(
570 keys.contains(&KeyEvent::char('a')),
571 "op-pending missing 'a' (around text obj)"
572 );
573 }
574
575 #[test]
576 fn op_pending_has_g_sub_prefix() {
577 let got = children_for(Mode::Normal, &[KeyEvent::char('d')]);
578 let g = got
579 .iter()
580 .find(|d| d.key == KeyEvent::char('g'))
581 .expect("op-pending missing g sub-prefix");
582 assert_eq!(g.desc, None, "g in op-pending should be a prefix node");
583 }
584
585 #[test]
588 fn unknown_prefix_returns_empty() {
589 let got = children_for(Mode::Normal, &[KeyEvent::char('q')]);
592 assert!(got.is_empty(), "unknown prefix should return empty vec");
593 }
594
595 #[test]
596 fn insert_mode_always_empty() {
597 assert!(children_for(Mode::Insert, &[]).is_empty());
598 assert!(children_for(Mode::Insert, &[KeyEvent::char('g')]).is_empty());
599 }
600
601 #[test]
602 fn op_pending_mode_root_matches_normal_d_prefix() {
603 let via_normal = children_for(Mode::Normal, &[KeyEvent::char('d')]);
604 let via_op = children_for(Mode::OpPending, &[]);
605 assert_eq!(via_normal, via_op);
606 }
607
608 #[test]
611 fn visual_mode_root_non_empty() {
612 let got = children_for(Mode::Visual, &[]);
613 assert!(!got.is_empty(), "visual root should not be empty");
614 }
615
616 #[test]
617 fn visual_mode_z_prefix_same_as_normal() {
618 let vn = children_for(Mode::Visual, &[KeyEvent::char('z')]);
619 let nn = children_for(Mode::Normal, &[KeyEvent::char('z')]);
620 assert_eq!(vn, nn, "visual z-prefix should equal normal z-prefix");
621 }
622}