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