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}