Skip to main content

rpi_tui/
kill_ring.rs

1//! Kill ring for Emacs-style kill and yank operations.
2//!
3//! The kill ring stores recently deleted text, allowing users to "yank" (paste)
4//! previously killed text. This supports the Emacs convention where consecutive
5//! kill operations accumulate in the same entry, and yank-pop cycles through
6//! previous entries.
7
8use std::collections::VecDeque;
9
10/// Maximum number of entries in the kill ring.
11const MAX_KILL_RING_SIZE: usize = 64;
12
13/// Options for pushing to the kill ring.
14#[derive(Debug, Clone)]
15pub struct PushOptions {
16    /// Whether to prepend to the current entry (for backward kills).
17    pub prepend: bool,
18    /// Whether to accumulate with the previous entry (for consecutive kills).
19    pub accumulate: bool,
20}
21
22impl Default for PushOptions {
23    fn default() -> Self {
24        Self {
25            prepend: false,
26            accumulate: false,
27        }
28    }
29}
30
31/// Kill ring for storing killed (deleted) text.
32#[derive(Debug, Clone)]
33pub struct KillRing {
34    /// The ring of killed text entries.
35    entries: VecDeque<String>,
36    /// Maximum size of the ring.
37    max_size: usize,
38}
39
40impl KillRing {
41    /// Create a new kill ring.
42    pub fn new() -> Self {
43        Self {
44            entries: VecDeque::with_capacity(MAX_KILL_RING_SIZE),
45            max_size: MAX_KILL_RING_SIZE,
46        }
47    }
48
49    /// Create a kill ring with a custom maximum size.
50    pub fn with_max_size(max_size: usize) -> Self {
51        Self {
52            entries: VecDeque::with_capacity(max_size),
53            max_size,
54        }
55    }
56
57    /// Push text to the kill ring.
58    pub fn push(&mut self, text: &str, options: PushOptions) {
59        if text.is_empty() {
60            return;
61        }
62
63        if options.accumulate && !self.entries.is_empty() {
64            // Accumulate with the most recent entry
65            if options.prepend {
66                let front = self.entries.front_mut().unwrap();
67                *front = format!("{}{}", text, front);
68            } else {
69                let front = self.entries.front_mut().unwrap();
70                *front = format!("{}{}", front, text);
71            }
72        } else {
73            // Add as a new entry
74            // Check if the text is already at the front (don't duplicate)
75            if self.entries.front() != Some(&text.to_string()) {
76                self.entries.push_front(text.to_string());
77
78                // Trim if over capacity
79                while self.entries.len() > self.max_size {
80                    self.entries.pop_back();
81                }
82            }
83        }
84    }
85
86    /// Peek at the most recent entry without removing it.
87    pub fn peek(&self) -> Option<&str> {
88        self.entries.front().map(|s| s.as_str())
89    }
90
91    /// Get the number of entries in the kill ring.
92    pub fn len(&self) -> usize {
93        self.entries.len()
94    }
95
96    /// Check if the kill ring is empty.
97    pub fn is_empty(&self) -> bool {
98        self.entries.is_empty()
99    }
100
101    /// Rotate the kill ring forward (move front to back).
102    /// Used for yank-pop to cycle through entries.
103    pub fn rotate(&mut self) {
104        if self.entries.len() > 1 {
105            if let Some(front) = self.entries.pop_front() {
106                self.entries.push_back(front);
107            }
108        }
109    }
110
111    /// Rotate the kill ring backward (move back to front).
112    pub fn rotate_back(&mut self) {
113        if self.entries.len() > 1 {
114            if let Some(back) = self.entries.pop_back() {
115                self.entries.push_front(back);
116            }
117        }
118    }
119
120    /// Clear all entries from the kill ring.
121    pub fn clear(&mut self) {
122        self.entries.clear();
123    }
124
125    /// Get all entries (for debugging/testing).
126    pub fn entries(&self) -> impl Iterator<Item = &String> {
127        self.entries.iter()
128    }
129
130    /// Get entry at index (0 is most recent).
131    pub fn get(&self, index: usize) -> Option<&str> {
132        self.entries.get(index).map(|s| s.as_str())
133    }
134}
135
136impl Default for KillRing {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_push_and_peek() {
148        let mut ring = KillRing::new();
149        ring.push("hello", PushOptions::default());
150        assert_eq!(ring.peek(), Some("hello"));
151        assert_eq!(ring.len(), 1);
152    }
153
154    #[test]
155    fn test_accumulate() {
156        let mut ring = KillRing::new();
157        ring.push("hello", PushOptions::default());
158        ring.push(
159            " world",
160            PushOptions {
161                prepend: false,
162                accumulate: true,
163            },
164        );
165        assert_eq!(ring.peek(), Some("hello world"));
166        assert_eq!(ring.len(), 1);
167    }
168
169    #[test]
170    fn test_accumulate_prepend() {
171        let mut ring = KillRing::new();
172        ring.push("world", PushOptions::default());
173        ring.push(
174            "hello ",
175            PushOptions {
176                prepend: true,
177                accumulate: true,
178            },
179        );
180        assert_eq!(ring.peek(), Some("hello world"));
181    }
182
183    #[test]
184    fn test_rotate() {
185        let mut ring = KillRing::new();
186        ring.push("first", PushOptions::default());
187        ring.push("second", PushOptions::default());
188        ring.push("third", PushOptions::default());
189
190        assert_eq!(ring.peek(), Some("third"));
191        ring.rotate();
192        assert_eq!(ring.peek(), Some("second"));
193        ring.rotate();
194        assert_eq!(ring.peek(), Some("first"));
195    }
196
197    #[test]
198    fn test_no_duplicate() {
199        let mut ring = KillRing::new();
200        ring.push("hello", PushOptions::default());
201        ring.push("hello", PushOptions::default());
202        assert_eq!(ring.len(), 1);
203    }
204
205    #[test]
206    fn test_max_size() {
207        let mut ring = KillRing::with_max_size(3);
208        ring.push("1", PushOptions::default());
209        ring.push("2", PushOptions::default());
210        ring.push("3", PushOptions::default());
211        ring.push("4", PushOptions::default());
212
213        assert_eq!(ring.len(), 3);
214        assert_eq!(ring.peek(), Some("4"));
215        assert_eq!(ring.get(2), Some("2")); // "1" should be evicted
216    }
217
218    #[test]
219    fn test_empty_text() {
220        let mut ring = KillRing::new();
221        ring.push("", PushOptions::default());
222        assert!(ring.is_empty());
223    }
224}