Skip to main content

rab/tui/
kill_ring.rs

1/// Ring buffer for Emacs-style kill/yank operations.
2///
3/// Tracks killed (deleted) text entries. Consecutive kills can accumulate
4/// into a single entry. Supports yank (paste most recent) and yank-pop
5/// (cycle through older entries).
6#[derive(Debug, Clone, Default)]
7pub struct KillRing {
8    ring: Vec<String>,
9}
10
11impl KillRing {
12    pub fn new() -> Self {
13        Self { ring: Vec::new() }
14    }
15
16    /// Add text to the kill ring.
17    ///
18    /// If `accumulate` is true, merges with the most recent entry.
19    /// If `prepend` is true, the new text goes before the existing entry.
20    pub fn push(&mut self, text: &str, prepend: bool, accumulate: bool) {
21        if text.is_empty() {
22            return;
23        }
24
25        if accumulate && let Some(last) = self.ring.last_mut() {
26            if prepend {
27                let new_entry = format!("{}{}", text, last);
28                *last = new_entry;
29            } else {
30                last.push_str(text);
31            }
32            return;
33        }
34
35        self.ring.push(text.to_string());
36    }
37
38    /// Get the most recent entry without modifying the ring.
39    pub fn peek(&self) -> Option<&str> {
40        self.ring.last().map(|s| s.as_str())
41    }
42
43    /// Move the last entry to the front (for yank-pop cycling).
44    pub fn rotate(&mut self) {
45        if self.ring.len() > 1
46            && let Some(last) = self.ring.pop()
47        {
48            self.ring.insert(0, last);
49        }
50    }
51
52    /// Number of entries in the ring.
53    pub fn len(&self) -> usize {
54        self.ring.len()
55    }
56
57    pub fn is_empty(&self) -> bool {
58        self.ring.is_empty()
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_push_and_peek() {
68        let mut kr = KillRing::new();
69        kr.push("hello", false, false);
70        assert_eq!(kr.peek(), Some("hello"));
71        assert_eq!(kr.len(), 1);
72    }
73
74    #[test]
75    fn test_push_empty_ignored() {
76        let mut kr = KillRing::new();
77        kr.push("", false, false);
78        assert!(kr.is_empty());
79    }
80
81    #[test]
82    fn test_accumulate_append() {
83        let mut kr = KillRing::new();
84        kr.push("hello", false, false);
85        kr.push(" world", false, true);
86        assert_eq!(kr.peek(), Some("hello world"));
87        assert_eq!(kr.len(), 1);
88    }
89
90    #[test]
91    fn test_accumulate_prepend() {
92        let mut kr = KillRing::new();
93        kr.push("world", false, false);
94        kr.push("hello ", true, true);
95        assert_eq!(kr.peek(), Some("hello world"));
96    }
97
98    #[test]
99    fn test_accumulate_without_entries() {
100        let mut kr = KillRing::new();
101        kr.push("hello", false, true);
102        assert_eq!(kr.peek(), Some("hello"));
103    }
104
105    #[test]
106    fn test_rotate_single_entry() {
107        let mut kr = KillRing::new();
108        kr.push("only", false, false);
109        kr.rotate();
110        assert_eq!(kr.peek(), Some("only"));
111    }
112
113    #[test]
114    fn test_rotate_multiple() {
115        let mut kr = KillRing::new();
116        kr.push("first", false, false);
117        kr.push("second", false, false);
118        kr.push("third", false, false);
119        // ring = [first, second, third]
120        assert_eq!(kr.peek(), Some("third"));
121        kr.rotate(); // ring = [third, first, second]
122        assert_eq!(kr.peek(), Some("second"));
123        kr.rotate(); // ring = [second, third, first]
124        assert_eq!(kr.peek(), Some("first"));
125        kr.rotate(); // ring = [first, second, third]
126        assert_eq!(kr.peek(), Some("third"));
127    }
128}