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}