Skip to main content

lattice_host/
mode_action_handlers.rs

1//! SN.3c.0: boot-time registration of modes' declarative *global*
2//! action handlers ([`Mode::action_handlers`](lattice_mode::Mode::action_handlers)).
3//!
4//! Sibling to [`crate::keymap_mode_contributions::translate_mode_keymaps`]:
5//! both walk the mode registry once at boot to apply a declarative
6//! `Mode` contribution. This one resolves each contribution's
7//! `action_name` to a `CommandId` and registers the handler in the
8//! shared [`ActionHandlerRegistry`](lattice_mode::ActionHandlerRegistry),
9//! returning the RAII tokens. The host holds them for the app's
10//! lifetime — the correct scope for *buffer-agnostic* handlers that
11//! read the active buffer/cursor/services from the `ActionContext`
12//! at call time (see `feedback_effect_vocabulary_is_host_boundary`).
13//! Per-buffer, session-scoped handlers register in
14//! `Mode::on_activate` instead, so their tokens drop with the Guard.
15
16use lattice_grammar::CommandRegistry;
17use lattice_mode::{ActionHandlerRegistration, ActionHandlerRegistryHandle, ModeRegistry};
18
19/// Walk every registered mode's `action_handlers()`, resolve each
20/// contribution's `action_name` → `CommandId`, register the handler
21/// globally, and return the registration tokens (held by the caller
22/// for the app's lifetime).
23///
24/// An unknown `action_name` (the mode declared a handler for a
25/// command the registry doesn't know — a wiring bug) is skipped
26/// with a `debug!`, non-fatal: the chord falls through to the
27/// default dispatch path.
28pub fn register_mode_action_handlers(
29    action_handlers: &ActionHandlerRegistryHandle,
30    mode_registry: &ModeRegistry,
31    command_registry: &CommandRegistry,
32) -> Vec<ActionHandlerRegistration> {
33    let mut tokens = Vec::new();
34    for (mode_id, mode) in mode_registry.iter() {
35        for contribution in mode.action_handlers() {
36            match command_registry.id_by_name(contribution.action_name) {
37                Some(id) => tokens.push(action_handlers.register(id, contribution.handler)),
38                None => tracing::debug!(
39                    mode = %mode_id,
40                    action = contribution.action_name,
41                    "mode action handler skipped: command not registered"
42                ),
43            }
44        }
45    }
46    tokens
47}
48
49#[cfg(test)]
50mod tests {
51    #![allow(clippy::unwrap_used)]
52    use super::*;
53    use lattice_grammar::registry::ActionSpec;
54    use lattice_mode::{
55        ActionContext, ActionHandler, ActionHandlerContribution, ActionHandlerRegistry,
56        CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind,
57    };
58    use std::sync::Arc;
59
60    /// Minimal mode that contributes one global action handler for
61    /// a configurable command name.
62    struct HandlerMode {
63        id: ModeId,
64        action_name: &'static str,
65    }
66
67    impl Mode for HandlerMode {
68        type Guard = ();
69        fn id(&self) -> ModeId {
70            self.id
71        }
72        fn kind(&self) -> ModeKind {
73            ModeKind::Minor
74        }
75        fn required_capabilities(&self) -> CapabilitySet {
76            CapabilitySet::empty()
77        }
78        fn action_handlers(&self) -> Vec<ActionHandlerContribution> {
79            let handler: ActionHandler = Arc::new(|_ctx: &ActionContext<'_>| None);
80            vec![ActionHandlerContribution {
81                action_name: self.action_name,
82                handler,
83            }]
84        }
85        fn on_activate(&self, _ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
86            Box::pin(async { Ok(()) })
87        }
88    }
89
90    fn cmd_registry_with(name: &str) -> (CommandRegistry, lattice_protocol::ids::CommandId) {
91        let mut cmd = CommandRegistry::new();
92        let id = cmd.register_action(
93            name,
94            "test action",
95            ActionSpec {
96                apply: Arc::new(|_| Ok(lattice_grammar::effect::Effect::None)),
97                args_schema: Vec::new(),
98            },
99        );
100        (cmd, id)
101    }
102
103    #[test]
104    fn registers_contributed_handler_and_tokens_control_lifetime() {
105        let (cmd, id) = cmd_registry_with("test:act");
106        let mut modes = ModeRegistry::new();
107        modes
108            .register(HandlerMode {
109                id: ModeId::new("handler-mode"),
110                action_name: "test:act",
111            })
112            .unwrap();
113        let action_handlers: ActionHandlerRegistryHandle = Arc::new(ActionHandlerRegistry::new());
114
115        let tokens = register_mode_action_handlers(&action_handlers, &modes, &cmd);
116        assert_eq!(tokens.len(), 1, "one contribution registered");
117        assert!(
118            action_handlers.lookup(id).is_some(),
119            "handler resolvable globally after the boot walk"
120        );
121
122        // The returned tokens own the registration's lifetime
123        // (app-lifetime when boot holds them); dropping them
124        // unregisters.
125        drop(tokens);
126        assert!(
127            action_handlers.lookup(id).is_none(),
128            "dropping the boot tokens unregisters the handler"
129        );
130    }
131
132    #[test]
133    fn unknown_action_name_is_skipped_not_panicked() {
134        let cmd = CommandRegistry::new(); // empty — name won't resolve
135        let mut modes = ModeRegistry::new();
136        modes
137            .register(HandlerMode {
138                id: ModeId::new("handler-mode"),
139                action_name: "test:nonexistent",
140            })
141            .unwrap();
142        let action_handlers: ActionHandlerRegistryHandle = Arc::new(ActionHandlerRegistry::new());
143
144        let tokens = register_mode_action_handlers(&action_handlers, &modes, &cmd);
145        assert!(tokens.is_empty(), "unresolvable name contributes no token");
146        assert_eq!(action_handlers.registered_count(), 0);
147    }
148}