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}