1use std::collections::HashMap;
45use std::sync::Arc;
46
47use lattice_grammar::{CommandInvocation, CommandRegistry};
48use lattice_mode::{BindingMode, DynMode, KeymapBinding, ModeId, ModeRegistry};
49use lattice_protocol::ChordPattern;
50
51use crate::keymap_registry::{KeymapHandle, PushLayerKind};
52use crate::keymap_trie::{BoundCommand, KeymapLayer, KeymapTrie};
53
54pub fn translate_mode_keymaps(
77 handle: &KeymapHandle,
78 registry: &ModeRegistry,
79 command_registry: &CommandRegistry,
80) {
81 for (mode_id, mode) in registry.iter() {
82 push_mode_keymap(handle, mode_id, &mode, command_registry);
83 }
84}
85
86pub fn translate_mode_keymap(
98 handle: &KeymapHandle,
99 mode_id: ModeId,
100 mode: &Arc<dyn DynMode>,
101 command_registry: &CommandRegistry,
102) {
103 push_mode_keymap(handle, mode_id, mode, command_registry);
104}
105
106fn push_mode_keymap(
107 handle: &KeymapHandle,
108 mode_id: ModeId,
109 mode: &Arc<dyn DynMode>,
110 command_registry: &CommandRegistry,
111) {
112 let keymap = mode.keymap();
113 if keymap.bindings.is_empty() && keymap.entries.is_empty() {
114 return;
115 }
116 let mut all_bindings: Vec<KeymapBinding> = keymap.bindings.clone();
121 all_bindings.extend(resolve_entries_into_bindings(
122 handle,
123 &keymap.entries,
124 command_registry,
125 ));
126 if all_bindings.is_empty() {
127 return;
131 }
132
133 let layer = match mode.kind() {
134 lattice_mode::ModeKind::Major => KeymapLayer::MajorMode(mode_id),
135 lattice_mode::ModeKind::Minor => KeymapLayer::MinorMode(mode_id),
136 };
137
138 let bindings_by_mode = group_bindings_into_tries(&all_bindings, layer);
139
140 let push_layer = match mode.kind() {
141 lattice_mode::ModeKind::Major => PushLayerKind::MajorMode(mode_id),
142 lattice_mode::ModeKind::Minor => PushLayerKind::MinorMode(mode_id),
143 };
144
145 handle.push_layer(push_layer, format!("{mode_id}"), bindings_by_mode);
146}
147
148fn group_bindings_into_tries(
158 bindings: &[KeymapBinding],
159 layer: KeymapLayer,
160) -> HashMap<BindingMode, KeymapTrie> {
161 let mut by_mode: HashMap<BindingMode, KeymapTrie> = HashMap::new();
162 for binding in bindings {
163 let bound = Arc::new(
164 BoundCommand::from_invocation(binding.command.clone(), binding.source.clone(), layer)
165 .with_fall_through(binding.fall_through),
168 );
169 by_mode
170 .entry(binding.mode)
171 .or_default()
172 .insert(&binding.chords, bound);
173 }
174 by_mode
175}
176
177fn resolve_entries_into_bindings(
197 handle: &KeymapHandle,
198 entries: &[&'static lattice_mode::KeymapEntry],
199 command_registry: &CommandRegistry,
200) -> Vec<KeymapBinding> {
201 let mut out = Vec::with_capacity(entries.len());
202 for entry in entries {
203 let Some(name) = entry.command else {
204 continue;
208 };
209 let Some(cmd_id) = command_registry.id_by_name(name) else {
210 tracing::warn!(
211 chord = entry.chord,
212 command = name,
213 modes = ?entry.modes,
214 "keymap_entry: command name not registered in CommandRegistry; skipping binding",
215 );
216 continue;
217 };
218 let expanded = handle.expand_leader(entry.chord);
228 let chords: Vec<ChordPattern> = match lattice_protocol::parse_chord_sequence(&expanded) {
229 Ok(parsed) => parsed.into_iter().map(ChordPattern::Literal).collect(),
230 Err(err) => {
231 tracing::warn!(
232 chord = entry.chord,
233 expanded = %expanded,
234 error = %err,
235 modes = ?entry.modes,
236 "keymap_entry: chord string failed to parse; skipping binding",
237 );
238 continue;
239 }
240 };
241 for &mode in entry.modes {
246 let binding = KeymapBinding::new(
247 mode,
248 chords.clone(),
249 CommandInvocation::of(cmd_id),
250 entry.source().clone(),
251 )
252 .with_doc(entry.doc)
253 .with_fall_through(entry.fall_through);
256 out.push(binding);
257 }
258 }
259 out
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 use std::sync::OnceLock;
267
268 use lattice_grammar::{CommandInvocation, CommandRegistry, SourceLocation};
269 use lattice_mode::{
270 Keymap, KeymapBinding, KeymapEntry, LifecycleFuture, Mode, ModeContext, ModeKind,
271 };
272 use lattice_protocol::ids::CommandId;
273 use lattice_protocol::{ChordPattern, KeyChord};
274
275 use crate::keymap_registry::KeymapHandle;
276 use crate::keymap_trie::LookupResult;
277
278 fn synthetic_invocation(raw: u64) -> CommandInvocation {
279 CommandInvocation::of(CommandId::new(raw))
280 }
281
282 fn here() -> SourceLocation {
283 SourceLocation::synthetic("keymap_mode_contributions::tests")
284 }
285
286 fn empty_command_registry() -> CommandRegistry {
289 CommandRegistry::new()
290 }
291
292 fn registry_with_builtins() -> CommandRegistry {
297 let mut r = CommandRegistry::new();
298 let _ = lattice_grammar::builtins::populate(&mut r);
299 r
300 }
301
302 fn fixture_table_form_entries() -> &'static [KeymapEntry] {
305 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
306 ENTRIES.get_or_init(|| {
307 vec![lattice_mode::keymap_entry! {
308 mode: Normal, chord: "z", doc: "Move down (table-form fixture)",
309 cmd: "motion:line-down"
310 }]
311 })
312 }
313
314 fn fixture_synthetic_entries() -> &'static [KeymapEntry] {
317 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
318 ENTRIES.get_or_init(|| {
319 vec![lattice_mode::keymap_entry! {
320 mode: Normal, chord: "z", doc: "Synthetic entry (no cmd)"
321 }]
322 })
323 }
324
325 fn fixture_unresolvable_entries() -> &'static [KeymapEntry] {
328 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
329 ENTRIES.get_or_init(|| {
330 vec![lattice_mode::keymap_entry! {
331 mode: Normal, chord: "z", doc: "Points at nonexistent command",
332 cmd: "test:nonexistent-command-xyz"
333 }]
334 })
335 }
336
337 struct TestMode {
342 id: ModeId,
343 keymap: Keymap,
344 }
345
346 impl Mode for TestMode {
347 type Guard = ();
348 fn id(&self) -> ModeId {
349 self.id
350 }
351 fn kind(&self) -> ModeKind {
352 ModeKind::Minor
353 }
354 fn keymap(&self) -> Keymap {
355 self.keymap.clone()
356 }
357 fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
358 Box::pin(async { Ok(()) })
359 }
360 }
361
362 fn test_mode(id_str: &str, keymap: Keymap) -> TestMode {
363 let id = if id_str.ends_with("-mode") {
366 ModeId::new(id_str)
367 } else {
368 ModeId::new(&format!("{id_str}-mode"))
369 };
370 TestMode { id, keymap }
371 }
372
373 fn lookup(
374 handle: &KeymapHandle,
375 mode: BindingMode,
376 active: &[ModeId],
377 chords: &[KeyChord],
378 ) -> LookupResult {
379 handle.lookup_with_context(mode, chords, active)
380 }
381
382 #[test]
383 fn translate_skips_modes_with_default_keymap() {
384 let h = KeymapHandle::new();
385 let mut registry = ModeRegistry::new();
386 let mode_id = registry
387 .register(test_mode("test-mode/empty", Keymap::default()))
388 .expect("register");
389
390 translate_mode_keymaps(&h, ®istry, &empty_command_registry());
391
392 let result = lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('a')]);
394 assert!(matches!(result, LookupResult::Unbound));
395 }
396
397 #[test]
398 fn translate_pushes_single_binding_reachable_via_registry() {
399 let h = KeymapHandle::new();
400 let cmd = synthetic_invocation(42);
401 let binding = KeymapBinding::new(
402 BindingMode::Normal,
403 vec![ChordPattern::Literal(KeyChord::char('z'))],
404 cmd.clone(),
405 here(),
406 );
407 let keymap = Keymap::new().bind(binding);
408 let mut registry = ModeRegistry::new();
409 let mode_id = registry
410 .register(test_mode("test-mode/single", keymap))
411 .expect("register");
412
413 translate_mode_keymaps(&h, ®istry, &empty_command_registry());
414
415 let result = lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('z')]);
416 match result {
417 LookupResult::Bound { command, .. } => {
418 assert_eq!(command.command, cmd);
419 assert_eq!(command.layer, KeymapLayer::MinorMode(mode_id));
420 }
421 other => panic!("expected Bound, got {other:?}"),
422 }
423 }
424
425 #[test]
426 fn translate_groups_bindings_across_binding_modes() {
427 let h = KeymapHandle::new();
428 let normal_cmd = synthetic_invocation(1);
429 let visual_cmd = synthetic_invocation(2);
430 let keymap = Keymap::new()
431 .bind(KeymapBinding::new(
432 BindingMode::Normal,
433 vec![ChordPattern::Literal(KeyChord::char('x'))],
434 normal_cmd.clone(),
435 here(),
436 ))
437 .bind(KeymapBinding::new(
438 BindingMode::Visual,
439 vec![ChordPattern::Literal(KeyChord::char('x'))],
440 visual_cmd.clone(),
441 here(),
442 ));
443 let mut registry = ModeRegistry::new();
444 let mode_id = registry
445 .register(test_mode("test-mode/multi", keymap))
446 .expect("register");
447
448 translate_mode_keymaps(&h, ®istry, &empty_command_registry());
449
450 match lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('x')]) {
451 LookupResult::Bound { command, .. } => {
452 assert_eq!(command.command, normal_cmd, "Normal-mode binding");
453 }
454 other => panic!("expected Normal Bound, got {other:?}"),
455 }
456 match lookup(&h, BindingMode::Visual, &[mode_id], &[KeyChord::char('x')]) {
457 LookupResult::Bound { command, .. } => {
458 assert_eq!(command.command, visual_cmd, "Visual-mode binding");
459 }
460 other => panic!("expected Visual Bound, got {other:?}"),
461 }
462 }
463
464 #[test]
478 fn a_native_mode_can_bind_a_leader_chord() {
479 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
480 let entries = ENTRIES.get_or_init(|| {
481 vec![lattice_mode::keymap_entry! {
482 mode: Normal,
483 chord: "<leader>z",
484 doc: "TB.0 leader fixture",
485 cmd: "motion:line-down"
486 }]
487 });
488 let h = KeymapHandle::new();
489 let keymap = Keymap::from_entries(entries.as_slice());
490 let mut registry = ModeRegistry::new();
491 let mode_id = registry
492 .register(test_mode("test-mode/leader", keymap))
493 .expect("register");
494
495 translate_mode_keymaps(&h, ®istry, ®istry_with_builtins());
496
497 match lookup(
501 &h,
502 BindingMode::Normal,
503 &[mode_id],
504 &[KeyChord::char(' '), KeyChord::char('z')],
505 ) {
506 LookupResult::Bound { .. } => {}
507 other => panic!("`<leader>z` must reach the trie as `<Space>z`, got {other:?}"),
508 }
509 }
510
511 #[test]
512 fn multi_mode_entry_fans_out_into_one_binding_per_mode() {
513 static ENTRIES: OnceLock<Vec<KeymapEntry>> = OnceLock::new();
517 let entries = ENTRIES.get_or_init(|| {
518 vec![lattice_mode::keymap_entry! {
519 mode: [Normal, Visual],
520 chord: "z",
521 doc: "B-field multi-mode fixture",
522 cmd: "motion:line-down"
523 }]
524 });
525 let h = KeymapHandle::new();
526 let keymap = Keymap::from_entries(entries.as_slice());
527 let mut registry = ModeRegistry::new();
528 let mode_id = registry
529 .register(test_mode("test-mode/bfield", keymap))
530 .expect("register");
531
532 translate_mode_keymaps(&h, ®istry, ®istry_with_builtins());
533
534 for mode in [BindingMode::Normal, BindingMode::Visual] {
535 assert!(
536 matches!(
537 lookup(&h, mode, &[mode_id], &[KeyChord::char('z')]),
538 LookupResult::Bound { .. }
539 ),
540 "multi-mode entry must bind in {mode:?}"
541 );
542 }
543 }
544
545 #[test]
546 fn translate_emacs_prefix_sequence_via_bind_chord() {
547 let h = KeymapHandle::new();
551 let cmd = synthetic_invocation(99);
552 let keymap = Keymap::new().bind_chord(BindingMode::Normal, "<C-x>pp", cmd.clone());
553 let mut registry = ModeRegistry::new();
554 let mode_id = registry
555 .register(test_mode("test-mode/emacs", keymap))
556 .expect("register");
557
558 translate_mode_keymaps(&h, ®istry, &empty_command_registry());
559
560 let partial = lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::ctrl('x')]);
562 assert!(matches!(partial, LookupResult::Partial), "after <C-x>");
563 let still_partial = lookup(
565 &h,
566 BindingMode::Normal,
567 &[mode_id],
568 &[KeyChord::ctrl('x'), KeyChord::char('p')],
569 );
570 assert!(
571 matches!(still_partial, LookupResult::Partial),
572 "after <C-x>p"
573 );
574 let result = lookup(
576 &h,
577 BindingMode::Normal,
578 &[mode_id],
579 &[
580 KeyChord::ctrl('x'),
581 KeyChord::char('p'),
582 KeyChord::char('p'),
583 ],
584 );
585 match result {
586 LookupResult::Bound { command, .. } => {
587 assert_eq!(command.command, cmd);
588 assert_eq!(command.layer, KeymapLayer::MinorMode(mode_id));
589 }
590 other => panic!("expected Bound on <C-x>pp, got {other:?}"),
591 }
592 }
593
594 #[test]
595 fn single_mode_translation_matches_bulk_pass() {
596 let bulk_handle = KeymapHandle::new();
600 let single_handle = KeymapHandle::new();
601 let cmd = synthetic_invocation(7);
602 let keymap = Keymap::new().bind_chord(BindingMode::Normal, "gd", cmd.clone());
603 let mut registry = ModeRegistry::new();
604 let mode_id = registry
605 .register(test_mode("test-mode/parity", keymap))
606 .expect("register");
607
608 translate_mode_keymaps(&bulk_handle, ®istry, &empty_command_registry());
609 let mode_arc = registry.get(mode_id).expect("registered mode");
610 translate_mode_keymap(
611 &single_handle,
612 mode_id,
613 &mode_arc,
614 &empty_command_registry(),
615 );
616
617 let chord_path = [KeyChord::char('g'), KeyChord::char('d')];
618 let bulk_result = lookup(&bulk_handle, BindingMode::Normal, &[mode_id], &chord_path);
619 let single_result = lookup(&single_handle, BindingMode::Normal, &[mode_id], &chord_path);
620 match (bulk_result, single_result) {
621 (
622 LookupResult::Bound {
623 command: bulk_cmd, ..
624 },
625 LookupResult::Bound {
626 command: single_cmd,
627 ..
628 },
629 ) => {
630 assert_eq!(bulk_cmd.command, cmd);
631 assert_eq!(single_cmd.command, cmd);
632 assert_eq!(bulk_cmd.layer, single_cmd.layer);
633 }
634 other => panic!("bulk vs single divergence: {other:?}"),
635 }
636 }
637
638 #[test]
641 fn translate_resolves_table_form_entries_via_command_registry() {
642 let h = KeymapHandle::new();
648 let cmd_registry = registry_with_builtins();
649 let expected_id = cmd_registry
650 .id_by_name("motion:line-down")
651 .expect("motion:line-down should be registered by builtins");
652
653 let keymap = Keymap::from_entries(fixture_table_form_entries());
654 let mut registry = ModeRegistry::new();
655 let mode_id = registry
656 .register(test_mode("test-mode/entries-resolve", keymap))
657 .expect("register");
658
659 translate_mode_keymaps(&h, ®istry, &cmd_registry);
660
661 match lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('z')]) {
662 LookupResult::Bound { command, .. } => {
663 assert_eq!(command.command.command, expected_id);
664 assert_eq!(command.layer, KeymapLayer::MinorMode(mode_id));
665 }
666 other => panic!("expected Bound, got {other:?}"),
667 }
668 }
669
670 #[test]
671 fn translate_skips_synthetic_entries_with_no_command() {
672 let h = KeymapHandle::new();
678 let cmd_registry = registry_with_builtins();
679 let keymap = Keymap::from_entries(fixture_synthetic_entries());
680 let mut registry = ModeRegistry::new();
681 let mode_id = registry
682 .register(test_mode("test-mode/synthetic", keymap))
683 .expect("register");
684
685 translate_mode_keymaps(&h, ®istry, &cmd_registry);
686
687 let result = lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('z')]);
688 assert!(matches!(result, LookupResult::Unbound));
689 }
690
691 #[test]
692 fn translate_warns_and_skips_unresolvable_entry_names() {
693 let h = KeymapHandle::new();
697 let cmd_registry = empty_command_registry();
698 let keymap = Keymap::from_entries(fixture_unresolvable_entries());
699 let mut registry = ModeRegistry::new();
700 let mode_id = registry
701 .register(test_mode("test-mode/unresolvable", keymap))
702 .expect("register");
703
704 translate_mode_keymaps(&h, ®istry, &cmd_registry);
705
706 let result = lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('z')]);
707 assert!(matches!(result, LookupResult::Unbound));
708 }
709
710 #[test]
713 fn translate_combines_chain_form_with_table_form_entries() {
714 let h = KeymapHandle::new();
723 let cmd_registry = registry_with_builtins();
724 let chain_cmd = synthetic_invocation(123);
725
726 let keymap = Keymap::from_entries(fixture_table_form_entries()).bind_chord(
727 BindingMode::Normal,
728 "<C-r>",
729 chain_cmd.clone(),
730 );
731
732 let mut registry = ModeRegistry::new();
733 let mode_id = registry
734 .register(test_mode("test-mode/combined", keymap))
735 .expect("register");
736
737 translate_mode_keymaps(&h, ®istry, &cmd_registry);
738
739 let expected_id = cmd_registry
741 .id_by_name("motion:line-down")
742 .expect("motion:line-down should be registered by builtins");
743 match lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::char('z')]) {
744 LookupResult::Bound { command, .. } => {
745 assert_eq!(command.command.command, expected_id, "entry-form");
746 assert_eq!(command.layer, KeymapLayer::MinorMode(mode_id));
747 }
748 other => panic!("expected Bound for entry-form 'z', got {other:?}"),
749 }
750
751 match lookup(&h, BindingMode::Normal, &[mode_id], &[KeyChord::ctrl('r')]) {
754 LookupResult::Bound { command, .. } => {
755 assert_eq!(command.command, chain_cmd, "chain-form");
756 assert_eq!(command.layer, KeymapLayer::MinorMode(mode_id));
757 }
758 other => panic!("expected Bound for chain-form '<C-r>', got {other:?}"),
759 }
760 }
761}