cursor_iter/
lib.rs

1use std::str::CharIndices;
2
3fn is_newline(c: char) -> bool {
4    c == '\n'
5}
6
7/// A cursor that can move both forward and backward through a string.
8#[derive(Debug, Clone)]
9pub struct Cursor<'a> {
10    data: &'a str,
11    offset: usize,
12    line: usize,
13    forward: CharIndices<'a>,
14}
15
16impl<'a> Cursor<'a> {
17    pub fn new(data: &'a str) -> Self {
18        Self {
19            data,
20            offset: 0,
21            line: 0,
22            forward: data.char_indices(),
23        }
24    }
25
26    fn backward(&self) -> CharIndices<'a> {
27        self.data[..self.offset].char_indices()
28    }
29
30    pub fn next_char(&mut self) -> Option<char> {
31        self.next().map(|(_, c)| c)
32    }
33
34    pub fn next_word(&mut self) -> Option<(usize, &'a str)> {
35        let start = self.offset;
36        while let Some((_, c)) = self.peek() {
37            if c.is_whitespace() {
38                break;
39            }
40            self.next();
41        }
42        let end = self.offset;
43        if start < end {
44            Some((start, &self.data[start..=end]))
45        } else {
46            None
47        }
48    }
49
50    pub fn next_line(&mut self) -> Option<&'a str> {
51        let start = self.offset;
52        while let Some((_, c)) = self.peek() {
53            if is_newline(c) {
54                break;
55            }
56            self.next();
57        }
58        let end = self.offset;
59        if start < end {
60            Some(&self.data[start..=end])
61        } else {
62            None
63        }
64    }
65
66    fn skip_whitespace(&mut self) {
67        while let Some((_, c)) = self.peek() {
68            if !c.is_whitespace() {
69                break;
70            }
71            self.next();
72        }
73    }
74
75    pub fn prev(&mut self) -> Option<(usize, char)> {
76        let mut backward = self.backward();
77
78        let last_byte_len = backward.as_str().as_bytes().len();
79        let (pos, c) = backward.next_back()?;
80        let cur_byte_len = backward.as_str().as_bytes().len();
81        self.offset -= last_byte_len - cur_byte_len;
82
83        self.forward = self.data[self.offset..].char_indices();
84
85        if is_newline(c) {
86            self.line -= 1;
87        }
88
89        Some((pos, c))
90    }
91
92    pub fn prev_char(&mut self) -> Option<char> {
93        self.prev().map(|(_, c)| c)
94    }
95
96    pub fn peek(&self) -> Option<(usize, char)> {
97        self.forward.clone().next()
98    }
99
100    pub fn peek_char(&self) -> Option<char> {
101        self.peek().map(|(_, c)| c)
102    }
103
104    pub fn lookback(&self) -> Option<(usize, char)> {
105        self.backward().next_back()
106    }
107
108    pub fn lookback_char(&self) -> Option<char> {
109        self.lookback().map(|(_, c)| c)
110    }
111
112    pub const fn line(&self) -> usize {
113        self.line
114    }
115
116    pub const fn words(&mut self) -> CursorWords<'a, '_> {
117        CursorWords::new(self)
118    }
119
120    pub const fn words_with_lines(&mut self) -> CursorWords<'a, '_, true> {
121        CursorWords::with_lines(self)
122    }
123
124    pub const fn lines(&mut self) -> CursorLines<'a, '_> {
125        CursorLines::new(self)
126    }
127}
128
129impl Iterator for Cursor<'_> {
130    type Item = (usize, char);
131
132    fn next(&mut self) -> Option<Self::Item> {
133        let last_byte_len = self.forward.as_str().as_bytes().len();
134        let (pos, c) = self.forward.next()?;
135        let cur_byte_len = self.forward.as_str().as_bytes().len();
136        self.offset += last_byte_len - cur_byte_len;
137
138        if is_newline(c) {
139            self.line += 1;
140        }
141
142        Some((pos, c))
143    }
144}
145
146pub struct CursorWords<'a, 'b, const LINES: bool = false> {
147    cursor: &'b mut Cursor<'a>,
148}
149
150impl<'a, 'b> CursorWords<'a, 'b> {
151    pub const fn new(cursor: &'b mut Cursor<'a>) -> Self {
152        Self { cursor }
153    }
154}
155
156impl<'a, 'b> CursorWords<'a, 'b, true> {
157    pub const fn with_lines(cursor: &'b mut Cursor<'a>) -> Self {
158        Self { cursor }
159    }
160}
161
162impl<'a> Iterator for CursorWords<'a, '_, false> {
163    type Item = (usize, &'a str);
164
165    fn next(&mut self) -> Option<Self::Item> {
166        let ret = self.cursor.next_word()?;
167        self.cursor.skip_whitespace();
168        Some(ret)
169    }
170}
171
172impl<'a> Iterator for CursorWords<'a, '_, true> {
173    type Item = (usize, usize, &'a str);
174
175    fn next(&mut self) -> Option<Self::Item> {
176        let line = self.cursor.line();
177        let (offset, word) = self.cursor.next_word()?;
178        self.cursor.skip_whitespace();
179        Some((offset, line, word))
180    }
181}
182
183pub struct CursorLines<'a, 'b> {
184    cursor: &'b mut Cursor<'a>,
185}
186
187impl<'a, 'b> CursorLines<'a, 'b> {
188    pub const fn new(cursor: &'b mut Cursor<'a>) -> Self {
189        Self { cursor }
190    }
191}
192
193impl<'a> Iterator for CursorLines<'a, '_> {
194    type Item = (usize, &'a str);
195
196    fn next(&mut self) -> Option<Self::Item> {
197        let line = self.cursor.line();
198        let ret = self.cursor.next_line()?;
199        Some((line, ret))
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_new_cursor() {
209        let cursor = Cursor::new("hello");
210        assert_eq!(cursor.peek_char(), Some('h'));
211        assert_eq!(cursor.lookback_char(), None);
212    }
213
214    #[test]
215    fn test_empty_string() {
216        let mut cursor = Cursor::new("");
217        assert_eq!(cursor.next_char(), None);
218        assert_eq!(cursor.prev_char(), None);
219        assert_eq!(cursor.peek_char(), None);
220        assert_eq!(cursor.lookback_char(), None);
221    }
222
223    #[test]
224    fn test_advance() {
225        let mut cursor = Cursor::new("abc");
226
227        assert_eq!(cursor.next(), Some((0, 'a')));
228        assert_eq!(cursor.next(), Some((1, 'b')));
229        assert_eq!(cursor.next(), Some((2, 'c')));
230        assert_eq!(cursor.next(), None);
231    }
232
233    #[test]
234    fn test_prev() {
235        let mut cursor = Cursor::new("abc");
236
237        // Advance to end
238        cursor.next();
239        cursor.next();
240        cursor.next();
241
242        assert_eq!(cursor.prev(), Some((2, 'c')));
243        assert_eq!(cursor.prev(), Some((1, 'b')));
244        assert_eq!(cursor.prev(), Some((0, 'a')));
245        assert_eq!(cursor.prev(), None);
246    }
247
248    #[test]
249    fn test_peek() {
250        let mut cursor = Cursor::new("abc");
251
252        assert_eq!(cursor.peek(), Some((0, 'a')));
253        cursor.next();
254        assert_eq!(cursor.peek(), Some((1, 'b')));
255        cursor.next();
256        assert_eq!(cursor.peek(), Some((2, 'c')));
257        cursor.next();
258        assert_eq!(cursor.peek(), None);
259    }
260
261    #[test]
262    fn test_lookback() {
263        let mut cursor = Cursor::new("abc");
264
265        assert_eq!(cursor.lookback(), None);
266        cursor.next();
267        assert_eq!(cursor.lookback(), Some((0, 'a')));
268        cursor.next();
269        assert_eq!(cursor.lookback(), Some((1, 'b')));
270    }
271
272    #[test]
273    fn test_bidirectional_movement() {
274        let mut cursor = Cursor::new("hello");
275
276        assert_eq!(cursor.next_char(), Some('h'));
277        assert_eq!(cursor.next_char(), Some('e'));
278        assert_eq!(cursor.prev_char(), Some('e'));
279        assert_eq!(cursor.prev_char(), Some('h'));
280        assert_eq!(cursor.next_char(), Some('h'));
281    }
282
283    #[test]
284    fn test_unicode() {
285        let mut cursor = Cursor::new("hello 👋 world");
286
287        // Advance to emoji
288        for _ in 0..6 {
289            cursor.next();
290        }
291
292        assert_eq!(cursor.next_char(), Some('👋'));
293        assert_eq!(cursor.prev_char(), Some('👋'));
294    }
295
296    #[test]
297    fn test_iterator_implementation() {
298        let cursor = Cursor::new("abc");
299        let collected: Vec<(usize, char)> = cursor.collect();
300
301        assert_eq!(collected, vec![(0, 'a'), (1, 'b'), (2, 'c'),]);
302    }
303
304    #[test]
305    fn test_mixed_operations() {
306        let mut cursor = Cursor::new("test");
307
308        assert_eq!(cursor.next_char(), Some('t'));
309        assert_eq!(cursor.peek_char(), Some('e'));
310        assert_eq!(cursor.prev_char(), Some('t'));
311        assert_eq!(cursor.lookback_char(), None);
312        assert_eq!(cursor.next_char(), Some('t'));
313        assert_eq!(cursor.next_char(), Some('e'));
314        assert_eq!(cursor.lookback_char(), Some('e'));
315    }
316}