Skip to main content

lattice_mode/
context.rs

1//! `ModeContext`: the handle passed to
2//! [`crate::Mode::on_activate`].
3//!
4//! Owned (`Send + 'static`) so the dispatcher can hand it to a
5//! `tokio::spawn`ed future without lifetime gymnastics. Hooks
6//! that need to mutate options (e.g. `lsp-folding-mode` swapping
7//! `foldmethod=lsp` on activate) reach the typed-options
8//! registry through [`ModeContext::config`]. Hooks that need to
9//! publish typed events use [`ModeContext::events`]. Hooks that
10//! need a subsystem handle (LSP supervisor, BufferStore) call
11//! [`ModeContext::service`].
12//!
13//! Why no `BufferLocals` access:
14//!
15//! Mode-private state is owned by the [`Mode::Guard`](crate::Mode::Guard)
16//! returned from `on_activate`. The Guard's `Drop` impl performs
17//! cleanup. App-managed buffer-locals (icons, syntax handles,
18//! folds, ...) live in an App-owned map and are written through
19//! App-side code paths, not through `ctx`.
20//!
21//! Why owned handles (not `&Arc<T>`):
22//!
23//! The context is constructed once per activation and moved into
24//! the lifecycle future. The future captures it across `await`
25//! points, so every field must be owned and `Send + 'static`.
26//! `Arc<T>` is cheap to clone -- the dispatcher does one
27//! `Arc::clone` per activation when building the context.
28
29use std::any::Any;
30use std::sync::Arc;
31
32use lattice_config::ConfigRegistry;
33use lattice_protocol::ids::BufferId;
34use lattice_runtime::EventBus;
35
36use crate::mode::ModeId;
37use crate::services::ServiceRegistry;
38
39/// Lifecycle context. Carries the current activation's
40/// metadata + cheap-clone handles to the system registries the
41/// mode may need.
42///
43/// **Owned, `Send + 'static`**: the dispatcher constructs one
44/// per activation, moves it into the lifecycle future, and
45/// drops it when the future resolves.
46pub struct ModeContext {
47    buffer_id: BufferId,
48    current_mode: ModeId,
49    config: Arc<ConfigRegistry>,
50    events: Arc<EventBus>,
51    services: Arc<ServiceRegistry>,
52}
53
54impl ModeContext {
55    /// Construct a new context. Public for tests that drive
56    /// mode lifecycle directly; production callers go through
57    /// the registry's activation methods which build the
58    /// context internally.
59    pub fn new(
60        buffer_id: BufferId,
61        current_mode: ModeId,
62        config: Arc<ConfigRegistry>,
63        events: Arc<EventBus>,
64        services: Arc<ServiceRegistry>,
65    ) -> Self {
66        Self {
67            buffer_id,
68            current_mode,
69            config,
70            events,
71            services,
72        }
73    }
74
75    /// Typed service lookup. Used by modes that need access to
76    /// subsystem handles (e.g. `LspMode` retrieves
77    /// `LspSupervisorHandle`; `LspLogMode` retrieves
78    /// `BufferStoreHandle` to synthesize its `*lsp*` buffer).
79    /// Returns `None` when no service of type `T` is registered
80    /// -- modes should fail gracefully (log / echo) rather than
81    /// panic, since tests may run a mode without wiring the
82    /// service.
83    pub fn service<T: Any + Send + Sync>(&self) -> Option<Arc<T>> {
84        self.services.get::<T>()
85    }
86
87    /// Shared typed-options registry. Mutations propagate
88    /// through the registry's `OptionChanged` event stream the
89    /// same way `:set` does, so subscribers fire automatically.
90    pub fn config(&self) -> &ConfigRegistry {
91        &self.config
92    }
93
94    /// Cheap-clone handle to the config registry. Use when the
95    /// mode needs to stash the registry inside its Guard for
96    /// later mutation (e.g. restoring an option in `Drop`).
97    pub fn config_handle(&self) -> Arc<ConfigRegistry> {
98        self.config.clone()
99    }
100
101    /// Shared typed event bus.
102    pub fn events(&self) -> &EventBus {
103        &self.events
104    }
105
106    /// Cheap-clone handle to the event bus. Use when the mode
107    /// needs to stash the bus inside its Guard for later
108    /// publish / unsubscribe (e.g. unsubscribing a subscription
109    /// ID in `Drop`).
110    pub fn events_handle(&self) -> Arc<EventBus> {
111        self.events.clone()
112    }
113
114    /// Buffer the activation is operating on.
115    pub fn buffer_id(&self) -> BufferId {
116        self.buffer_id
117    }
118
119    /// The mode whose lifecycle hook is currently running.
120    pub fn current_mode(&self) -> ModeId {
121        self.current_mode
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    #![allow(clippy::unwrap_used)]
128    use super::*;
129
130    fn ctx() -> ModeContext {
131        ModeContext::new(
132            BufferId::new(1),
133            ModeId::new("a-mode"),
134            Arc::new(ConfigRegistry::new()),
135            Arc::new(EventBus::new()),
136            Arc::new(ServiceRegistry::new()),
137        )
138    }
139
140    #[test]
141    fn buffer_id_and_current_mode_round_trip() {
142        let c = ctx();
143        assert_eq!(c.buffer_id(), BufferId::new(1));
144        assert_eq!(c.current_mode().as_str(), "a-mode");
145    }
146
147    #[test]
148    fn handles_are_cheap_to_clone() {
149        let c = ctx();
150        let h1 = c.events_handle();
151        let h2 = c.events_handle();
152        // Both Arc clones point at the same bus.
153        assert!(Arc::ptr_eq(&h1, &h2));
154    }
155
156    /// The context is `Send + 'static` so a spawned future can
157    /// hold it across `.await` points.
158    #[test]
159    fn ctx_is_send_static() {
160        fn assert_send_static<T: Send + 'static>() {}
161        assert_send_static::<ModeContext>();
162    }
163}