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 }
69 }
70
71 pub fn record_delete(&mut self, text: String, linewise: bool, target: Option<char>) {
77 if text.is_empty() {
78 return;
79 }
80 if target == Some('_') {
82 return;
83 }
84 let slot = Slot::new(text, linewise);
85 self.unnamed = slot.clone();
86 for i in (1..9).rev() {
87 self.delete_ring[i] = self.delete_ring[i - 1].clone();
88 }
89 self.delete_ring[0] = slot.clone();
90 if let Some(c) = target {
91 self.write_named(c, slot);
92 }
93 }
94
95 pub fn read(&self, reg: char) -> Option<&Slot> {
101 match reg {
102 '"' => Some(&self.unnamed),
103 '0' => Some(&self.yank_zero),
104 '1'..='9' => Some(&self.delete_ring[(reg as u8 - b'1') as usize]),
105 'a'..='z' => Some(&self.named[(reg as u8 - b'a') as usize]),
106 'A'..='Z' => Some(&self.named[(reg.to_ascii_lowercase() as u8 - b'a') as usize]),
107 '+' | '*' => Some(&self.clip),
108 '%' => self.filename_slot.as_ref(),
110 _ => None,
111 }
112 }
113
114 pub fn set_filename(&mut self, name: Option<String>) {
117 self.filename = name.clone();
118 self.filename_slot = name.map(|n| Slot::new(n, false));
119 }
120
121 pub fn set_clipboard(&mut self, text: String, linewise: bool) {
124 self.clip = Slot::new(text, linewise);
125 }
126
127 fn write_named(&mut self, c: char, slot: Slot) {
128 if c.is_ascii_lowercase() {
129 self.named[(c as u8 - b'a') as usize] = slot;
130 } else if c.is_ascii_uppercase() {
131 let idx = (c.to_ascii_lowercase() as u8 - b'a') as usize;
132 let cur = &mut self.named[idx];
133 cur.text.push_str(&slot.text);
134 cur.linewise = slot.linewise || cur.linewise;
135 } else if c == '+' || c == '*' {
136 self.clip = slot;
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn yank_writes_unnamed_and_zero() {
147 let mut r = Registers::default();
148 r.record_yank("foo".into(), false, None);
149 assert_eq!(r.read('"').unwrap().text, "foo");
150 assert_eq!(r.read('0').unwrap().text, "foo");
151 }
152
153 #[test]
154 fn delete_rotates_ring_and_skips_zero() {
155 let mut r = Registers::default();
156 r.record_yank("kept".into(), false, None);
157 r.record_delete("d1".into(), false, None);
158 r.record_delete("d2".into(), false, None);
159 assert_eq!(r.read('1').unwrap().text, "d2");
161 assert_eq!(r.read('2').unwrap().text, "d1");
162 assert_eq!(r.read('0').unwrap().text, "kept");
164 assert_eq!(r.read('"').unwrap().text, "d2");
166 }
167
168 #[test]
169 fn named_lowercase_overwrites_uppercase_appends() {
170 let mut r = Registers::default();
171 r.record_yank("hello ".into(), false, Some('a'));
172 r.record_yank("world".into(), false, Some('A'));
173 assert_eq!(r.read('a').unwrap().text, "hello world");
174 assert_eq!(r.read('A').unwrap().text, "hello world");
176 }
177
178 #[test]
179 fn empty_delete_is_dropped() {
180 let mut r = Registers::default();
181 r.record_delete("first".into(), false, None);
182 r.record_delete(String::new(), false, None);
183 assert_eq!(r.read('1').unwrap().text, "first");
184 assert!(r.read('2').unwrap().text.is_empty());
185 }
186
187 #[test]
188 fn unknown_selector_returns_none() {
189 let r = Registers::default();
190 assert!(r.read('?').is_none());
191 assert!(r.read('!').is_none());
192 }
193
194 #[test]
195 fn plus_and_star_alias_clipboard_slot() {
196 let mut r = Registers::default();
197 r.set_clipboard("payload".into(), false);
198 assert_eq!(r.read('+').unwrap().text, "payload");
199 assert_eq!(r.read('*').unwrap().text, "payload");
200 }
201
202 #[test]
203 fn yank_to_plus_writes_clipboard_slot() {
204 let mut r = Registers::default();
205 r.record_yank("hi".into(), false, Some('+'));
206 assert_eq!(r.read('+').unwrap().text, "hi");
207 assert_eq!(r.read('"').unwrap().text, "hi");
209 }
210
211 #[test]
212 fn percent_register_returns_none_when_no_filename() {
213 let r = Registers::default();
214 assert!(r.read('%').is_none());
215 }
216
217 #[test]
218 fn percent_register_returns_filename_after_set() {
219 let mut r = Registers::default();
220 r.set_filename(Some("src/main.rs".into()));
221 let slot = r
222 .read('%')
223 .expect("'%' should return Some after set_filename");
224 assert_eq!(slot.text, "src/main.rs");
225 assert!(!slot.linewise, "'%' slot should be charwise");
226 }
227
228 #[test]
229 fn percent_register_clears_when_set_to_none() {
230 let mut r = Registers::default();
231 r.set_filename(Some("foo.txt".into()));
232 r.set_filename(None);
233 assert!(r.read('%').is_none());
234 }
235}