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}