1#[derive(Default, Clone, Debug)]
19
20pub struct Slot {
21 pub text: String,
22 pub linewise: bool,
23 pub blockwise: bool,
30 pub block_width: usize,
34}
35
36impl Slot {
37 fn new(text: String, linewise: bool) -> Self {
38 Self {
39 text,
40 linewise,
41 blockwise: false,
42 block_width: 0,
43 }
44 }
45
46 fn new_block(text: String, width: usize) -> Self {
49 Self {
50 text,
51 linewise: false,
52 blockwise: true,
53 block_width: width,
54 }
55 }
56}
57
58#[derive(Default, Debug, Clone)]
59
60pub struct Registers {
61 pub unnamed: Slot,
63 pub yank_zero: Slot,
65 pub delete_ring: [Slot; 9],
67 pub small_delete: Slot,
70 pub named: [Slot; 26],
72 pub clip: Slot,
78 pub filename: Option<String>,
81 filename_slot: Option<Slot>,
84}
85
86impl Registers {
87 pub fn record_yank(&mut self, text: String, linewise: bool, target: Option<char>) {
92 if target == Some('_') {
94 return;
95 }
96 self.store_yank(Slot::new(text, linewise), target);
97 }
98
99 pub fn record_yank_block(&mut self, text: String, width: usize, target: Option<char>) {
104 if target == Some('_') {
105 return;
106 }
107 self.store_yank(Slot::new_block(text, width), target);
108 }
109
110 fn store_yank(&mut self, slot: Slot, target: Option<char>) {
113 self.unnamed = slot.clone();
114 if target.is_none() {
117 self.yank_zero = slot.clone();
118 }
119 if let Some(c) = target {
120 self.write_named(c, slot);
121 if let Some(named) = self.read(c) {
125 self.unnamed = named.clone();
126 }
127 }
128 }
129
130 pub fn record_delete(&mut self, text: String, linewise: bool, target: Option<char>) {
142 if text.is_empty() {
143 return;
144 }
145 if target == Some('_') {
147 return;
148 }
149 self.store_delete(Slot::new(text, linewise), target);
150 }
151
152 pub fn record_delete_block(&mut self, text: String, width: usize, target: Option<char>) {
156 if text.is_empty() {
157 return;
158 }
159 if target == Some('_') {
160 return;
161 }
162 self.store_delete(Slot::new_block(text, width), target);
163 }
164
165 fn store_delete(&mut self, slot: Slot, target: Option<char>) {
167 self.unnamed = slot.clone();
168 if let Some(c) = target {
169 self.write_named(c, slot);
171 if let Some(named) = self.read(c) {
173 self.unnamed = named.clone();
174 }
175 return;
176 }
177 if slot.linewise || slot.text.contains('\n') {
178 for i in (1..9).rev() {
180 self.delete_ring[i] = self.delete_ring[i - 1].clone();
181 }
182 self.delete_ring[0] = slot;
183 } else {
184 self.small_delete = slot;
186 }
187 }
188
189 pub fn read(&self, reg: char) -> Option<&Slot> {
195 match reg {
196 '"' => Some(&self.unnamed),
197 '0' => Some(&self.yank_zero),
198 '1'..='9' => Some(&self.delete_ring[(reg as u8 - b'1') as usize]),
199 '-' => Some(&self.small_delete),
200 'a'..='z' => Some(&self.named[(reg as u8 - b'a') as usize]),
201 'A'..='Z' => Some(&self.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize]),
202 '+' | '*' => Some(&self.clip),
203 '%' => self.filename_slot.as_ref(),
205 _ => None,
206 }
207 }
208
209 pub fn set_filename(&mut self, name: Option<String>) {
212 self.filename = name.clone();
213 self.filename_slot = name.map(|n| Slot::new(n, false));
214 }
215
216 pub fn set_clipboard(&mut self, text: String, linewise: bool) {
219 self.clip = Slot::new(text, linewise);
220 }
221
222 fn write_named(&mut self, c: char, slot: Slot) {
223 if c.is_ascii_lowercase() {
224 self.named[(c as u8 - b'a') as usize] = slot;
225 } else if c.is_ascii_uppercase() {
226 let idx = (c.to_ascii_lowercase() as u8 - b'a') as usize;
227 let cur = &mut self.named[idx];
228 cur.text.push_str(&slot.text);
229 cur.linewise = slot.linewise || cur.linewise;
230 cur.blockwise = slot.blockwise || cur.blockwise;
231 cur.block_width = cur.block_width.max(slot.block_width);
232 } else if c == '+' || c == '*' {
233 self.clip = slot;
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn yank_writes_unnamed_and_zero() {
244 let mut r = Registers::default();
245 r.record_yank("foo".into(), false, None);
246 assert_eq!(r.read('"').unwrap().text, "foo");
247 assert_eq!(r.read('0').unwrap().text, "foo");
248 }
249
250 #[test]
251 fn delete_rotates_ring_and_skips_zero() {
252 let mut r = Registers::default();
253 r.record_yank("kept".into(), false, None);
254 r.record_delete("d1\n".into(), true, None);
256 r.record_delete("d2\n".into(), true, None);
257 assert_eq!(r.read('1').unwrap().text, "d2\n");
259 assert_eq!(r.read('2').unwrap().text, "d1\n");
260 assert_eq!(r.read('0').unwrap().text, "kept");
262 assert_eq!(r.read('"').unwrap().text, "d2\n");
264 }
265
266 #[test]
267 fn small_delete_goes_to_dash_not_ring() {
268 let mut r = Registers::default();
269 r.record_delete("line\n".into(), true, None);
271 r.record_delete("x".into(), false, None);
273 r.record_delete("y".into(), false, None);
274 assert_eq!(r.read('-').unwrap().text, "y");
275 assert_eq!(r.read('1').unwrap().text, "line\n");
277 assert!(r.read('2').unwrap().text.is_empty());
278 assert_eq!(r.read('"').unwrap().text, "y");
280 }
281
282 #[test]
283 fn multiline_charwise_delete_fills_ring_not_dash() {
284 let mut r = Registers::default();
285 r.record_delete("a\nb".into(), false, None);
287 assert_eq!(r.read('1').unwrap().text, "a\nb");
288 assert!(r.read('-').unwrap().text.is_empty());
289 }
290
291 #[test]
292 fn named_delete_leaves_ring_and_dash_untouched() {
293 let mut r = Registers::default();
294 r.record_delete("ring\n".into(), true, None);
295 r.record_delete("dash".into(), false, None);
296 r.record_delete("named\n".into(), true, Some('a'));
298 assert_eq!(r.read('a').unwrap().text, "named\n");
299 assert_eq!(r.read('1').unwrap().text, "ring\n");
300 assert_eq!(r.read('-').unwrap().text, "dash");
301 assert_eq!(r.read('"').unwrap().text, "named\n");
303 }
304
305 #[test]
306 fn yank_to_named_preserves_zero() {
307 let mut r = Registers::default();
308 r.record_yank("original".into(), false, None);
309 assert_eq!(r.read('0').unwrap().text, "original");
310 r.record_yank("into a".into(), false, Some('a'));
312 assert_eq!(r.read('0').unwrap().text, "original");
313 assert_eq!(r.read('a').unwrap().text, "into a");
314 }
315
316 #[test]
317 fn named_lowercase_overwrites_uppercase_appends() {
318 let mut r = Registers::default();
319 r.record_yank("hello ".into(), false, Some('a'));
320 r.record_yank("world".into(), false, Some('A'));
321 assert_eq!(r.read('a').unwrap().text, "hello world");
322 assert_eq!(r.read('A').unwrap().text, "hello world");
324 }
325
326 #[test]
327 fn empty_delete_is_dropped() {
328 let mut r = Registers::default();
329 r.record_delete("first\n".into(), true, None);
330 r.record_delete(String::new(), true, None);
331 assert_eq!(r.read('1').unwrap().text, "first\n");
332 assert!(r.read('2').unwrap().text.is_empty());
333 }
334
335 #[test]
336 fn unknown_selector_returns_none() {
337 let r = Registers::default();
338 assert!(r.read('?').is_none());
339 assert!(r.read('!').is_none());
340 }
341
342 #[test]
343 fn plus_and_star_alias_clipboard_slot() {
344 let mut r = Registers::default();
345 r.set_clipboard("payload".into(), false);
346 assert_eq!(r.read('+').unwrap().text, "payload");
347 assert_eq!(r.read('*').unwrap().text, "payload");
348 }
349
350 #[test]
351 fn yank_to_plus_writes_clipboard_slot() {
352 let mut r = Registers::default();
353 r.record_yank("hi".into(), false, Some('+'));
354 assert_eq!(r.read('+').unwrap().text, "hi");
355 assert_eq!(r.read('"').unwrap().text, "hi");
357 }
358
359 #[test]
360 fn percent_register_returns_none_when_no_filename() {
361 let r = Registers::default();
362 assert!(r.read('%').is_none());
363 }
364
365 #[test]
366 fn percent_register_returns_filename_after_set() {
367 let mut r = Registers::default();
368 r.set_filename(Some("src/main.rs".into()));
369 let slot = r
370 .read('%')
371 .expect("'%' should return Some after set_filename");
372 assert_eq!(slot.text, "src/main.rs");
373 assert!(!slot.linewise, "'%' slot should be charwise");
374 }
375
376 #[test]
377 fn block_yank_flags_slot_blockwise_with_width() {
378 let mut r = Registers::default();
379 r.record_yank_block("ab\nef".into(), 2, None);
380 let s = r.read('"').unwrap();
381 assert_eq!(s.text, "ab\nef");
382 assert!(s.blockwise);
383 assert!(!s.linewise);
384 assert_eq!(s.block_width, 2);
385 let z = r.read('0').unwrap();
387 assert!(z.blockwise);
388 assert_eq!(z.block_width, 2);
389 }
390
391 #[test]
392 fn block_yank_to_named_carries_kind_and_width() {
393 let mut r = Registers::default();
394 r.record_yank_block("cd\nkl".into(), 3, Some('a'));
395 let a = r.read('a').unwrap();
396 assert!(a.blockwise);
397 assert_eq!(a.block_width, 3);
398 assert!(r.read('"').unwrap().blockwise);
400 assert_eq!(r.read('"').unwrap().block_width, 3);
401 }
402
403 #[test]
404 fn charwise_yank_leaves_slot_non_block() {
405 let mut r = Registers::default();
406 r.record_yank("plain".into(), false, None);
407 let s = r.read('"').unwrap();
408 assert!(!s.blockwise);
409 assert_eq!(s.block_width, 0);
410 }
411
412 #[test]
413 fn block_delete_flags_slot_blockwise() {
414 let mut r = Registers::default();
415 r.record_delete_block("a\nb".into(), 1, None);
417 let s = r.read('1').unwrap();
418 assert!(s.blockwise);
419 assert_eq!(s.block_width, 1);
420 assert!(r.read('"').unwrap().blockwise);
421 }
422
423 #[test]
424 fn percent_register_clears_when_set_to_none() {
425 let mut r = Registers::default();
426 r.set_filename(Some("foo.txt".into()));
427 r.set_filename(None);
428 assert!(r.read('%').is_none());
429 }
430}