pub struct ErrorContext<'a> {
pub source: &'a str,
pub filename: Option<&'a str>,
line_starts: Vec<usize>,
}
impl<'a> ErrorContext<'a> {
#[must_use]
pub fn new(source: &'a str, filename: Option<&'a str>) -> Self {
let line_starts = compute_line_starts(source);
Self {
source,
filename,
line_starts,
}
}
#[must_use]
pub fn position(&self, offset: u32) -> Position {
let offset = offset as usize;
let line_idx = line_number(&self.line_starts, offset);
let line_start = self.line_starts.get(line_idx).copied().unwrap_or(0);
let column = column_number(self.source, line_start, offset);
Position {
line: line_idx + 1, column,
}
}
#[must_use]
pub fn line_at(&self, offset: usize) -> (usize, &str) {
let line_idx = line_number(&self.line_starts, offset);
let line_num = line_idx + 1;
let line_start = self.line_starts.get(line_idx).copied().unwrap_or(0);
let line_end = self
.line_starts
.get(line_idx + 1)
.copied()
.unwrap_or(self.source.len());
let line_end = if line_end > line_start
&& self
.source
.as_bytes()
.get(line_end - 1)
.is_some_and(|&b| b == b'\n')
{
line_end - 1
} else {
line_end
};
let line_text = &self.source[line_start..line_end];
(line_num, line_text)
}
#[must_use]
pub fn line_start(&self, line_idx: usize) -> usize {
self.line_starts.get(line_idx).copied().unwrap_or(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Position {
pub line: usize,
pub column: usize,
}
fn compute_line_starts(source: &str) -> Vec<usize> {
std::iter::once(0)
.chain(
source.match_indices('\n').map(|(i, _)| i + 1), )
.collect()
}
fn line_number(line_starts: &[usize], offset: usize) -> usize {
match line_starts.binary_search(&offset) {
Ok(idx) => idx,
Err(idx) => idx.saturating_sub(1),
}
}
fn column_number(source: &str, line_start: usize, offset: usize) -> usize {
if offset <= line_start {
return 0;
}
let end = offset.min(source.len());
source[line_start..end].chars().count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_position() {
let source = "line1\nline2\nline3";
let ctx = ErrorContext::new(source, None);
let pos = ctx.position(0);
assert_eq!(pos.line, 1);
let pos = ctx.position(6);
assert_eq!(pos.line, 2);
}
}