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::{borrow::Cow, 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: Cow<'static, str>,
235}
236
237impl CommandId {
238    /// Create a new command identifier from static strings.
239    ///
240    /// This is the preferred way to create command IDs for statically-known commands.
241    /// It's a const fn and involves no allocation.
242    #[must_use]
243    pub const fn new(module: ModuleId, name: &'static str) -> Self {
244        Self {
245            module,
246            name: Cow::Borrowed(name),
247        }
248    }
249
250    /// Create a command identifier from owned strings.
251    ///
252    /// Use this for dynamically-generated command IDs (e.g., picker selections,
253    /// FFI calls, manifest parsing).
254    #[must_use]
255    #[allow(clippy::missing_const_for_fn)] // String operations aren't const-stable
256    pub fn from_owned(module: ModuleId, name: String) -> Self {
257        Self {
258            module,
259            name: Cow::Owned(name),
260        }
261    }
262
263    /// Create a command identifier from a qualified string like "module:command".
264    ///
265    /// This method is intended for dynamic use cases like FFI where command IDs
266    /// are specified as strings at runtime.
267    ///
268    /// If the string doesn't contain ':', the entire string is treated as the
269    /// command name with "unknown" as the module.
270    ///
271    /// # Example
272    ///
273    /// ```
274    /// use reovim_kernel::api::v1::CommandId;
275    ///
276    /// let cmd = CommandId::from_qualified("editor:cursor-down".to_string());
277    /// assert_eq!(cmd.module().as_str(), "editor");
278    /// assert_eq!(cmd.name(), "cursor-down");
279    /// ```
280    #[must_use]
281    pub fn from_qualified(qualified: String) -> Self {
282        let (module_str, name_str) = if let Some(idx) = qualified.find(':') {
283            (qualified[..idx].to_string(), qualified[idx + 1..].to_string())
284        } else {
285            ("unknown".to_string(), qualified)
286        };
287
288        Self {
289            module: ModuleId::from_string(module_str),
290            name: Cow::Owned(name_str),
291        }
292    }
293
294    /// Get the owning module.
295    #[must_use]
296    pub const fn module(&self) -> &ModuleId {
297        &self.module
298    }
299
300    /// Get the local name.
301    #[must_use]
302    pub fn name(&self) -> &str {
303        // SAFETY NOTE: This returns &str borrowing from &self.
304        // If you need a value that outlives the CommandId (e.g. calling
305        // .name() on a temporary), use .name_owned() instead.
306        &self.name
307    }
308
309    /// Get the local name as an owned `Cow`.
310    ///
311    /// Use this when you need a value that isn't tied to the `CommandId`'s
312    /// lifetime (e.g., when calling `.id().name_owned()` on a temporary).
313    #[must_use]
314    pub fn name_owned(&self) -> Cow<'static, str> {
315        self.name.clone()
316    }
317}
318
319impl fmt::Display for CommandId {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        write!(f, "{}:{}", self.module, self.name)
322    }
323}
324
325// Note: OperatorId was removed from kernel (Epic #385).
326// Operators are vim-specific policy - OperatorId now lives in modules/vim/src/ids.rs
327
328// ============================================================================
329// Mode Trait
330// ============================================================================
331
332/// Mode trait for type-safe, compile-time enforced mode ownership.
333///
334/// Policy modules (e.g., vim, emacs) define their own Mode enums and implement
335/// this trait. The trait provides both identity and behavior information,
336/// allowing the kernel and drivers to query mode properties without knowing
337/// the concrete type.
338///
339/// # Type Safety
340///
341/// The trait requires `Copy + Clone + PartialEq + Eq + Hash` to ensure modes
342/// are lightweight value types that can be efficiently compared and stored.
343/// This also makes the trait **not object-safe**, which is intentional:
344/// runtime storage uses `ModeId`, not `dyn Mode`.
345///
346/// # Blanket Implementation
347///
348/// All `Mode` types automatically implement `From<M> for ModeId`, allowing
349/// ergonomic conversion when storing modes in `ModeStack` or `ModeRegistry`.
350///
351/// # Example
352///
353/// ```ignore
354/// use reovim_kernel::api::v1::{Mode, ModeId, ModuleId, CursorStyle};
355///
356/// const VIM_MODULE: ModuleId = ModuleId::new("vim");
357///
358/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
359/// #[repr(u16)]
360/// enum VimMode {
361///     Normal = 0,
362///     Insert = 1,
363///     Visual = 2,
364/// }
365///
366/// impl Mode for VimMode {
367///     fn module() -> ModuleId { VIM_MODULE }
368///
369///     fn discriminant(&self) -> u16 { *self as u16 }
370///
371///     fn display_name(&self) -> &'static str {
372///         match self {
373///             Self::Normal => "NORMAL",
374///             Self::Insert => "INSERT",
375///             Self::Visual => "VISUAL",
376///         }
377///     }
378///
379///     fn cursor_style(&self) -> CursorStyle {
380///         match self {
381///             Self::Insert => CursorStyle::Bar,
382///             _ => CursorStyle::Block,
383///         }
384///     }
385///
386///     fn accepts_char_input(&self) -> bool {
387///         matches!(self, Self::Insert)
388///     }
389/// }
390/// ```
391pub trait Mode: Copy + Clone + PartialEq + Eq + Hash + Send + Sync + 'static {
392    /// The module that owns this mode type.
393    ///
394    /// This is a type-level constant, not an instance method.
395    fn module() -> ModuleId
396    where
397        Self: Sized;
398
399    /// Get the unique discriminant for this mode variant.
400    ///
401    /// For `#[repr(u16)]` enums, this is typically `*self as u16`.
402    /// Discriminants must be stable across versions for serialization.
403    fn discriminant(&self) -> u16;
404
405    /// Convert to the runtime storage type.
406    ///
407    /// Default implementation creates a `ModeId` from module, `display_name`,
408    /// and discriminant. Override only if custom behavior is needed.
409    fn id(&self) -> ModeId
410    where
411        Self: Sized,
412    {
413        ModeId::with_discriminant(Self::module(), self.display_name(), self.discriminant())
414    }
415
416    /// Get the display name for this mode.
417    ///
418    /// This is shown in the statusline (e.g., "NORMAL", "INSERT", "VISUAL").
419    fn display_name(&self) -> &'static str;
420
421    /// Get the cursor style for this mode.
422    ///
423    /// Different modes typically use different cursor styles:
424    /// - Normal mode: Block cursor
425    /// - Insert mode: Bar cursor
426    /// - Replace mode: Underline cursor
427    fn cursor_style(&self) -> CursorStyle;
428
429    /// Whether this mode accepts direct character input.
430    ///
431    /// Returns `true` for modes like Insert, `CommandLine`, Replace.
432    /// Returns `false` for Normal, Visual, `OperatorPending`.
433    fn accepts_char_input(&self) -> bool;
434
435    /// Whether this mode has an active selection.
436    ///
437    /// Returns `true` for Visual, Select modes.
438    /// Default is `false`.
439    // LLVM coverage artifact: default trait method body is a single `false` literal;
440    // LLVM marks the closing brace DA:0 even when the method is called through overrides.
441    #[cfg_attr(coverage_nightly, coverage(off))]
442    fn has_selection(&self) -> bool {
443        false
444    }
445
446    /// Get the parent mode for keybinding inheritance.
447    ///
448    /// For example, `VisualLine` might inherit from Visual, which inherits
449    /// from Normal. Returns `None` for root modes.
450    // LLVM coverage artifact: default trait method body is a single `None` literal;
451    // LLVM marks the closing brace DA:0 even when the method is called through overrides.
452    #[cfg_attr(coverage_nightly, coverage(off))]
453    fn inherits_from(&self) -> Option<Self>
454    where
455        Self: Sized,
456    {
457        None
458    }
459
460    /// Whether this is the entry/default mode for new sessions.
461    ///
462    /// Only one mode per module should return true. The first mode
463    /// registered with `is_entry() = true` becomes the session's initial mode.
464    ///
465    /// Default is `false`.
466    fn is_entry(&self) -> bool {
467        false
468    }
469}
470
471/// Blanket implementation: all Mode types convert to `ModeId`.
472///
473/// This allows ergonomic usage:
474/// ```ignore
475/// let stack = ModeStack::new(VimMode::Normal);  // No .into() needed
476/// stack.push(VimMode::Insert);
477/// ```
478impl<M: Mode> From<M> for ModeId {
479    fn from(mode: M) -> Self {
480        mode.id()
481    }
482}
483
484// ============================================================================
485// ModeStack
486// ============================================================================
487
488/// Mode stack for push/pop mode switching.
489///
490/// Supports vim-style mode stacking where modes can be pushed and popped.
491/// For example, entering operator-pending mode pushes onto the stack,
492/// and completing/canceling the operation pops back.
493///
494/// # Generic Mode Support
495///
496/// All methods accept `impl Into<ModeId>`, allowing both `ModeId` and
497/// any type implementing `Mode` to be used directly:
498///
499/// ```ignore
500/// let mut stack = ModeStack::new(VimMode::Normal);  // VimMode implements Mode
501/// stack.push(VimMode::OperatorPending);
502/// ```
503///
504/// # Example
505///
506/// ```
507/// use reovim_kernel::api::v1::{ModeId, ModeStack, ModuleId};
508///
509/// let module = ModuleId::new("editor");
510/// let normal = ModeId::with_discriminant(module.clone(), "NORMAL", 0);
511/// let insert = ModeId::with_discriminant(module.clone(), "INSERT", 1);
512/// let op_pending = ModeId::with_discriminant(module, "OP-PEND", 5);
513///
514/// let mut stack = ModeStack::new(normal.clone());
515/// assert_eq!(stack.current(), &normal);
516///
517/// // Push operator-pending mode
518/// stack.push(op_pending.clone());
519/// assert_eq!(stack.current(), &op_pending);
520///
521/// // Pop back to normal
522/// assert_eq!(stack.pop(), Some(op_pending));
523/// assert_eq!(stack.current(), &normal);
524///
525/// // Set directly to insert (replaces current)
526/// stack.set(insert.clone());
527/// assert_eq!(stack.current(), &insert);
528/// ```
529#[derive(Debug, Clone)]
530pub struct ModeStack {
531    /// The mode stack. Always has at least one element.
532    stack: Vec<ModeId>,
533}
534
535impl ModeStack {
536    /// Create a new mode stack with an initial mode.
537    ///
538    /// Accepts any type that implements `Into<ModeId>`, including
539    /// `ModeId` itself and any type implementing `Mode`.
540    #[must_use]
541    pub fn new<M: Into<ModeId>>(initial: M) -> Self {
542        Self {
543            stack: vec![initial.into()],
544        }
545    }
546
547    /// Get the current (top) mode.
548    ///
549    /// # Panics
550    ///
551    /// This function will never panic in normal use. It only panics if the internal
552    /// invariant (stack has at least one element) is violated, which indicates a bug.
553    #[must_use]
554    pub fn current(&self) -> &ModeId {
555        // Safety: stack always has at least one element (invariant maintained by all methods)
556        self.stack.last().expect("mode stack is never empty")
557    }
558
559    /// Push a new mode onto the stack.
560    ///
561    /// Accepts any type that implements `Into<ModeId>`.
562    pub fn push<M: Into<ModeId>>(&mut self, mode: M) {
563        self.stack.push(mode.into());
564    }
565
566    /// Pop the top mode from the stack.
567    ///
568    /// Returns `None` if only one mode remains (cannot pop the base mode).
569    pub fn pop(&mut self) -> Option<ModeId> {
570        if self.stack.len() > 1 {
571            self.stack.pop()
572        } else {
573            None
574        }
575    }
576
577    /// Set the current mode, replacing the top of the stack.
578    ///
579    /// This is equivalent to pop + push, but works even when only one mode exists.
580    /// Accepts any type that implements `Into<ModeId>`.
581    #[cfg_attr(coverage_nightly, coverage(off))]
582    pub fn set<M: Into<ModeId>>(&mut self, mode: M) {
583        if let Some(last) = self.stack.last_mut() {
584            *last = mode.into();
585        }
586    }
587
588    /// Get the stack depth.
589    #[must_use]
590    pub const fn depth(&self) -> usize {
591        self.stack.len()
592    }
593
594    /// Check if we're in the base mode (stack depth is 1).
595    #[must_use]
596    pub const fn is_base(&self) -> bool {
597        self.stack.len() == 1
598    }
599
600    /// Get all modes in the stack (bottom to top).
601    #[must_use]
602    pub fn as_slice(&self) -> &[ModeId] {
603        &self.stack
604    }
605
606    /// Get the home (base) mode.
607    ///
608    /// The home mode is the first mode pushed onto the stack and cannot be popped.
609    ///
610    /// # Panics
611    ///
612    /// This function will never panic in normal use. It only panics if the internal
613    /// invariant (stack has at least one element) is violated, which indicates a bug.
614    #[must_use]
615    pub fn home(&self) -> &ModeId {
616        // Safety: stack always has at least one element (invariant maintained by all methods)
617        self.stack.first().expect("mode stack is never empty")
618    }
619
620    /// Check if a mode is anywhere in the stack.
621    #[must_use]
622    pub fn contains(&self, mode_id: &ModeId) -> bool {
623        self.stack.contains(mode_id)
624    }
625}
626
627// =========================================================================
628// #713 repro: CommandId::from_qualified now uses Cow<'static, str>.
629// No more Box::leak — memory is freed when CommandId is dropped.
630// =========================================================================
631
632#[cfg(test)]
633mod b2_repro {
634    use super::CommandId;
635
636    #[test]
637    fn b2_from_qualified_no_longer_leaks() {
638        // from_qualified uses Cow::Owned — memory freed on drop.
639        for i in 0..1000 {
640            let cmd = CommandId::from_qualified(format!("module{i}:command{i}"));
641            assert_eq!(cmd.module().as_str(), format!("module{i}"));
642            assert_eq!(cmd.name(), format!("command{i}").as_str());
643        }
644        // All 1000 CommandIds have been dropped — no leaked memory.
645    }
646
647    #[test]
648    fn b2_static_new_still_const() {
649        use crate::api::module::ModuleId;
650        // const fn new() still works with static strings
651        const CMD: CommandId = CommandId::new(ModuleId::new("editor"), "cursor-down");
652        assert_eq!(CMD.name(), "cursor-down");
653        assert_eq!(CMD.module().as_str(), "editor");
654    }
655}