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}