ass_core/parser/
position_tracker.rs1use crate::parser::ast::Span;
7
8#[derive(Debug, Clone)]
10pub struct PositionTracker<'a> {
11 source: &'a str,
13 offset: usize,
15 line: u32,
17 column: u32,
19 line_start: usize,
21}
22
23impl<'a> PositionTracker<'a> {
24 #[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 #[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 #[must_use]
50 pub const fn offset(&self) -> usize {
51 self.offset
52 }
53
54 #[must_use]
56 pub const fn line(&self) -> u32 {
57 self.line
58 }
59
60 #[must_use]
62 pub const fn column(&self) -> u32 {
63 self.column
64 }
65
66 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 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 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 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 #[must_use]
113 pub fn remaining(&self) -> &'a str {
114 &self.source[self.offset..]
115 }
116
117 #[must_use]
119 pub const fn is_at_end(&self) -> bool {
120 self.offset >= self.source.len()
121 }
122
123 #[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 #[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 #[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); assert_eq!(tracker.offset(), 6);
183 assert_eq!(tracker.line(), 2);
184 assert_eq!(tracker.column(), 1);
185
186 tracker.advance(6); 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); assert_eq!(tracker.offset(), 7);
286 assert_eq!(tracker.line(), 2);
287 assert_eq!(tracker.column(), 1);
288 }
289}