1#[derive(Default, Clone, Debug)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20pub struct Slot {
21 pub text: String,
22 pub linewise: bool,
23 #[cfg_attr(feature = "serde", serde(default))]
33 pub blockwise: bool,
34 #[cfg_attr(feature = "serde", serde(default))]
38 pub block_width: usize,
39}
40
41impl Slot {
42 fn new(text: String, linewise: bool) -> Self {
43 Self {
44 text,
45 linewise,
46 blockwise: false,
47 block_width: 0,
48 }
49 }
50
51 fn new_block(text: String, width: usize) -> Self {
54 Self {
55 text,
56 linewise: false,
57 blockwise: true,
58 block_width: width,
59 }
60 }
61}
62
63#[derive(Default, Debug, Clone)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct Registers {
66 pub unnamed: Slot,
68 pub yank_zero: Slot,
70 pub delete_ring: [Slot; 9],
72 pub small_delete: Slot,
75 pub named: [Slot; 26],
77 pub clip: Slot,
83 pub filename: Option<String>,
86 #[cfg_attr(feature = "serde", serde(skip))]
90 filename_slot: Option<Slot>,
91}
92
93impl Registers {
94 pub fn record_yank(&mut self, text: String, linewise: bool, target: Option<char>) {
99 if target == Some('_') {
101 return;
102 }
103 self.store_yank(Slot::new(text, linewise), target);
104 }
105
106 pub fn record_yank_block(&mut self, text: String, width: usize, target: Option<char>) {
111 if target == Some('_') {
112 return;
113 }
114 self.store_yank(Slot::new_block(text, width), target);
115 }
116
117 fn store_yank(&mut self, slot: Slot, target: Option<char>) {
120 self.unnamed = slot.clone();
121 if target.is_none() {
124 self.yank_zero = slot.clone();
125 }
126 if let Some(c) = target {
127 self.write_named(c, slot);
128 if let Some(named) = self.read(c) {
132 self.unnamed = named.clone();
133 }
134 }
135 }
136
137 pub fn record_delete(&mut self, text: String, linewise: bool, target: Option<char>) {
149 if text.is_empty() {
150 return;
151 }
152 if target == Some('_') {
154 return;
155 }
156 self.store_delete(Slot::new(text, linewise), target);
157 }
158
159 pub fn record_delete_block(&mut self, text: String, width: usize, target: Option<char>) {
163 if text.is_empty() {
164 return;
165 }
166 if target == Some('_') {
167 return;
168 }
169 self.store_delete(Slot::new_block(text, width), target);
170 }
171
172 fn store_delete(&mut self, slot: Slot, target: Option<char>) {
174 self.unnamed = slot.clone();
175 if let Some(c) = target {
176 self.write_named(c, slot);
178 if let Some(named) = self.read(c) {
180 self.unnamed = named.clone();
181 }
182 return;
183 }
184 if slot.linewise || slot.text.contains('\n') {
185 for i in (1..9).rev() {
187 self.delete_ring[i] = self.delete_ring[i - 1].clone();
188 }
189 self.delete_ring[0] = slot;
190 } else {
191 self.small_delete = slot;
193 }
194 }
195
196 pub fn read(&self, reg: char) -> Option<&Slot> {
202 match reg {
203 '"' => Some(&self.unnamed),
204 '0' => Some(&self.yank_zero),
205 '1'..='9' => Some(&self.delete_ring[(reg as u8 - b'1') as usize]),
206 '-' => Some(&self.small_delete),
207 'a'..='z' => Some(&self.named[(reg as u8 - b'a') as usize]),
208 'A'..='Z' => Some(&self.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize]),
209 '+' | '*' => Some(&self.clip),
210 '%' => self.filename_slot.as_ref(),
212 _ => None,
213 }
214 }
215
216 pub fn set_filename(&mut self, name: Option<String>) {
219 self.filename = name.clone();
220 self.filename_slot = name.map(|n| Slot::new(n, false));
221 }
222
223 pub fn set_clipboard(&mut self, text: String, linewise: bool) {
226 self.clip = Slot::new(text, linewise);
227 }
228
229 fn write_named(&mut self, c: char, slot: Slot) {
230 if c.is_ascii_lowercase() {
231 self.named[(c as u8 - b'a') as usize] = slot;
232 } else if c.is_ascii_uppercase() {
233 let idx = (c.to_ascii_lowercase() as u8 - b'a') as usize;
234 let cur = &mut self.named[idx];
235 cur.text.push_str(&slot.text);
236 cur.linewise = slot.linewise || cur.linewise;
237 cur.blockwise = slot.blockwise || cur.blockwise;
238 cur.block_width = cur.block_width.max(slot.block_width);
239 } else if c == '+' || c == '*' {
240 self.clip = slot;
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn yank_writes_unnamed_and_zero() {
251 let mut r = Registers::default();
252 r.record_yank("foo".into(), false, None);
253 assert_eq!(r.read('"').unwrap().text, "foo");
254 assert_eq!(r.read('0').unwrap().text, "foo");
255 }
256
257 #[test]
258 fn delete_rotates_ring_and_skips_zero() {
259 let mut r = Registers::default();
260 r.record_yank("kept".into(), false, None);
261 r.record_delete("d1\n".into(), true, None);
263 r.record_delete("d2\n".into(), true, None);
264 assert_eq!(r.read('1').unwrap().text, "d2\n");
266 assert_eq!(r.read('2').unwrap().text, "d1\n");
267 assert_eq!(r.read('0').unwrap().text, "kept");
269 assert_eq!(r.read('"').unwrap().text, "d2\n");
271 }
272
273 #[test]
274 fn small_delete_goes_to_dash_not_ring() {
275 let mut r = Registers::default();
276 r.record_delete("line\n".into(), true, None);
278 r.record_delete("x".into(), false, None);
280 r.record_delete("y".into(), false, None);
281 assert_eq!(r.read('-').unwrap().text, "y");
282 assert_eq!(r.read('1').unwrap().text, "line\n");
284 assert!(r.read('2').unwrap().text.is_empty());
285 assert_eq!(r.read('"').unwrap().text, "y");
287 }
288
289 #[test]
290 fn multiline_charwise_delete_fills_ring_not_dash() {
291 let mut r = Registers::default();
292 r.record_delete("a\nb".into(), false, None);
294 assert_eq!(r.read('1').unwrap().text, "a\nb");
295 assert!(r.read('-').unwrap().text.is_empty());
296 }
297
298 #[test]
299 fn named_delete_leaves_ring_and_dash_untouched() {
300 let mut r = Registers::default();
301 r.record_delete("ring\n".into(), true, None);
302 r.record_delete("dash".into(), false, None);
303 r.record_delete("named\n".into(), true, Some('a'));
305 assert_eq!(r.read('a').unwrap().text, "named\n");
306 assert_eq!(r.read('1').unwrap().text, "ring\n");
307 assert_eq!(r.read('-').unwrap().text, "dash");
308 assert_eq!(r.read('"').unwrap().text, "named\n");
310 }
311
312 #[test]
313 fn yank_to_named_preserves_zero() {
314 let mut r = Registers::default();
315 r.record_yank("original".into(), false, None);
316 assert_eq!(r.read('0').unwrap().text, "original");
317 r.record_yank("into a".into(), false, Some('a'));
319 assert_eq!(r.read('0').unwrap().text, "original");
320 assert_eq!(r.read('a').unwrap().text, "into a");
321 }
322
323 #[test]
324 fn named_lowercase_overwrites_uppercase_appends() {
325 let mut r = Registers::default();
326 r.record_yank("hello ".into(), false, Some('a'));
327 r.record_yank("world".into(), false, Some('A'));
328 assert_eq!(r.read('a').unwrap().text, "hello world");
329 assert_eq!(r.read('A').unwrap().text, "hello world");
331 }
332
333 #[test]
334 fn empty_delete_is_dropped() {
335 let mut r = Registers::default();
336 r.record_delete("first\n".into(), true, None);
337 r.record_delete(String::new(), true, None);
338 assert_eq!(r.read('1').unwrap().text, "first\n");
339 assert!(r.read('2').unwrap().text.is_empty());
340 }
341
342 #[test]
343 fn unknown_selector_returns_none() {
344 let r = Registers::default();
345 assert!(r.read('?').is_none());
346 assert!(r.read('!').is_none());
347 }
348
349 #[test]
350 fn plus_and_star_alias_clipboard_slot() {
351 let mut r = Registers::default();
352 r.set_clipboard("payload".into(), false);
353 assert_eq!(r.read('+').unwrap().text, "payload");
354 assert_eq!(r.read('*').unwrap().text, "payload");
355 }
356
357 #[test]
358 fn yank_to_plus_writes_clipboard_slot() {
359 let mut r = Registers::default();
360 r.record_yank("hi".into(), false, Some('+'));
361 assert_eq!(r.read('+').unwrap().text, "hi");
362 assert_eq!(r.read('"').unwrap().text, "hi");
364 }
365
366 #[test]
367 fn percent_register_returns_none_when_no_filename() {
368 let r = Registers::default();
369 assert!(r.read('%').is_none());
370 }
371
372 #[test]
373 fn percent_register_returns_filename_after_set() {
374 let mut r = Registers::default();
375 r.set_filename(Some("src/main.rs".into()));
376 let slot = r
377 .read('%')
378 .expect("'%' should return Some after set_filename");
379 assert_eq!(slot.text, "src/main.rs");
380 assert!(!slot.linewise, "'%' slot should be charwise");
381 }
382
383 #[test]
384 fn block_yank_flags_slot_blockwise_with_width() {
385 let mut r = Registers::default();
386 r.record_yank_block("ab\nef".into(), 2, None);
387 let s = r.read('"').unwrap();
388 assert_eq!(s.text, "ab\nef");
389 assert!(s.blockwise);
390 assert!(!s.linewise);
391 assert_eq!(s.block_width, 2);
392 let z = r.read('0').unwrap();
394 assert!(z.blockwise);
395 assert_eq!(z.block_width, 2);
396 }
397
398 #[test]
399 fn block_yank_to_named_carries_kind_and_width() {
400 let mut r = Registers::default();
401 r.record_yank_block("cd\nkl".into(), 3, Some('a'));
402 let a = r.read('a').unwrap();
403 assert!(a.blockwise);
404 assert_eq!(a.block_width, 3);
405 assert!(r.read('"').unwrap().blockwise);
407 assert_eq!(r.read('"').unwrap().block_width, 3);
408 }
409
410 #[test]
411 fn charwise_yank_leaves_slot_non_block() {
412 let mut r = Registers::default();
413 r.record_yank("plain".into(), false, None);
414 let s = r.read('"').unwrap();
415 assert!(!s.blockwise);
416 assert_eq!(s.block_width, 0);
417 }
418
419 #[test]
420 fn block_delete_flags_slot_blockwise() {
421 let mut r = Registers::default();
422 r.record_delete_block("a\nb".into(), 1, None);
424 let s = r.read('1').unwrap();
425 assert!(s.blockwise);
426 assert_eq!(s.block_width, 1);
427 assert!(r.read('"').unwrap().blockwise);
428 }
429
430 #[test]
431 fn percent_register_clears_when_set_to_none() {
432 let mut r = Registers::default();
433 r.set_filename(Some("foo.txt".into()));
434 r.set_filename(None);
435 assert!(r.read('%').is_none());
436 }
437}