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