Skip to main content

reovim_kernel/core/
register.rs

1//! Register storage and addressing for yank/paste operations.
2//!
3//! This module provides:
4//! - **`Register`** - Type-safe register addressing (mechanism names, not vim terms)
5//! - **`RegisterBank`** - Pure register storage without clipboard integration
6//! - **`RegisterContent`** - Content stored in a register (text + yank type)
7//!
8//! System clipboard access is a driver-level concern.
9//! Session-scoped registers are stored at the session level, not here.
10
11use std::collections::HashMap;
12
13/// Type of yank operation.
14///
15/// This affects how paste operations behave:
16/// - **Characterwise**: Paste at cursor position
17/// - **Linewise**: Paste below/above current line
18///
19/// # Example
20///
21/// ```
22/// use reovim_kernel::api::v1::*;
23///
24/// // yw yanks characterwise
25/// let char_yank = YankType::Characterwise;
26///
27/// // yy yanks linewise
28/// let line_yank = YankType::Linewise;
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
31pub enum YankType {
32    /// Characterwise yank (e.g., yw, y$, d2w)
33    #[default]
34    Characterwise,
35    /// Linewise yank (e.g., yy, yj, dd)
36    Linewise,
37}
38
39/// Content stored in a register.
40///
41/// Combines the yanked text with its yank type for proper paste behavior.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct RegisterContent {
44    /// The yanked text.
45    pub text: String,
46    /// How the text was yanked (affects paste behavior).
47    pub yank_type: YankType,
48}
49
50impl RegisterContent {
51    /// Create new register content with specified yank type.
52    #[must_use]
53    pub fn new(text: impl Into<String>, yank_type: YankType) -> Self {
54        Self {
55            text: text.into(),
56            yank_type,
57        }
58    }
59
60    /// Create characterwise register content.
61    #[must_use]
62    pub fn characterwise(text: impl Into<String>) -> Self {
63        Self::new(text, YankType::Characterwise)
64    }
65
66    /// Create linewise register content.
67    #[must_use]
68    pub fn linewise(text: impl Into<String>) -> Self {
69        Self::new(text, YankType::Linewise)
70    }
71
72    /// Check if content is empty.
73    #[must_use]
74    #[allow(clippy::missing_const_for_fn)] // String::is_empty is not const
75    pub fn is_empty(&self) -> bool {
76        self.text.is_empty()
77    }
78
79    /// Check if content is characterwise.
80    #[must_use]
81    pub const fn is_characterwise(&self) -> bool {
82        matches!(self.yank_type, YankType::Characterwise)
83    }
84
85    /// Check if content is linewise.
86    #[must_use]
87    pub const fn is_linewise(&self) -> bool {
88        matches!(self.yank_type, YankType::Linewise)
89    }
90}
91
92impl Default for RegisterContent {
93    fn default() -> Self {
94        Self {
95            text: String::new(),
96            yank_type: YankType::Characterwise,
97        }
98    }
99}
100
101// ============================================================================
102// Register Addressing
103// ============================================================================
104
105/// Register addressing for the kernel register subsystem.
106///
107/// Represents storage locations using mechanism names (not editor-specific
108/// terminology). The kernel provides WHAT registers exist; modules decide
109/// HOW they map to user-facing keys.
110///
111/// # Storage Routing
112///
113/// Different variants are stored in different subsystems:
114///
115/// | Variant | Storage | Mutability |
116/// |---------|---------|------------|
117/// | `Default` | Per-client `RegisterBank` | Read/Write |
118/// | `Slot(char)` | Per-client `RegisterBank` | Read/Write |
119/// | `History(u8)` | Per-client `HistoryRing` | Read-only |
120/// | `System` | OS clipboard (driver) | Read/Write |
121/// | `Session(char)` | Session-level shared storage | Read/Write |
122/// | `PeerHistory` | Another client's `HistoryRing` | Read-only |
123///
124/// # Example
125///
126/// ```
127/// use reovim_kernel::api::v1::Register;
128///
129/// let default = Register::Default;
130/// assert!(default.is_bank_register());
131/// assert!(!default.is_read_only());
132///
133/// let slot = Register::Slot('a');
134/// assert!(slot.is_bank_register());
135///
136/// let history = Register::History(0);
137/// assert!(history.is_read_only());
138///
139/// let session = Register::Session('A');
140/// assert!(session.is_session_scoped());
141/// ```
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub enum Register {
144    /// The default/fallback register.
145    Default,
146    /// A keyed storage slot (a-z, 26 per-client slots).
147    Slot(char),
148    /// Index into the per-client history ring (0-255).
149    History(u8),
150    /// System clipboard (OS-level).
151    System,
152    /// Session-scoped shared register (A-Z, shared across all clients).
153    Session(char),
154    /// Read another client's history ring entry.
155    PeerHistory {
156        /// Target client identifier.
157        client: usize,
158        /// History ring index (0-255).
159        index: u8,
160    },
161}
162
163impl Register {
164    /// Whether this register is stored in per-client `RegisterBank`.
165    #[must_use]
166    pub const fn is_bank_register(&self) -> bool {
167        matches!(self, Self::Default | Self::Slot(_))
168    }
169
170    /// Whether this register is read-only.
171    ///
172    /// History and peer history registers cannot be written to directly.
173    /// They are populated as side effects of other operations.
174    #[must_use]
175    pub const fn is_read_only(&self) -> bool {
176        matches!(self, Self::History(_) | Self::PeerHistory { .. })
177    }
178
179    /// Whether this register requires session-level access.
180    ///
181    /// Session registers and peer history both need access to shared
182    /// session state rather than per-client state.
183    #[must_use]
184    pub const fn is_session_scoped(&self) -> bool {
185        matches!(self, Self::Session(_) | Self::PeerHistory { .. })
186    }
187}
188
189/// Register storage for yank/paste operations.
190///
191/// This is a pure data structure without any system clipboard integration.
192/// System clipboard access (`"+` and `"*`) is handled at the driver level.
193///
194/// # Supported Registers
195///
196/// - Unnamed register (`""`) - Default for all operations
197/// - Named registers (`"a` to `"z`) - User-specified storage
198///
199/// # Example
200///
201/// ```
202/// use reovim_kernel::api::v1::*;
203///
204/// let mut bank = RegisterBank::new();
205///
206/// // Set unnamed register (default yank target)
207/// bank.set(RegisterContent::characterwise("hello"));
208/// assert_eq!(bank.get().text, "hello");
209///
210/// // Use named register
211/// bank.set_named('a', RegisterContent::linewise("line content"));
212/// assert_eq!(bank.get_named('a').map(|r| r.text.as_str()), Some("line content"));
213/// ```
214#[derive(Debug, Clone)]
215pub struct RegisterBank {
216    /// Unnamed register (default target).
217    unnamed: RegisterContent,
218    /// Named registers (a-z).
219    named: HashMap<char, RegisterContent>,
220}
221
222impl RegisterBank {
223    /// Create a new empty register bank.
224    #[must_use]
225    pub fn new() -> Self {
226        Self {
227            unnamed: RegisterContent::default(),
228            named: HashMap::new(),
229        }
230    }
231
232    /// Get the unnamed register content.
233    #[must_use]
234    pub const fn get(&self) -> &RegisterContent {
235        &self.unnamed
236    }
237
238    /// Set the unnamed register content.
239    pub fn set(&mut self, content: RegisterContent) {
240        self.unnamed = content;
241    }
242
243    /// Get a named register content ('a'-'z').
244    ///
245    /// Returns `None` if the register name is invalid or empty.
246    #[must_use]
247    pub fn get_named(&self, name: char) -> Option<&RegisterContent> {
248        if name.is_ascii_lowercase() {
249            self.named.get(&name)
250        } else {
251            None
252        }
253    }
254
255    /// Set a named register content ('a'-'z').
256    ///
257    /// Returns `true` if successful, `false` if the register name is invalid.
258    pub fn set_named(&mut self, name: char, content: RegisterContent) -> bool {
259        if name.is_ascii_lowercase() {
260            self.named.insert(name, content);
261            true
262        } else {
263            false
264        }
265    }
266
267    /// Append to a named register ('A'-'Z' appends to 'a'-'z').
268    ///
269    /// Returns `true` if successful, `false` if the register name is invalid.
270    pub fn append_named(&mut self, name: char, content: &str) -> bool {
271        if name.is_ascii_uppercase() {
272            let lower = name.to_ascii_lowercase();
273            if let Some(existing) = self.named.get_mut(&lower) {
274                existing.text.push_str(content);
275            } else {
276                self.named
277                    .insert(lower, RegisterContent::characterwise(content.to_string()));
278            }
279            true
280        } else {
281            false
282        }
283    }
284
285    /// Clear the unnamed register.
286    pub fn clear(&mut self) {
287        self.unnamed = RegisterContent::default();
288    }
289
290    /// Clear a named register.
291    ///
292    /// Returns `true` if the register existed and was cleared.
293    pub fn clear_named(&mut self, name: char) -> bool {
294        if name.is_ascii_lowercase() {
295            self.named.remove(&name).is_some()
296        } else {
297            false
298        }
299    }
300
301    /// Clear all registers.
302    pub fn clear_all(&mut self) {
303        self.unnamed = RegisterContent::default();
304        self.named.clear();
305    }
306
307    /// Get register by name.
308    ///
309    /// - `None` or `'"'` returns the unnamed register
310    /// - `'a'`-`'z'` returns named registers
311    #[must_use]
312    pub fn get_by_name(&self, name: Option<char>) -> Option<&RegisterContent> {
313        match name {
314            None | Some('"') => Some(&self.unnamed),
315            Some(c) if c.is_ascii_lowercase() => self.named.get(&c),
316            _ => None,
317        }
318    }
319
320    /// Iterate over all non-empty registers.
321    ///
322    /// Returns an iterator of (name, content) pairs.
323    /// The unnamed register uses `'"'` as its name.
324    pub fn iter_non_empty(&self) -> impl Iterator<Item = (char, &RegisterContent)> {
325        // Start with unnamed register if non-empty
326        let unnamed_iter = if self.unnamed.is_empty() {
327            None
328        } else {
329            Some(('"', &self.unnamed))
330        };
331
332        // Chain with named registers, filtering empty ones
333        unnamed_iter.into_iter().chain(
334            self.named
335                .iter()
336                .filter(|(_, content)| !content.is_empty())
337                .map(|(name, content)| (*name, content)),
338        )
339    }
340
341    // ========================================================================
342    // Register-typed accessors (#515 Phase 5)
343    // ========================================================================
344
345    /// Get register content by typed `Register`.
346    ///
347    /// Returns `None` for `History`, `System`, `Session`, and `PeerHistory`
348    /// variants (not stored in the per-client bank).
349    ///
350    /// # Example
351    ///
352    /// ```
353    /// use reovim_kernel::api::v1::*;
354    ///
355    /// let mut bank = RegisterBank::new();
356    /// bank.set(RegisterContent::characterwise("hello"));
357    ///
358    /// assert_eq!(bank.get_register(&Register::Default).map(|r| r.text.as_str()), Some("hello"));
359    /// assert!(bank.get_register(&Register::System).is_none());
360    /// ```
361    #[must_use]
362    pub fn get_register(&self, reg: &Register) -> Option<&RegisterContent> {
363        match reg {
364            Register::Default => Some(&self.unnamed),
365            Register::Slot(c) if c.is_ascii_lowercase() => self.named.get(c),
366            _ => None,
367        }
368    }
369
370    /// Set register content by typed `Register`.
371    ///
372    /// Returns `false` for read-only or non-bank registers.
373    ///
374    /// # Example
375    ///
376    /// ```
377    /// use reovim_kernel::api::v1::*;
378    ///
379    /// let mut bank = RegisterBank::new();
380    /// assert!(bank.set_register(&Register::Slot('a'), RegisterContent::characterwise("alpha")));
381    /// assert!(!bank.set_register(&Register::System, RegisterContent::characterwise("nope")));
382    /// ```
383    pub fn set_register(&mut self, reg: &Register, content: RegisterContent) -> bool {
384        match reg {
385            Register::Default => {
386                self.unnamed = content;
387                true
388            }
389            Register::Slot(c) if c.is_ascii_lowercase() => {
390                self.named.insert(*c, content);
391                true
392            }
393            _ => false,
394        }
395    }
396
397    /// Append content to a slot register (lowercase a-z).
398    ///
399    /// If the slot doesn't exist, it is created with the given content.
400    /// Returns `false` if the slot character is not lowercase a-z.
401    pub fn append_slot(&mut self, slot: char, content: &str) -> bool {
402        if !slot.is_ascii_lowercase() {
403            return false;
404        }
405        if let Some(existing) = self.named.get_mut(&slot) {
406            existing.text.push_str(content);
407        } else {
408            self.named
409                .insert(slot, RegisterContent::characterwise(content.to_string()));
410        }
411        true
412    }
413
414    /// Set register by name.
415    ///
416    /// - `None` or `'"'` sets the unnamed register
417    /// - `'a'`-`'z'` sets named registers
418    /// - `'A'`-`'Z'` appends to named registers
419    ///
420    /// Returns `true` if successful.
421    pub fn set_by_name(&mut self, name: Option<char>, content: RegisterContent) -> bool {
422        match name {
423            None | Some('"') => {
424                self.unnamed = content;
425                true
426            }
427            Some(c) if c.is_ascii_lowercase() => {
428                self.named.insert(c, content);
429                true
430            }
431            Some(c) if c.is_ascii_uppercase() => {
432                let lower = c.to_ascii_lowercase();
433                if let Some(existing) = self.named.get_mut(&lower) {
434                    existing.text.push_str(&content.text);
435                } else {
436                    self.named.insert(lower, content);
437                }
438                true
439            }
440            _ => false,
441        }
442    }
443}
444
445impl Default for RegisterBank {
446    fn default() -> Self {
447        Self::new()
448    }
449}