use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Position {
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
pub line: u32,
pub col: u32,
}
impl Position {
pub fn new(file: Option<String>, line: u32, col: u32) -> Self {
Self { file, line, col }
}
pub fn render(&self) -> String {
match &self.file {
Some(f) => format!("{f}:{}:{}", self.line, self.col),
None => format!("{}:{}", self.line, self.col),
}
}
}
pub fn byte_to_line_col(src: &str, byte_offset: usize) -> (u32, u32) {
let cap = byte_offset.min(src.len());
let mut line: u32 = 1;
let mut last_line_start = 0usize;
for (i, b) in src.as_bytes().iter().enumerate().take(cap) {
if *b == b'\n' {
line += 1;
last_line_start = i + 1;
}
}
let col = src[last_line_start..cap].chars().count() as u32 + 1;
(line, col)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn line_col_at_start_of_file() {
assert_eq!(byte_to_line_col("hello", 0), (1, 1));
}
#[test]
fn line_col_after_newline() {
assert_eq!(byte_to_line_col("ab\ncd", 3), (2, 1));
}
#[test]
fn line_col_mid_second_line() {
assert_eq!(byte_to_line_col("ab\ncde", 5), (2, 3));
}
#[test]
fn line_col_with_multibyte_chars() {
let s = "héllo";
let off = s.find('l').unwrap();
let (line, col) = byte_to_line_col(s, off);
assert_eq!((line, col), (1, 3));
}
#[test]
fn out_of_range_offset_clamps() {
let (line, col) = byte_to_line_col("abc", 999);
assert_eq!((line, col), (1, 4));
}
#[test]
fn position_renders_with_and_without_file() {
let p = Position::new(Some("hello.lex".into()), 12, 3);
assert_eq!(p.render(), "hello.lex:12:3");
let p = Position::new(None, 5, 7);
assert_eq!(p.render(), "5:7");
}
}