Skip to main content

lattice_grammar/
register.rs

1//! Vim registers (DESIGN.md ยง5.2.2).
2//!
3//! This crate owns only the register *name* -- the `"<X>` a user types
4//! before an operator, carried on a [`crate::CommandInvocation`] and in
5//! [`crate::AppEffect::SelectRegister`]. Storage (contents, the yank ring
6//! the numbered registers project, clipboard mirroring) lives in the host's
7//! `Editor`, which interprets each variant as described below.
8
9use serde::{Deserialize, Serialize};
10
11/// A vim register name.
12///
13/// Parse user input with [`Register::from_input_char`]; the variants that
14/// function never returns ([`Register::Expression`],
15/// [`Register::ReadOnly`]) are modelled for completeness and the WIT mirror
16/// but nothing in the host produces or reads them yet.
17///
18/// # Examples
19///
20/// ```
21/// use lattice_grammar::Register;
22///
23/// assert_eq!(Register::from_input_char('a'), Some(Register::Named('a')));
24/// assert_eq!(Register::from_input_char('3'), Some(Register::Numbered(3)));
25/// assert_eq!(Register::from_input_char('+'), Some(Register::System));
26/// assert_eq!(Register::from_input_char('_'), Some(Register::BlackHole));
27/// // `"` re-selects the default.
28/// assert_eq!(Register::from_input_char('"'), Some(Register::default()));
29/// // `=` (expression) is not bindable through `"<X>`.
30/// assert_eq!(Register::from_input_char('='), None);
31/// ```
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub enum Register {
34    /// `""` -- the default target of every yank / delete / put when no
35    /// register was named. With the `clipboard` option on, reads prefer the
36    /// system clipboard.
37    #[default]
38    Unnamed,
39    /// `"a`-`"z` / `"A`-`"Z`. The char is stored exactly as typed:
40    /// uppercase is a *separate* register, not vim's append-to-lowercase
41    /// form (not implemented).
42    Named(char),
43    /// `"+` / `"*` -- the system clipboard. Both chars map here; writes
44    /// always mirror to the clipboard, reads fall back to the unnamed
45    /// register when the clipboard is unavailable.
46    System,
47    /// `"_` -- the sink. Writes are discarded (and do not clobber the
48    /// unnamed register); reads yield nothing.
49    BlackHole,
50    /// `"=` -- vim's expression register. Modelled, not implemented: no
51    /// input maps to it and the host stores nothing for it.
52    Expression,
53    /// Vim's read-only registers (`".`, `"%`, `":`, `"/`), keyed by their
54    /// char. Modelled, not implemented: no input maps to it.
55    ReadOnly(char),
56    /// `"0`-`"9`. A read is a projection of the host's yank ring (`"0` the
57    /// newest yank, `"1`-`"9` the newest deletes), falling back to an
58    /// explicit write into that slot. Values above 9 are not produced by
59    /// [`Register::from_input_char`].
60    Numbered(u8),
61}
62
63impl Register {
64    /// Map a user-typed register-prefix char (the `<X>` in `"<X>`)
65    /// to a [`Register`] variant. Returns `None` for chars that
66    /// don't name any register (the App treats `None` as "drop
67    /// pending state" -- see `docs/dev/notes/8i-approach.md` slice 8.i.3).
68    ///
69    /// Mirrors vim's `:help registers`: letters name a register,
70    /// digits name the numbered ring, `"` re-selects the unnamed
71    /// register, `_` is the black-hole sink, `+` / `*` are the
72    /// system clipboard (X11 / macOS conventions overlap here).
73    /// Expression / readonly registers aren't user-bindable via
74    /// `"<X>` and intentionally return `None`.
75    pub fn from_input_char(c: char) -> Option<Self> {
76        match c {
77            'a'..='z' | 'A'..='Z' => Some(Register::Named(c)),
78            '0'..='9' => Some(Register::Numbered((c as u8) - b'0')),
79            '"' => Some(Register::Unnamed),
80            '_' => Some(Register::BlackHole),
81            '+' | '*' => Some(Register::System),
82            _ => None,
83        }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    #![allow(clippy::unwrap_used, clippy::panic)]
90    use super::*;
91
92    #[test]
93    fn default_is_unnamed() {
94        assert_eq!(Register::default(), Register::Unnamed);
95    }
96
97    #[test]
98    fn named_registers_are_distinct_by_letter() {
99        assert_ne!(Register::Named('a'), Register::Named('b'));
100        assert_eq!(Register::Named('a'), Register::Named('a'));
101    }
102
103    #[test]
104    fn numbered_registers_distinct_by_index() {
105        assert_ne!(Register::Numbered(0), Register::Numbered(1));
106    }
107}