Skip to main content

reovim_driver_session/
extension.rs

1//! Session extension mechanism for module-provided per-session state.
2//!
3//! This module provides the [`SessionExtension`] trait that modules implement
4//! to store per-session policy state. The runner manages an [`ExtensionMap`]
5//! for each session, allowing type-safe access to module extensions.
6//!
7//! # Design
8//!
9//! - **Mechanism (Session Driver)**: Type-erased storage via `TypeId`
10//! - **Policy (Modules)**: What state to store (e.g., `VimSessionState`)
11//!
12//! # `TextInputSink`
13//!
14//! Extensions that accept text input (like command-line input) implement
15//! [`TextInputSink`]. The resolver specifies the target via [`InputTarget`],
16//! and the runner routes characters accordingly.
17//!
18//! # Example
19//!
20//! ```ignore
21//! use reovim_driver_session::{SessionExtension, ExtensionMap, TextInputSink};
22//!
23//! // Module defines its per-session state
24//! #[derive(Default)]
25//! pub struct VimSessionState {
26//!     pub pending_count: Option<usize>,
27//!     pub pending_register: Option<char>,
28//! }
29//!
30//! impl SessionExtension for VimSessionState {
31//!     fn create() -> Self { Self::default() }
32//! }
33//!
34//! // Command-line state that accepts text input
35//! #[derive(Default)]
36//! pub struct CmdlineState {
37//!     pub buffer: String,
38//! }
39//!
40//! impl SessionExtension for CmdlineState {
41//!     fn create() -> Self { Self::default() }
42//!
43//!     fn as_text_input_sink(&mut self) -> Option<&mut dyn TextInputSink> {
44//!         Some(self)
45//!     }
46//! }
47//!
48//! impl TextInputSink for CmdlineState {
49//!     fn insert_char(&mut self, ch: char) {
50//!         self.buffer.push(ch);
51//!     }
52//! }
53//!
54//! // Access in resolvers/commands
55//! let mut extensions = ExtensionMap::new();
56//! let vim = extensions.get_or_insert::<VimSessionState>();
57//! vim.pending_count = Some(5);
58//! ```
59
60use std::{
61    any::{Any, TypeId},
62    collections::HashMap,
63};
64
65// ============================================================================
66// TextInputSink - Trait for extensions that accept text input (#482)
67// ============================================================================
68
69/// Trait for extensions that can receive text input.
70///
71/// Implement this trait for session extensions that accept character input,
72/// such as command-line input, search input, or any other text entry mode.
73///
74/// # Architecture (#482)
75///
76/// This trait enables generic input routing without string-based mode detection:
77/// - **Resolver** specifies target via `ResolveResult::insert_char_to::<T>()`
78/// - **Runner** routes to extension via `InputTarget::Extension(TypeId)`
79/// - **Extension** receives character via this trait
80///
81/// # Example
82///
83/// ```ignore
84/// use reovim_driver_session::{SessionExtension, TextInputSink};
85///
86/// #[derive(Default)]
87/// pub struct CmdlineState {
88///     pub buffer: String,
89///     pub cursor: usize,
90/// }
91///
92/// impl TextInputSink for CmdlineState {
93///     fn insert_char(&mut self, ch: char) {
94///         self.buffer.insert(self.cursor, ch);
95///         self.cursor += ch.len_utf8();
96///     }
97/// }
98/// ```
99pub trait TextInputSink {
100    /// Insert a character at the current position.
101    fn insert_char(&mut self, ch: char);
102}
103
104// ============================================================================
105// SessionExtension - Trait for module-provided per-session state
106// ============================================================================
107
108/// Trait for module-provided per-session state.
109///
110/// Modules implement this trait to store policy state that varies per client
111/// session. The session driver provides type-safe storage via [`ExtensionMap`].
112///
113/// # Requirements
114///
115/// - `Send + Sync`: Extensions must be thread-safe
116/// - `'static`: No borrowed references (owned data only)
117///
118/// # Example
119///
120/// ```ignore
121/// use reovim_driver_session::SessionExtension;
122///
123/// #[derive(Default)]
124/// pub struct MyModuleState {
125///     pub counter: usize,
126/// }
127///
128/// impl SessionExtension for MyModuleState {
129///     fn create() -> Self {
130///         Self::default()
131///     }
132/// }
133/// ```
134pub trait SessionExtension: Send + Sync + 'static {
135    /// Create default state for a new session.
136    ///
137    /// Called when the extension is first accessed for a session.
138    fn create() -> Self
139    where
140        Self: Sized;
141
142    /// Return self as a [`TextInputSink`] if this extension accepts text input.
143    ///
144    /// Override this method in extensions that implement `TextInputSink` to
145    /// enable input routing via `InputTarget::Extension`.
146    ///
147    /// # Default Implementation
148    ///
149    /// Returns `None` - most extensions don't accept text input.
150    ///
151    /// # Example
152    ///
153    /// ```ignore
154    /// impl SessionExtension for CmdlineState {
155    ///     fn create() -> Self { Self::default() }
156    ///
157    ///     fn as_text_input_sink(&mut self) -> Option<&mut dyn TextInputSink> {
158    ///         Some(self)
159    ///     }
160    /// }
161    /// ```
162    fn as_text_input_sink(&mut self) -> Option<&mut dyn TextInputSink> {
163        None
164    }
165}
166
167// ============================================================================
168// SessionExtensionDyn - Object-safe wrapper for runtime access (#482)
169// ============================================================================
170
171/// Object-safe trait for runtime access to session extensions.
172///
173/// This trait provides a dyn-compatible interface to `SessionExtension` methods.
174/// It's automatically implemented for all `SessionExtension` types via blanket impl.
175///
176/// # Why This Exists
177///
178/// `SessionExtension::create()` has `where Self: Sized`, making the trait not
179/// object-safe. This wrapper provides object-safe access to:
180/// - `Any` downcasting (for type-safe retrieval)
181/// - `TextInputSink` access (for input routing)
182pub trait SessionExtensionDyn: Send + Sync + 'static {
183    /// Get as `&dyn Any` for downcasting.
184    fn as_any(&self) -> &dyn Any;
185
186    /// Get as `&mut dyn Any` for mutable downcasting.
187    fn as_any_mut(&mut self) -> &mut dyn Any;
188
189    /// Get as `TextInputSink` if this extension accepts text input.
190    fn as_text_input_sink(&mut self) -> Option<&mut dyn TextInputSink>;
191}
192
193/// Blanket implementation of `SessionExtensionDyn` for all `SessionExtension` types.
194impl<T: SessionExtension> SessionExtensionDyn for T {
195    fn as_any(&self) -> &dyn Any {
196        self
197    }
198
199    fn as_any_mut(&mut self) -> &mut dyn Any {
200        self
201    }
202
203    fn as_text_input_sink(&mut self) -> Option<&mut dyn TextInputSink> {
204        SessionExtension::as_text_input_sink(self)
205    }
206}
207
208// ============================================================================
209// ExtensionMap - Type-erased extension storage
210// ============================================================================
211
212/// Type-erased extension storage using `TypeId`.
213///
214/// Each session has its own `ExtensionMap`. Modules access their state
215/// via the generic `get` and `get_mut` methods, which use `TypeId` for
216/// type-safe lookup.
217///
218/// # Thread Safety
219///
220/// `ExtensionMap` itself is not `Sync`, but the stored extensions are
221/// `Send + Sync`. Access should be synchronized at the session level.
222///
223/// # `TextInputSink` Support (#482)
224///
225/// Extensions that implement `TextInputSink` can be accessed via
226/// `get_text_input_sink_by_id()` for input routing without knowing
227/// the concrete type at compile time.
228#[derive(Default)]
229pub struct ExtensionMap {
230    /// Type-erased storage. Key is `TypeId` of the concrete extension type.
231    map: HashMap<TypeId, Box<dyn SessionExtensionDyn>>,
232}
233
234impl ExtensionMap {
235    /// Create a new empty extension map.
236    #[must_use]
237    pub fn new() -> Self {
238        Self {
239            map: HashMap::new(),
240        }
241    }
242
243    /// Get extension by type (immutable).
244    ///
245    /// Returns `None` if the extension hasn't been inserted yet.
246    #[must_use]
247    pub fn get<T: SessionExtension>(&self) -> Option<&T> {
248        self.map
249            .get(&TypeId::of::<T>())
250            .and_then(|boxed| (**boxed).as_any().downcast_ref())
251    }
252
253    /// Get extension by type (mutable).
254    ///
255    /// Returns `None` if the extension hasn't been inserted yet.
256    pub fn get_mut<T: SessionExtension>(&mut self) -> Option<&mut T> {
257        self.map
258            .get_mut(&TypeId::of::<T>())
259            .and_then(|boxed| (**boxed).as_any_mut().downcast_mut())
260    }
261
262    /// Get or create extension (lazy initialization).
263    ///
264    /// If the extension doesn't exist, creates it using `T::create()`.
265    /// This is the primary way modules access their state.
266    ///
267    /// # Panics
268    ///
269    /// Panics if the stored type doesn't match `T`. This should never
270    /// happen in correct code since `TypeId` is used as the key.
271    pub fn get_or_insert<T: SessionExtension>(&mut self) -> &mut T {
272        (**self
273            .map
274            .entry(TypeId::of::<T>())
275            .or_insert_with(|| Box::new(T::create())))
276        .as_any_mut()
277        .downcast_mut()
278        .expect("ExtensionMap type mismatch - this is a bug")
279    }
280
281    /// Get extension as [`TextInputSink`] by type ID.
282    ///
283    /// This enables routing input to extensions without knowing the concrete
284    /// type at compile time. Used by the runner to handle `InputTarget::Extension`.
285    ///
286    /// # Arguments
287    ///
288    /// * `type_id` - The `TypeId` of the extension (from `InputTarget::Extension`)
289    ///
290    /// # Returns
291    ///
292    /// * `Some(&mut dyn TextInputSink)` - Extension exists and implements `TextInputSink`
293    /// * `None` - Extension doesn't exist or doesn't implement `TextInputSink`
294    ///
295    /// # Example
296    ///
297    /// ```ignore
298    /// use std::any::TypeId;
299    /// use reovim_driver_session::ExtensionMap;
300    ///
301    /// let mut extensions = ExtensionMap::new();
302    /// let type_id = TypeId::of::<CmdlineState>();
303    ///
304    /// // First, ensure the extension exists
305    /// extensions.get_or_insert::<CmdlineState>();
306    ///
307    /// // Then route input by TypeId
308    /// if let Some(sink) = extensions.get_text_input_sink_by_id(type_id) {
309    ///     sink.insert_char('x');
310    /// }
311    /// ```
312    pub fn get_text_input_sink_by_id(&mut self, type_id: TypeId) -> Option<&mut dyn TextInputSink> {
313        (**self.map.get_mut(&type_id)?).as_text_input_sink()
314    }
315
316    /// Check if an extension exists.
317    #[must_use]
318    pub fn contains<T: SessionExtension>(&self) -> bool {
319        self.map.contains_key(&TypeId::of::<T>())
320    }
321
322    /// Remove an extension.
323    ///
324    /// Returns `true` if the extension was present.
325    pub fn remove<T: SessionExtension>(&mut self) -> bool {
326        self.map.remove(&TypeId::of::<T>()).is_some()
327    }
328
329    /// Get the number of extensions.
330    #[must_use]
331    pub fn len(&self) -> usize {
332        self.map.len()
333    }
334
335    /// Check if empty.
336    #[must_use]
337    pub fn is_empty(&self) -> bool {
338        self.map.is_empty()
339    }
340
341    /// Clear all extensions.
342    pub fn clear(&mut self) {
343        self.map.clear();
344    }
345}
346
347impl std::fmt::Debug for ExtensionMap {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        f.debug_struct("ExtensionMap")
350            .field("count", &self.map.len())
351            .finish()
352    }
353}
354#[cfg(test)]
355#[path = "extension_tests.rs"]
356mod tests;