use crate::parser::ast::Span;
#[derive(Debug, Clone)]
pub struct PositionTracker<'a> {
source: &'a str,
offset: usize,
line: u32,
column: u32,
line_start: usize,
}
impl<'a> PositionTracker<'a> {
#[must_use]
pub const fn new(source: &'a str) -> Self {
Self {
source,
offset: 0,
line: 1,
column: 1,
line_start: 0,
}
}
#[must_use]
pub const fn new_at(source: &'a str, offset: usize, line: u32, column: u32) -> Self {
Self {
source,
offset,
line,
column,
line_start: offset.saturating_sub((column - 1) as usize),
}
}
#[must_use]
pub const fn offset(&self) -> usize {
self.offset
}
#[must_use]
pub const fn line(&self) -> u32 {
self.line
}
#[must_use]
pub const fn column(&self) -> u32 {
self.column
}
pub fn advance(&mut self, bytes: usize) {
let end = (self.offset + bytes).min(self.source.len());
while self.offset < end {
if self.source.as_bytes().get(self.offset) == Some(&b'\n') {
self.offset += 1;
self.line += 1;
self.column = 1;
self.line_start = self.offset;
} else {
self.offset += 1;
self.column += 1;
}
}
}
pub fn advance_to(&mut self, target_offset: usize) {
if target_offset > self.offset {
self.advance(target_offset - self.offset);
}
}
pub fn skip_whitespace(&mut self) {
while let Some(&ch) = self.source.as_bytes().get(self.offset) {
if ch == b' ' || ch == b'\t' || ch == b'\r' {
self.advance(1);
} else {
break;
}
}
}
pub fn skip_line(&mut self) {
while let Some(&ch) = self.source.as_bytes().get(self.offset) {
self.advance(1);
if ch == b'\n' {
break;
}
}
}
#[must_use]
pub fn remaining(&self) -> &'a str {
&self.source[self.offset..]
}
#[must_use]
pub const fn is_at_end(&self) -> bool {
self.offset >= self.source.len()
}
#[must_use]
pub const fn span_from(&self, start: &PositionTracker) -> Span {
Span::new(start.offset, self.offset, start.line, start.column)
}
#[must_use]
pub const fn span_for(&self, length: usize) -> Span {
Span::new(self.offset, self.offset + length, self.line, self.column)
}
#[must_use]
pub const fn checkpoint(&self) -> Self {
PositionTracker {
source: self.source,
offset: self.offset,
line: self.line,
column: self.column,
line_start: self.line_start,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tracker_creation() {
let source = "Hello\nWorld";
let tracker = PositionTracker::new(source);
assert_eq!(tracker.offset(), 0);
assert_eq!(tracker.line(), 1);
assert_eq!(tracker.column(), 1);
}
#[test]
fn tracker_advance_single_line() {
let source = "Hello World";
let mut tracker = PositionTracker::new(source);
tracker.advance(5);
assert_eq!(tracker.offset(), 5);
assert_eq!(tracker.line(), 1);
assert_eq!(tracker.column(), 6);
tracker.advance(6);
assert_eq!(tracker.offset(), 11);
assert_eq!(tracker.line(), 1);
assert_eq!(tracker.column(), 12);
}
#[test]
fn tracker_advance_multiline() {
let source = "Hello\nWorld\nTest";
let mut tracker = PositionTracker::new(source);
tracker.advance(6); assert_eq!(tracker.offset(), 6);
assert_eq!(tracker.line(), 2);
assert_eq!(tracker.column(), 1);
tracker.advance(6); assert_eq!(tracker.offset(), 12);
assert_eq!(tracker.line(), 3);
assert_eq!(tracker.column(), 1);
}
#[test]
fn tracker_skip_whitespace() {
let source = " Hello";
let mut tracker = PositionTracker::new(source);
tracker.skip_whitespace();
assert_eq!(tracker.offset(), 3);
assert_eq!(tracker.column(), 4);
}
#[test]
fn tracker_skip_line() {
let source = "Hello World\nNext Line";
let mut tracker = PositionTracker::new(source);
tracker.skip_line();
assert_eq!(tracker.offset(), 12);
assert_eq!(tracker.line(), 2);
assert_eq!(tracker.column(), 1);
}
#[test]
fn tracker_span_creation() {
let source = "Hello\nWorld";
let mut tracker = PositionTracker::new(source);
let start = tracker.checkpoint();
tracker.advance(5);
let span = tracker.span_from(&start);
assert_eq!(span.start, 0);
assert_eq!(span.end, 5);
assert_eq!(span.line, 1);
assert_eq!(span.column, 1);
}
#[test]
fn tracker_remaining_text() {
let source = "Hello World";
let mut tracker = PositionTracker::new(source);
tracker.advance(6);
assert_eq!(tracker.remaining(), "World");
}
#[test]
fn tracker_advance_to() {
let source = "Hello World Test";
let mut tracker = PositionTracker::new(source);
tracker.advance_to(11);
assert_eq!(tracker.offset(), 11);
assert_eq!(tracker.column(), 12);
}
#[test]
fn tracker_at_end() {
let source = "Hi";
let mut tracker = PositionTracker::new(source);
assert!(!tracker.is_at_end());
tracker.advance(2);
assert!(tracker.is_at_end());
}
#[test]
fn tracker_new_at_position() {
let source = "Hello\nWorld";
let tracker = PositionTracker::new_at(source, 6, 2, 1);
assert_eq!(tracker.offset(), 6);
assert_eq!(tracker.line(), 2);
assert_eq!(tracker.column(), 1);
}
#[test]
fn tracker_span_for() {
let source = "Hello World";
let tracker = PositionTracker::new(source);
let span = tracker.span_for(5);
assert_eq!(span.start, 0);
assert_eq!(span.end, 5);
assert_eq!(span.line, 1);
assert_eq!(span.column, 1);
}
#[test]
fn tracker_windows_line_endings() {
let source = "Hello\r\nWorld";
let mut tracker = PositionTracker::new(source);
tracker.advance(7); assert_eq!(tracker.offset(), 7);
assert_eq!(tracker.line(), 2);
assert_eq!(tracker.column(), 1);
}
}