use crate::error::Pos;
#[derive(Clone, Debug)]
pub struct Cursor<'a> {
input: &'a [u8],
pos: usize,
line: u32,
col: u32,
}
impl<'a> Cursor<'a> {
pub const fn new(input: &'a [u8]) -> Self {
Self {
input,
pos: 0,
line: 1,
col: 1,
}
}
#[allow(clippy::indexing_slicing)]
pub const fn current(&self) -> Option<u8> {
if self.pos < self.input.len() {
Some(self.input[self.pos])
} else {
None
}
}
#[allow(clippy::indexing_slicing)]
pub const fn peek(&self, ahead: usize) -> Option<u8> {
let idx = self.pos.saturating_add(ahead);
if idx < self.input.len() {
Some(self.input[idx])
} else {
None
}
}
#[allow(clippy::indexing_slicing)]
pub fn peek_bytes(&self, n: usize) -> Option<&[u8]> {
let end = self.pos.saturating_add(n);
if end <= self.input.len() {
Some(&self.input[self.pos..end])
} else {
None
}
}
pub fn advance_by(&mut self, n: usize) {
for _ in 0..n {
if self.is_eof() {
break;
}
self.advance();
}
}
pub fn advance(&mut self) {
if let Some(b) = self.current() {
self.pos += 1;
if b == b'\n' {
self.line += 1;
self.col = 1;
} else {
self.col += 1;
}
}
}
pub fn skip_whitespace(&mut self) {
while let Some(b) = self.current() {
if matches!(b, b' ' | b'\t' | b'\n' | b'\r') {
self.advance();
} else {
break;
}
}
}
pub fn consume(&mut self, expected: u8) -> bool {
if self.current() == Some(expected) {
self.advance();
true
} else {
false
}
}
pub const fn position(&self) -> Pos {
Pos::new(self.pos, self.line, self.col)
}
pub const fn is_eof(&self) -> bool {
self.pos >= self.input.len()
}
#[allow(clippy::indexing_slicing)]
pub fn remaining(&self) -> &[u8] {
&self.input[self.pos..]
}
pub const fn pos(&self) -> usize {
self.pos
}
#[allow(clippy::indexing_slicing)]
pub fn slice_from(&self, start: usize) -> &'a [u8] {
&self.input[start..self.pos]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cursor_basic() {
let mut cursor = Cursor::new(b"hello");
assert_eq!(cursor.current(), Some(b'h'));
assert_eq!(cursor.peek(1), Some(b'e'));
cursor.advance();
assert_eq!(cursor.current(), Some(b'e'));
}
#[test]
fn test_cursor_whitespace() {
let mut cursor = Cursor::new(b" \t\nhello");
cursor.skip_whitespace();
assert_eq!(cursor.current(), Some(b'h'));
assert_eq!(cursor.position().line, 2);
}
#[test]
fn test_cursor_consume() {
let mut cursor = Cursor::new(b"abc");
assert!(cursor.consume(b'a'));
assert!(!cursor.consume(b'z'));
assert_eq!(cursor.current(), Some(b'b'));
}
#[test]
fn test_cursor_eof() {
let cursor = Cursor::new(b"");
assert!(cursor.is_eof());
assert_eq!(cursor.current(), None);
}
#[test]
fn test_cursor_slice() {
let mut cursor = Cursor::new(b"hello world");
let start = cursor.pos();
cursor.advance();
cursor.advance();
cursor.advance();
cursor.advance();
assert_eq!(cursor.slice_from(start), b"hell");
}
}