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>) {
49 let slot = Slot::new(text, linewise);
50 self.unnamed = slot.clone();
51 self.yank_zero = slot.clone();
52 if let Some(c) = target {
53 self.write_named(c, slot);
54 }
55 }
56
57 pub fn record_delete(&mut self, text: String, linewise: bool, target: Option<char>) {
62 if text.is_empty() {
63 return;
64 }
65 let slot = Slot::new(text, linewise);
66 self.unnamed = slot.clone();
67 for i in (1..9).rev() {
68 self.delete_ring[i] = self.delete_ring[i - 1].clone();
69 }
70 self.delete_ring[0] = slot.clone();
71 if let Some(c) = target {
72 self.write_named(c, slot);
73 }
74 }
75
76 pub fn read(&self, reg: char) -> Option<&Slot> {
79 match reg {
80 '"' => Some(&self.unnamed),
81 '0' => Some(&self.yank_zero),
82 '1'..='9' => Some(&self.delete_ring[(reg as u8 - b'1') as usize]),
83 'a'..='z' => Some(&self.named[(reg as u8 - b'a') as usize]),
84 'A'..='Z' => Some(&self.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize]),
85 '+' | '*' => Some(&self.clip),
86 _ => None,
87 }
88 }
89
90 pub fn set_clipboard(&mut self, text: String, linewise: bool) {
93 self.clip = Slot::new(text, linewise);
94 }
95
96 fn write_named(&mut self, c: char, slot: Slot) {
97 if c.is_ascii_lowercase() {
98 self.named[(c as u8 - b'a') as usize] = slot;
99 } else if c.is_ascii_uppercase() {
100 let idx = (c.to_ascii_lowercase() as u8 - b'a') as usize;
101 let cur = &mut self.named[idx];
102 cur.text.push_str(&slot.text);
103 cur.linewise = slot.linewise || cur.linewise;
104 } else if c == '+' || c == '*' {
105 self.clip = slot;
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn yank_writes_unnamed_and_zero() {
116 let mut r = Registers::default();
117 r.record_yank("foo".into(), false, None);
118 assert_eq!(r.read('"').unwrap().text, "foo");
119 assert_eq!(r.read('0').unwrap().text, "foo");
120 }
121
122 #[test]
123 fn delete_rotates_ring_and_skips_zero() {
124 let mut r = Registers::default();
125 r.record_yank("kept".into(), false, None);
126 r.record_delete("d1".into(), false, None);
127 r.record_delete("d2".into(), false, None);
128 assert_eq!(r.read('1').unwrap().text, "d2");
130 assert_eq!(r.read('2').unwrap().text, "d1");
131 assert_eq!(r.read('0').unwrap().text, "kept");
133 assert_eq!(r.read('"').unwrap().text, "d2");
135 }
136
137 #[test]
138 fn named_lowercase_overwrites_uppercase_appends() {
139 let mut r = Registers::default();
140 r.record_yank("hello ".into(), false, Some('a'));
141 r.record_yank("world".into(), false, Some('A'));
142 assert_eq!(r.read('a').unwrap().text, "hello world");
143 assert_eq!(r.read('A').unwrap().text, "hello world");
145 }
146
147 #[test]
148 fn empty_delete_is_dropped() {
149 let mut r = Registers::default();
150 r.record_delete("first".into(), false, None);
151 r.record_delete(String::new(), false, None);
152 assert_eq!(r.read('1').unwrap().text, "first");
153 assert!(r.read('2').unwrap().text.is_empty());
154 }
155
156 #[test]
157 fn unknown_selector_returns_none() {
158 let r = Registers::default();
159 assert!(r.read('?').is_none());
160 assert!(r.read('!').is_none());
161 }
162
163 #[test]
164 fn plus_and_star_alias_clipboard_slot() {
165 let mut r = Registers::default();
166 r.set_clipboard("payload".into(), false);
167 assert_eq!(r.read('+').unwrap().text, "payload");
168 assert_eq!(r.read('*').unwrap().text, "payload");
169 }
170
171 #[test]
172 fn yank_to_plus_writes_clipboard_slot() {
173 let mut r = Registers::default();
174 r.record_yank("hi".into(), false, Some('+'));
175 assert_eq!(r.read('+').unwrap().text, "hi");
176 assert_eq!(r.read('"').unwrap().text, "hi");
178 }
179}