Skip to main content

ass_core/parser/position_tracker/
tracker.rs

1//! Core [`PositionTracker`] type and its position-advancing operations.
2//!
3//! Holds the cursor state (byte offset, line, column) over a source string and
4//! exposes span-construction helpers used throughout the parser.
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}