Skip to main content

reovim_kernel/core/
mode.rs

1//! Mode and command identity types.
2//!
3//! Linux equivalent: Mode/command identification (mechanism only)
4//!
5//! This module provides identity types for modes and commands. The kernel
6//! only tracks WHAT modes and commands exist - HOW they behave is policy
7//! defined by drivers and modules.
8//!
9//! # Architecture
10//!
11//! | Layer | Responsibility |
12//! |-------|---------------|
13//! | Kernel (this) | Identity + Behavior: `ModeId`, `CommandId`, `Mode` trait, `CursorStyle` |
14//! | Display Driver | Display: `ModeDisplay` (uses kernel `CursorStyle`) |
15//! | Input Driver | Input: `KeySequence`, `Keybinding` |
16//! | Command Driver | Execution: `Command`, `CommandHandler` |
17//! | Modules | Policy: actual mode/command implementations |
18//!
19//! # Mode Ownership
20//!
21//! The `Mode` trait is designed for type-safe, compile-time enforced mode ownership:
22//!
23//! - Policy modules (e.g., vim) define their own Mode enums
24//! - Mode enums implement the `Mode` trait
25//! - `ModeId` provides runtime identity for storage in `ModeStack`
26//! - Blanket impl `From<M> for ModeId` allows ergonomic conversion
27//!
28//! # Usage
29//!
30//! ```
31//! use reovim_kernel::api::v1::{Mode, ModeId, ModuleId, CommandId, CursorStyle};
32//!
33//! // Define a module ID
34//! const MY_MODULE: ModuleId = ModuleId::new("my-module");
35//!
36//! // Create mode and command IDs
37//! let normal_mode = ModeId::new(MY_MODULE.clone(), "normal");
38//! let insert_mode = ModeId::with_discriminant(MY_MODULE.clone(), "insert", 1);
39//! let cursor_down = CommandId::new(MY_MODULE.clone(), "cursor-down");
40//!
41//! assert_eq!(insert_mode.discriminant(), 1);
42//! ```
43
44use std::{fmt, hash::Hash};
45
46use crate::api::module::ModuleId;
47
48// ============================================================================
49// CursorStyle
50// ============================================================================
51
52/// Cursor display style.
53///
54/// Different modes typically use different cursor styles to provide
55/// visual feedback about the current mode. This enum is defined in the
56/// kernel so that the `Mode` trait can include cursor style information.
57///
58/// # Variants
59///
60/// - `Block`: Full cell cursor, typical for Normal mode
61/// - `Bar`: Thin vertical line, typical for Insert mode
62/// - `Underline`: Horizontal line under the character
63/// - `Hidden`: Cursor not visible
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
65pub enum CursorStyle {
66    /// Block cursor (full cell, typical for Normal mode).
67    #[default]
68    Block,
69    /// Vertical bar cursor (thin line, typical for Insert mode).
70    Bar,
71    /// Underline cursor (horizontal line under the character).
72    Underline,
73    /// Hidden cursor (cursor not visible).
74    Hidden,
75}
76
77impl CursorStyle {
78    /// Check if the cursor is visible.
79    #[must_use]
80    pub const fn is_visible(&self) -> bool {
81        !matches!(self, Self::Hidden)
82    }
83
84    /// Get the style name for display.
85    #[must_use]
86    pub const fn name(&self) -> &'static str {
87        match self {
88            Self::Block => "block",
89            Self::Bar => "bar",
90            Self::Underline => "underline",
91            Self::Hidden => "hidden",
92        }
93    }
94}
95
96// ============================================================================
97// ModeId
98// ============================================================================
99
100/// Namespaced mode identifier with numeric discriminant.
101///
102/// Modes are identified by their owning module, a local name, and a numeric
103/// discriminant for O(1) identity comparison. The discriminant provides
104/// compile-time type safety when used with the `Mode` trait.
105///
106/// # Identity
107///
108/// Two `ModeId`s are equal if and only if their module AND discriminant match.
109/// The name is for display purposes only and does not affect equality.
110///
111/// # Example
112///
113/// ```
114/// use reovim_kernel::api::v1::{ModeId, ModuleId};
115///
116/// let editor_module = ModuleId::new("editor");
117/// let normal = ModeId::with_discriminant(editor_module.clone(), "NORMAL", 0);
118/// let insert = ModeId::with_discriminant(editor_module, "INSERT", 1);
119///
120/// assert_eq!(normal.name(), "NORMAL");
121/// assert_eq!(normal.discriminant(), 0);
122/// assert_eq!(insert.discriminant(), 1);
123/// assert_ne!(normal, insert);
124/// ```
125#[derive(Debug, Clone)]
126pub struct ModeId {
127    /// The module that owns this mode.
128    module: ModuleId,
129    /// The display name (for statusline, etc.).
130    name: &'static str,
131    /// Numeric discriminant for O(1) identity comparison.
132    discriminant: u16,
133}
134
135impl ModeId {
136    /// Create a new mode identifier with default discriminant (0).
137    ///
138    /// This constructor is provided for backward compatibility.
139    /// Prefer `with_discriminant` for new code.
140    #[must_use]
141    pub const fn new(module: ModuleId, name: &'static str) -> Self {
142        Self {
143            module,
144            name,
145            discriminant: 0,
146        }
147    }
148
149    /// Create a new mode identifier with explicit discriminant.
150    ///
151    /// The discriminant should be unique within the module and stable
152    /// across versions for serialization compatibility.
153    #[must_use]
154    pub const fn with_discriminant(
155        module: ModuleId,
156        name: &'static str,
157        discriminant: u16,
158    ) -> Self {
159        Self {
160            module,
161            name,
162            discriminant,
163        }
164    }
165
166    /// Get the owning module.
167    #[must_use]
168    pub const fn module(&self) -> &ModuleId {
169        &self.module
170    }
171
172    /// Get the display name.
173    #[must_use]
174    pub const fn name(&self) -> &'static str {
175        self.name
176    }
177
178    /// Get the numeric discriminant.
179    #[must_use]
180    pub const fn discriminant(&self) -> u16 {
181        self.discriminant
182    }
183}
184
185// Manual PartialEq: equality based on module + discriminant only
186impl PartialEq for ModeId {
187    fn eq(&self, other: &Self) -> bool {
188        self.module == other.module && self.discriminant == other.discriminant
189    }
190}
191
192impl Eq for ModeId {}
193
194// Manual Hash: hash based on module + discriminant only
195impl std::hash::Hash for ModeId {
196    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
197        self.module.hash(state);
198        self.discriminant.hash(state);
199    }
200}
201
202impl fmt::Display for ModeId {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(f, "{}:{}", self.module, self.name)
205    }
206}
207
208// ============================================================================
209// CommandId
210// ============================================================================
211
212/// Namespaced command identifier.
213///
214/// Commands are identified by their owning module and a local name.
215/// This prevents naming conflicts between modules.
216///
217/// # Example
218///
219/// ```
220/// use reovim_kernel::api::v1::{CommandId, ModuleId};
221///
222/// let editor_module = ModuleId::new("editor");
223/// let cursor_down = CommandId::new(editor_module.clone(), "cursor-down");
224/// let cursor_up = CommandId::new(editor_module, "cursor-up");
225///
226/// assert_eq!(cursor_down.name(), "cursor-down");
227/// assert_ne!(cursor_down, cursor_up);
228/// ```
229#[derive(Debug, Clone, PartialEq, Eq, Hash)]
230pub struct CommandId {
231    /// The module that owns this command.
232    module: ModuleId,
233    /// The local name within the module.
234    name: &'static str,
235}
236
237impl CommandId {
238    /// Create a new command identifier.
239    #[must_use]
240    pub const fn new(module: ModuleId, name: &'static str) -> Self {
241        Self { module, name }
242    }
243
244    /// Create a command identifier from a qualified string like "module:command".
245    ///
246    /// This method is intended for dynamic use cases like FFI where command IDs
247    /// are specified as strings at runtime. The strings are leaked to get
248    /// `'static` lifetime, so this should only be used for long-lived commands.
249    ///
250    /// If the string doesn't contain ':', the entire string is treated as the
251    /// command name with "unknown" as the module.
252    ///
253    /// # Example
254    ///
255    /// ```
256    /// use reovim_kernel::api::v1::CommandId;
257    ///
258    /// let cmd = CommandId::from_qualified_leaked("editor:cursor-down".to_string());
259    /// assert_eq!(cmd.module().as_str(), "editor");
260    /// assert_eq!(cmd.name(), "cursor-down");
261    /// ```
262    #[must_use]
263    pub fn from_qualified_leaked(qualified: String) -> Self {
264        let (module_str, name_str) = if let Some(idx) = qualified.find(':') {
265            (qualified[..idx].to_string(), qualified[idx + 1..].to_string())
266        } else {
267            ("unknown".to_string(), qualified)
268        };
269
270        let module_static: &'static str = Box::leak(module_str.into_boxed_str());
271        let name_static: &'static str = Box::leak(name_str.into_boxed_str());
272
273        Self {
274            module: ModuleId::new(module_static),
275            name: name_static,
276        }
277    }
278
279    /// Get the owning module.
280    #[must_use]
281    pub const fn module(&self) -> &ModuleId {
282        &self.module
283    }
284
285    /// Get the local name.
286    #[must_use]
287    pub const fn name(&self) -> &'static str {
288        self.name
289    }
290}
291
292impl fmt::Display for CommandId {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        write!(f, "{}:{}", self.module, self.name)
295    }
296}
297
298// Note: OperatorId was removed from kernel (Epic #385).
299// Operators are vim-specific policy - OperatorId now lives in modules/vim/src/ids.rs
300
301// ============================================================================
302// Mode Trait
303// ============================================================================
304
305/// Mode trait for type-safe, compile-time enforced mode ownership.
306///
307/// Policy modules (e.g., vim, emacs) define their own Mode enums and implement
308/// this trait. The trait provides both identity and behavior information,
309/// allowing the kernel and drivers to query mode properties without knowing
310/// the concrete type.
311///
312/// # Type Safety
313///
314/// The trait requires `Copy + Clone + PartialEq + Eq + Hash` to ensure modes
315/// are lightweight value types that can be efficiently compared and stored.
316/// This also makes the trait **not object-safe**, which is intentional:
317/// runtime storage uses `ModeId`, not `dyn Mode`.
318///
319/// # Blanket Implementation
320///
321/// All `Mode` types automatically implement `From<M> for ModeId`, allowing
322/// ergonomic conversion when storing modes in `ModeStack` or `ModeRegistry`.
323///
324/// # Example
325///
326/// ```ignore
327/// use reovim_kernel::api::v1::{Mode, ModeId, ModuleId, CursorStyle};
328///
329/// const VIM_MODULE: ModuleId = ModuleId::new("vim");
330///
331/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
332/// #[repr(u16)]
333/// enum VimMode {
334///     Normal = 0,
335///     Insert = 1,
336///     Visual = 2,
337/// }
338///
339/// impl Mode for VimMode {
340///     fn module() -> ModuleId { VIM_MODULE }
341///
342///     fn discriminant(&self) -> u16 { *self as u16 }
343///
344///     fn display_name(&self) -> &'static str {
345///         match self {
346///             Self::Normal => "NORMAL",
347///             Self::Insert => "INSERT",
348///             Self::Visual => "VISUAL",
349///         }
350///     }
351///
352///     fn cursor_style(&self) -> CursorStyle {
353///         match self {
354///             Self::Insert => CursorStyle::Bar,
355///             _ => CursorStyle::Block,
356///         }
357///     }
358///
359///     fn accepts_char_input(&self) -> bool {
360///         matches!(self, Self::Insert)
361///     }
362/// }
363/// ```
364pub trait Mode: Copy + Clone + PartialEq + Eq + Hash + Send + Sync + 'static {
365    /// The module that owns this mode type.
366    ///
367    /// This is a type-level constant, not an instance method.
368    fn module() -> ModuleId
369    where
370        Self: Sized;
371
372    /// Get the unique discriminant for this mode variant.
373    ///
374    /// For `#[repr(u16)]` enums, this is typically `*self as u16`.
375    /// Discriminants must be stable across versions for serialization.
376    fn discriminant(&self) -> u16;
377
378    /// Convert to the runtime storage type.
379    ///
380    /// Default implementation creates a `ModeId` from module, `display_name`,
381    /// and discriminant. Override only if custom behavior is needed.
382    fn id(&self) -> ModeId
383    where
384        Self: Sized,
385    {
386        ModeId::with_discriminant(Self::module(), self.display_name(), self.discriminant())
387    }
388
389    /// Get the display name for this mode.
390    ///
391    /// This is shown in the statusline (e.g., "NORMAL", "INSERT", "VISUAL").
392    fn display_name(&self) -> &'static str;
393
394    /// Get the cursor style for this mode.
395    ///
396    /// Different modes typically use different cursor styles:
397    /// - Normal mode: Block cursor
398    /// - Insert mode: Bar cursor
399    /// - Replace mode: Underline cursor
400    fn cursor_style(&self) -> CursorStyle;
401
402    /// Whether this mode accepts direct character input.
403    ///
404    /// Returns `true` for modes like Insert, `CommandLine`, Replace.
405    /// Returns `false` for Normal, Visual, `OperatorPending`.
406    fn accepts_char_input(&self) -> bool;
407
408    /// Whether this mode has an active selection.
409    ///
410    /// Returns `true` for Visual, Select modes.
411    /// Default is `false`.
412    // LLVM coverage artifact: default trait method body is a single `false` literal;
413    // LLVM marks the closing brace DA:0 even when the method is called through overrides.
414    #[cfg_attr(coverage_nightly, coverage(off))]
415    fn has_selection(&self) -> bool {
416        false
417    }
418
419    /// Get the parent mode for keybinding inheritance.
420    ///
421    /// For example, `VisualLine` might inherit from Visual, which inherits
422    /// from Normal. Returns `None` for root modes.
423    // LLVM coverage artifact: default trait method body is a single `None` literal;
424    // LLVM marks the closing brace DA:0 even when the method is called through overrides.
425    #[cfg_attr(coverage_nightly, coverage(off))]
426    fn inherits_from(&self) -> Option<Self>
427    where
428        Self: Sized,
429    {
430        None
431    }
432
433    /// Whether this is the entry/default mode for new sessions.
434    ///
435    /// Only one mode per module should return true. The first mode
436    /// registered with `is_entry() = true` becomes the session's initial mode.
437    ///
438    /// Default is `false`.
439    fn is_entry(&self) -> bool {
440        false
441    }
442}
443
444/// Blanket implementation: all Mode types convert to `ModeId`.
445///
446/// This allows ergonomic usage:
447/// ```ignore
448/// let stack = ModeStack::new(VimMode::Normal);  // No .into() needed
449/// stack.push(VimMode::Insert);
450/// ```
451impl<M: Mode> From<M> for ModeId {
452    fn from(mode: M) -> Self {
453        mode.id()
454    }
455}
456
457// ============================================================================
458// ModeStack
459// ============================================================================
460
461/// Mode stack for push/pop mode switching.
462///
463/// Supports vim-style mode stacking where modes can be pushed and popped.
464/// For example, entering operator-pending mode pushes onto the stack,
465/// and completing/canceling the operation pops back.
466///
467/// # Generic Mode Support
468///
469/// All methods accept `impl Into<ModeId>`, allowing both `ModeId` and
470/// any type implementing `Mode` to be used directly:
471///
472/// ```ignore
473/// let mut stack = ModeStack::new(VimMode::Normal);  // VimMode implements Mode
474/// stack.push(VimMode::OperatorPending);
475/// ```
476///
477/// # Example
478///
479/// ```
480/// use reovim_kernel::api::v1::{ModeId, ModeStack, ModuleId};
481///
482/// let module = ModuleId::new("editor");
483/// let normal = ModeId::with_discriminant(module.clone(), "NORMAL", 0);
484/// let insert = ModeId::with_discriminant(module.clone(), "INSERT", 1);
485/// let op_pending = ModeId::with_discriminant(module, "OP-PEND", 5);
486///
487/// let mut stack = ModeStack::new(normal.clone());
488/// assert_eq!(stack.current(), &normal);
489///
490/// // Push operator-pending mode
491/// stack.push(op_pending.clone());
492/// assert_eq!(stack.current(), &op_pending);
493///
494/// // Pop back to normal
495/// assert_eq!(stack.pop(), Some(op_pending));
496/// assert_eq!(stack.current(), &normal);
497///
498/// // Set directly to insert (replaces current)
499/// stack.set(insert.clone());
500/// assert_eq!(stack.current(), &insert);
501/// ```
502#[derive(Debug, Clone)]
503pub struct ModeStack {
504    /// The mode stack. Always has at least one element.
505    stack: Vec<ModeId>,
506}
507
508impl ModeStack {
509    /// Create a new mode stack with an initial mode.
510    ///
511    /// Accepts any type that implements `Into<ModeId>`, including
512    /// `ModeId` itself and any type implementing `Mode`.
513    #[must_use]
514    pub fn new<M: Into<ModeId>>(initial: M) -> Self {
515        Self {
516            stack: vec![initial.into()],
517        }
518    }
519
520    /// Get the current (top) mode.
521    ///
522    /// # Panics
523    ///
524    /// This function will never panic in normal use. It only panics if the internal
525    /// invariant (stack has at least one element) is violated, which indicates a bug.
526    #[must_use]
527    pub fn current(&self) -> &ModeId {
528        // Safety: stack always has at least one element (invariant maintained by all methods)
529        self.stack.last().expect("mode stack is never empty")
530    }
531
532    /// Push a new mode onto the stack.
533    ///
534    /// Accepts any type that implements `Into<ModeId>`.
535    pub fn push<M: Into<ModeId>>(&mut self, mode: M) {
536        self.stack.push(mode.into());
537    }
538
539    /// Pop the top mode from the stack.
540    ///
541    /// Returns `None` if only one mode remains (cannot pop the base mode).
542    pub fn pop(&mut self) -> Option<ModeId> {
543        if self.stack.len() > 1 {
544            self.stack.pop()
545        } else {
546            None
547        }
548    }
549
550    /// Set the current mode, replacing the top of the stack.
551    ///
552    /// This is equivalent to pop + push, but works even when only one mode exists.
553    /// Accepts any type that implements `Into<ModeId>`.
554    #[cfg_attr(coverage_nightly, coverage(off))]
555    pub fn set<M: Into<ModeId>>(&mut self, mode: M) {
556        if let Some(last) = self.stack.last_mut() {
557            *last = mode.into();
558        }
559    }
560
561    /// Get the stack depth.
562    #[must_use]
563    pub const fn depth(&self) -> usize {
564        self.stack.len()
565    }
566
567    /// Check if we're in the base mode (stack depth is 1).
568    #[must_use]
569    pub const fn is_base(&self) -> bool {
570        self.stack.len() == 1
571    }
572
573    /// Get all modes in the stack (bottom to top).
574    #[must_use]
575    pub fn as_slice(&self) -> &[ModeId] {
576        &self.stack
577    }
578
579    /// Get the home (base) mode.
580    ///
581    /// The home mode is the first mode pushed onto the stack and cannot be popped.
582    ///
583    /// # Panics
584    ///
585    /// This function will never panic in normal use. It only panics if the internal
586    /// invariant (stack has at least one element) is violated, which indicates a bug.
587    #[must_use]
588    pub fn home(&self) -> &ModeId {
589        // Safety: stack always has at least one element (invariant maintained by all methods)
590        self.stack.first().expect("mode stack is never empty")
591    }
592
593    /// Check if a mode is anywhere in the stack.
594    #[must_use]
595    pub fn contains(&self, mode_id: &ModeId) -> bool {
596        self.stack.contains(mode_id)
597    }
598}