ass_core/parser/
position_tracker.rs

1//! Position tracking utilities for incremental parsing
2//!
3//! Provides efficient position tracking with line/column information
4//! for accurate span generation during parsing.
5
6use crate::parser::ast::Span;
7
8/// Tracks current position in source text with line/column information
9#[derive(Debug, Clone)]
10pub struct PositionTracker<'a> {
11    /// Source text being tracked
12    source: &'a str,
13    /// Current byte offset in source
14    offset: usize,
15    /// Current line number (1-based)
16    line: u32,
17    /// Current column number (1-based)
18    column: u32,
19    /// Byte offset of current line start
20    line_start: usize,
21}
22
23impl<'a> PositionTracker<'a> {
24    /// Create a new position tracker for source text
25    #[must_use]
26    pub const fn new(source: &'a str) -> Self {
27        Self {
28            source,
29            offset: 0,
30            line: 1,
31            column: 1,
32            line_start: 0,
33        }
34    }
35
36    /// Create a tracker starting at a specific position
37    #[must_use]
38    pub const fn new_at(source: &'a str, offset: usize, line: u32, column: u32) -> Self {
39        Self {
40            source,
41            offset,
42            line,
43            column,
44            line_start: offset.saturating_sub((column - 1) as usize),
45        }
46    }
47
48    /// Get current byte offset
49    #[must_use]
50    pub const fn offset(&self) -> usize {
51        self.offset
52    }
53
54    /// Get current line number (1-based)
55    #[must_use]
56    pub const fn line(&self) -> u32 {
57        self.line
58    }
59
60    /// Get current column number (1-based)
61    #[must_use]
62    pub const fn column(&self) -> u32 {
63        self.column
64    }
65
66    /// Advance position by a given number of bytes
67    pub fn advance(&mut self, bytes: usize) {
68        let end = (self.offset + bytes).min(self.source.len());
69
70        while self.offset < end {
71            if self.source.as_bytes().get(self.offset) == Some(&b'\n') {
72                self.offset += 1;
73                self.line += 1;
74                self.column = 1;
75                self.line_start = self.offset;
76            } else {
77                self.offset += 1;
78                self.column += 1;
79            }
80        }
81    }
82
83    /// Advance to a specific byte offset
84    pub fn advance_to(&mut self, target_offset: usize) {
85        if target_offset > self.offset {
86            self.advance(target_offset - self.offset);
87        }
88    }
89
90    /// Skip whitespace and update position
91    pub fn skip_whitespace(&mut self) {
92        while let Some(&ch) = self.source.as_bytes().get(self.offset) {
93            if ch == b' ' || ch == b'\t' || ch == b'\r' {
94                self.advance(1);
95            } else {
96                break;
97            }
98        }
99    }
100
101    /// Skip to end of current line
102    pub fn skip_line(&mut self) {
103        while let Some(&ch) = self.source.as_bytes().get(self.offset) {
104            self.advance(1);
105            if ch == b'\n' {
106                break;
107            }
108        }
109    }
110
111    /// Get remaining source text from current position
112    #[must_use]
113    pub fn remaining(&self) -> &'a str {
114        &self.source[self.offset..]
115    }
116
117    /// Check if at end of source
118    #[must_use]
119    pub const fn is_at_end(&self) -> bool {
120        self.offset >= self.source.len()
121    }
122
123    /// Create a span from a start position to current position
124    #[must_use]
125    pub const fn span_from(&self, start: &PositionTracker) -> Span {
126        Span::new(start.offset, self.offset, start.line, start.column)
127    }
128
129    /// Create a span for a range of bytes from current position
130    #[must_use]
131    pub const fn span_for(&self, length: usize) -> Span {
132        Span::new(self.offset, self.offset + length, self.line, self.column)
133    }
134
135    /// Clone current position state
136    #[must_use]
137    pub const fn checkpoint(&self) -> Self {
138        PositionTracker {
139            source: self.source,
140            offset: self.offset,
141            line: self.line,
142            column: self.column,
143            line_start: self.line_start,
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    #[test]
152    fn tracker_creation() {
153        let source = "Hello\nWorld";
154        let tracker = PositionTracker::new(source);
155        assert_eq!(tracker.offset(), 0);
156        assert_eq!(tracker.line(), 1);
157        assert_eq!(tracker.column(), 1);
158    }
159
160    #[test]
161    fn tracker_advance_single_line() {
162        let source = "Hello World";
163        let mut tracker = PositionTracker::new(source);
164
165        tracker.advance(5);
166        assert_eq!(tracker.offset(), 5);
167        assert_eq!(tracker.line(), 1);
168        assert_eq!(tracker.column(), 6);
169
170        tracker.advance(6);
171        assert_eq!(tracker.offset(), 11);
172        assert_eq!(tracker.line(), 1);
173        assert_eq!(tracker.column(), 12);
174    }
175
176    #[test]
177    fn tracker_advance_multiline() {
178        let source = "Hello\nWorld\nTest";
179        let mut tracker = PositionTracker::new(source);
180
181        tracker.advance(6); // "Hello\n"
182        assert_eq!(tracker.offset(), 6);
183        assert_eq!(tracker.line(), 2);
184        assert_eq!(tracker.column(), 1);
185
186        tracker.advance(6); // "World\n"
187        assert_eq!(tracker.offset(), 12);
188        assert_eq!(tracker.line(), 3);
189        assert_eq!(tracker.column(), 1);
190    }
191
192    #[test]
193    fn tracker_skip_whitespace() {
194        let source = "   Hello";
195        let mut tracker = PositionTracker::new(source);
196
197        tracker.skip_whitespace();
198        assert_eq!(tracker.offset(), 3);
199        assert_eq!(tracker.column(), 4);
200    }
201
202    #[test]
203    fn tracker_skip_line() {
204        let source = "Hello World\nNext Line";
205        let mut tracker = PositionTracker::new(source);
206
207        tracker.skip_line();
208        assert_eq!(tracker.offset(), 12);
209        assert_eq!(tracker.line(), 2);
210        assert_eq!(tracker.column(), 1);
211    }
212
213    #[test]
214    fn tracker_span_creation() {
215        let source = "Hello\nWorld";
216        let mut tracker = PositionTracker::new(source);
217
218        let start = tracker.checkpoint();
219        tracker.advance(5);
220
221        let span = tracker.span_from(&start);
222        assert_eq!(span.start, 0);
223        assert_eq!(span.end, 5);
224        assert_eq!(span.line, 1);
225        assert_eq!(span.column, 1);
226    }
227
228    #[test]
229    fn tracker_remaining_text() {
230        let source = "Hello World";
231        let mut tracker = PositionTracker::new(source);
232
233        tracker.advance(6);
234        assert_eq!(tracker.remaining(), "World");
235    }
236
237    #[test]
238    fn tracker_advance_to() {
239        let source = "Hello World Test";
240        let mut tracker = PositionTracker::new(source);
241
242        tracker.advance_to(11);
243        assert_eq!(tracker.offset(), 11);
244        assert_eq!(tracker.column(), 12);
245    }
246
247    #[test]
248    fn tracker_at_end() {
249        let source = "Hi";
250        let mut tracker = PositionTracker::new(source);
251
252        assert!(!tracker.is_at_end());
253        tracker.advance(2);
254        assert!(tracker.is_at_end());
255    }
256
257    #[test]
258    fn tracker_new_at_position() {
259        let source = "Hello\nWorld";
260        let tracker = PositionTracker::new_at(source, 6, 2, 1);
261
262        assert_eq!(tracker.offset(), 6);
263        assert_eq!(tracker.line(), 2);
264        assert_eq!(tracker.column(), 1);
265    }
266
267    #[test]
268    fn tracker_span_for() {
269        let source = "Hello World";
270        let tracker = PositionTracker::new(source);
271
272        let span = tracker.span_for(5);
273        assert_eq!(span.start, 0);
274        assert_eq!(span.end, 5);
275        assert_eq!(span.line, 1);
276        assert_eq!(span.column, 1);
277    }
278
279    #[test]
280    fn tracker_windows_line_endings() {
281        let source = "Hello\r\nWorld";
282        let mut tracker = PositionTracker::new(source);
283
284        tracker.advance(7); // "Hello\r\n"
285        assert_eq!(tracker.offset(), 7);
286        assert_eq!(tracker.line(), 2);
287        assert_eq!(tracker.column(), 1);
288    }
289}