use std::cell::{Cell, OnceCell};
use crate::parse::line_separator_at;
use crate::{Problem, Severity};
#[derive(Debug)]
pub struct Report<'a> {
text: &'a str,
problems: Vec<Problem>,
line_starts: OnceCell<Vec<usize>>,
furthest: Cell<(usize, usize, usize)>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Location {
pub line: usize,
pub column: usize,
}
impl<'a> Report<'a> {
pub(crate) fn new(text: &'a str, mut problems: Vec<Problem>) -> Self {
problems.sort_by_key(Problem::offset);
Report {
text,
problems,
line_starts: OnceCell::new(),
furthest: Cell::new((0, 1, 1)),
}
}
pub fn problems(&self) -> &[Problem] {
&self.problems
}
pub fn passed(&self) -> bool {
self.problems.iter().all(|p| p.severity() < Severity::Error)
}
pub fn location(&self, offset: usize) -> Location {
let offset = offset.min(self.text.len());
let starts = self.line_starts.get_or_init(|| line_starts(self.text));
let line = starts.partition_point(|&s| s <= offset);
let start = starts[line - 1];
let count =
|from: usize, to: usize| self.text.get(from..to).map_or(0, |s| s.chars().count());
let (furthest, furthest_line, furthest_column) = self.furthest.get();
let column = if furthest_line != line {
1 + count(start, offset)
} else if furthest <= offset {
furthest_column + count(furthest, offset)
} else if furthest - offset < offset - start {
furthest_column - count(offset, furthest)
} else {
1 + count(start, offset)
};
if offset >= furthest {
self.furthest.set((offset, line, column));
}
Location { line, column }
}
}
fn line_starts(text: &str) -> Vec<usize> {
let bytes = text.as_bytes();
let mut starts = vec![0];
let mut i = 0;
while let Some(found) = memchr::memchr3(b'\n', b'\r', 0xE2, &bytes[i..]) {
let at = i + found;
match bytes[at] {
b'\r' if bytes.get(at + 1) == Some(&b'\n') => {
starts.push(at + 2);
i = at + 2;
continue;
}
b'\n' | b'\r' => starts.push(at + 1),
_ if line_separator_at(bytes, at) => starts.push(at + 3),
_ => {}
}
i = at + 1;
}
starts
}
#[cfg(test)]
mod tests {
use super::{Location, Report, line_starts};
#[test]
fn every_json5_line_terminator_starts_a_line() {
assert_eq!(
line_starts("a\nb\r\nc\rd\u{2028}e\u{2029}f"),
vec![0, 2, 5, 7, 11, 15]
);
assert_eq!(line_starts("no breaks, but é and ★"), vec![0]);
}
#[test]
fn locations_agree_in_any_order() {
let report = Report::new("aé★b\ncdé", Vec::new());
let at = |offset| report.location(offset);
let expected = [
(6, (1, 4)),
(1, (1, 2)),
(9, (2, 2)),
(7, (1, 5)),
(12, (2, 4)),
];
for (offset, (line, column)) in expected {
assert_eq!(at(offset), Location { line, column }, "offset {offset}");
}
}
}