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 pub filename: Option<String>,
46 #[cfg_attr(feature = "serde", serde(skip))]
50 filename_slot: Option<Slot>,
51}
52
53impl Registers {
54 pub fn record_yank(&mut self, text: String, linewise: bool, target: Option<char>) {
59 if target == Some('_') {
61 return;
62 }
63 let slot = Slot::new(text, linewise);
64 self.unnamed = slot.clone();
65 self.yank_zero = slot.clone();
66 if let Some(c) = target {
67 self.write_named(c, slot);
68 if let Some(named) = self.read(c) {
72 self.unnamed = named.clone();
73 }
74 }
75 }
76
77 pub fn record_delete(&mut self, text: String, linewise: bool, target: Option<char>) {
83 if text.is_empty() {
84 return;
85 }
86 if target == Some('_') {
88 return;
89 }
90 let slot = Slot::new(text, linewise);
91 self.unnamed = slot.clone();
92 for i in (1..9).rev() {
93 self.delete_ring[i] = self.delete_ring[i - 1].clone();
94 }
95 self.delete_ring[0] = slot.clone();
96 if let Some(c) = target {
97 self.write_named(c, slot);
98 if let Some(named) = self.read(c) {
100 self.unnamed = named.clone();
101 }
102 }
103 }
104
105 pub fn read(&self, reg: char) -> Option<&Slot> {
111 match reg {
112 '"' => Some(&self.unnamed),
113 '0' => Some(&self.yank_zero),
114 '1'..='9' => Some(&self.delete_ring[(reg as u8 - b'1') as usize]),
115 'a'..='z' => Some(&self.named[(reg as u8 - b'a') as usize]),
116 'A'..='Z' => Some(&self.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize]),
117 '+' | '*' => Some(&self.clip),
118 '%' => self.filename_slot.as_ref(),
120 _ => None,
121 }
122 }
123
124 pub fn set_filename(&mut self, name: Option<String>) {
127 self.filename = name.clone();
128 self.filename_slot = name.map(|n| Slot::new(n, false));
129 }
130
131 pub fn set_clipboard(&mut self, text: String, linewise: bool) {
134 self.clip = Slot::new(text, linewise);
135 }
136
137 fn write_named(&mut self, c: char, slot: Slot) {
138 if c.is_ascii_lowercase() {
139 self.named[(c as u8 - b'a') as usize] = slot;
140 } else if c.is_ascii_uppercase() {
141 let idx = (c.to_ascii_lowercase() as u8 - b'a') as usize;
142 let cur = &mut self.named[idx];
143 cur.text.push_str(&slot.text);
144 cur.linewise = slot.linewise || cur.linewise;
145 } else if c == '+' || c == '*' {
146 self.clip = slot;
147 }
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn yank_writes_unnamed_and_zero() {
157 let mut r = Registers::default();
158 r.record_yank("foo".into(), false, None);
159 assert_eq!(r.read('"').unwrap().text, "foo");
160 assert_eq!(r.read('0').unwrap().text, "foo");
161 }
162
163 #[test]
164 fn delete_rotates_ring_and_skips_zero() {
165 let mut r = Registers::default();
166 r.record_yank("kept".into(), false, None);
167 r.record_delete("d1".into(), false, None);
168 r.record_delete("d2".into(), false, None);
169 assert_eq!(r.read('1').unwrap().text, "d2");
171 assert_eq!(r.read('2').unwrap().text, "d1");
172 assert_eq!(r.read('0').unwrap().text, "kept");
174 assert_eq!(r.read('"').unwrap().text, "d2");
176 }
177
178 #[test]
179 fn named_lowercase_overwrites_uppercase_appends() {
180 let mut r = Registers::default();
181 r.record_yank("hello ".into(), false, Some('a'));
182 r.record_yank("world".into(), false, Some('A'));
183 assert_eq!(r.read('a').unwrap().text, "hello world");
184 assert_eq!(r.read('A').unwrap().text, "hello world");
186 }
187
188 #[test]
189 fn empty_delete_is_dropped() {
190 let mut r = Registers::default();
191 r.record_delete("first".into(), false, None);
192 r.record_delete(String::new(), false, None);
193 assert_eq!(r.read('1').unwrap().text, "first");
194 assert!(r.read('2').unwrap().text.is_empty());
195 }
196
197 #[test]
198 fn unknown_selector_returns_none() {
199 let r = Registers::default();
200 assert!(r.read('?').is_none());
201 assert!(r.read('!').is_none());
202 }
203
204 #[test]
205 fn plus_and_star_alias_clipboard_slot() {
206 let mut r = Registers::default();
207 r.set_clipboard("payload".into(), false);
208 assert_eq!(r.read('+').unwrap().text, "payload");
209 assert_eq!(r.read('*').unwrap().text, "payload");
210 }
211
212 #[test]
213 fn yank_to_plus_writes_clipboard_slot() {
214 let mut r = Registers::default();
215 r.record_yank("hi".into(), false, Some('+'));
216 assert_eq!(r.read('+').unwrap().text, "hi");
217 assert_eq!(r.read('"').unwrap().text, "hi");
219 }
220
221 #[test]
222 fn percent_register_returns_none_when_no_filename() {
223 let r = Registers::default();
224 assert!(r.read('%').is_none());
225 }
226
227 #[test]
228 fn percent_register_returns_filename_after_set() {
229 let mut r = Registers::default();
230 r.set_filename(Some("src/main.rs".into()));
231 let slot = r
232 .read('%')
233 .expect("'%' should return Some after set_filename");
234 assert_eq!(slot.text, "src/main.rs");
235 assert!(!slot.linewise, "'%' slot should be charwise");
236 }
237
238 #[test]
239 fn percent_register_clears_when_set_to_none() {
240 let mut r = Registers::default();
241 r.set_filename(Some("foo.txt".into()));
242 r.set_filename(None);
243 assert!(r.read('%').is_none());
244 }
245}