1#[derive(Default, Clone, Debug)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub struct Slot {
16 pub text: String,
17 pub linewise: bool,
18}
19
20impl Slot {
21 fn new(text: String, linewise: bool) -> Self {
22 Self { text, linewise }
23 }
24}
25
26#[derive(Default, Debug, Clone)]
27#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
28pub struct Registers {
29 pub unnamed: Slot,
31 pub yank_zero: Slot,
33 pub delete_ring: [Slot; 9],
35 pub named: [Slot; 26],
37 pub clip: Slot,
43}
44
45impl Registers {
46 pub fn record_yank(&mut self, text: String, linewise: bool, target: Option<char>) {
51 if target == Some('_') {
53 return;
54 }
55 let slot = Slot::new(text, linewise);
56 self.unnamed = slot.clone();
57 self.yank_zero = slot.clone();
58 if let Some(c) = target {
59 self.write_named(c, slot);
60 }
61 }
62
63 pub fn record_delete(&mut self, text: String, linewise: bool, target: Option<char>) {
69 if text.is_empty() {
70 return;
71 }
72 if target == Some('_') {
74 return;
75 }
76 let slot = Slot::new(text, linewise);
77 self.unnamed = slot.clone();
78 for i in (1..9).rev() {
79 self.delete_ring[i] = self.delete_ring[i - 1].clone();
80 }
81 self.delete_ring[0] = slot.clone();
82 if let Some(c) = target {
83 self.write_named(c, slot);
84 }
85 }
86
87 pub fn read(&self, reg: char) -> Option<&Slot> {
90 match reg {
91 '"' => Some(&self.unnamed),
92 '0' => Some(&self.yank_zero),
93 '1'..='9' => Some(&self.delete_ring[(reg as u8 - b'1') as usize]),
94 'a'..='z' => Some(&self.named[(reg as u8 - b'a') as usize]),
95 'A'..='Z' => Some(&self.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize]),
96 '+' | '*' => Some(&self.clip),
97 _ => None,
98 }
99 }
100
101 pub fn set_clipboard(&mut self, text: String, linewise: bool) {
104 self.clip = Slot::new(text, linewise);
105 }
106
107 fn write_named(&mut self, c: char, slot: Slot) {
108 if c.is_ascii_lowercase() {
109 self.named[(c as u8 - b'a') as usize] = slot;
110 } else if c.is_ascii_uppercase() {
111 let idx = (c.to_ascii_lowercase() as u8 - b'a') as usize;
112 let cur = &mut self.named[idx];
113 cur.text.push_str(&slot.text);
114 cur.linewise = slot.linewise || cur.linewise;
115 } else if c == '+' || c == '*' {
116 self.clip = slot;
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn yank_writes_unnamed_and_zero() {
127 let mut r = Registers::default();
128 r.record_yank("foo".into(), false, None);
129 assert_eq!(r.read('"').unwrap().text, "foo");
130 assert_eq!(r.read('0').unwrap().text, "foo");
131 }
132
133 #[test]
134 fn delete_rotates_ring_and_skips_zero() {
135 let mut r = Registers::default();
136 r.record_yank("kept".into(), false, None);
137 r.record_delete("d1".into(), false, None);
138 r.record_delete("d2".into(), false, None);
139 assert_eq!(r.read('1').unwrap().text, "d2");
141 assert_eq!(r.read('2').unwrap().text, "d1");
142 assert_eq!(r.read('0').unwrap().text, "kept");
144 assert_eq!(r.read('"').unwrap().text, "d2");
146 }
147
148 #[test]
149 fn named_lowercase_overwrites_uppercase_appends() {
150 let mut r = Registers::default();
151 r.record_yank("hello ".into(), false, Some('a'));
152 r.record_yank("world".into(), false, Some('A'));
153 assert_eq!(r.read('a').unwrap().text, "hello world");
154 assert_eq!(r.read('A').unwrap().text, "hello world");
156 }
157
158 #[test]
159 fn empty_delete_is_dropped() {
160 let mut r = Registers::default();
161 r.record_delete("first".into(), false, None);
162 r.record_delete(String::new(), false, None);
163 assert_eq!(r.read('1').unwrap().text, "first");
164 assert!(r.read('2').unwrap().text.is_empty());
165 }
166
167 #[test]
168 fn unknown_selector_returns_none() {
169 let r = Registers::default();
170 assert!(r.read('?').is_none());
171 assert!(r.read('!').is_none());
172 }
173
174 #[test]
175 fn plus_and_star_alias_clipboard_slot() {
176 let mut r = Registers::default();
177 r.set_clipboard("payload".into(), false);
178 assert_eq!(r.read('+').unwrap().text, "payload");
179 assert_eq!(r.read('*').unwrap().text, "payload");
180 }
181
182 #[test]
183 fn yank_to_plus_writes_clipboard_slot() {
184 let mut r = Registers::default();
185 r.record_yank("hi".into(), false, Some('+'));
186 assert_eq!(r.read('+').unwrap().text, "hi");
187 assert_eq!(r.read('"').unwrap().text, "hi");
189 }
190}