use php_ast::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LineCol {
pub line: u32,
pub col: u32,
}
impl LineCol {
pub fn to_one_based(self) -> (u32, u32) {
(self.line + 1, self.col + 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LineColSpan {
pub start: LineCol,
pub end: LineCol,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceMap {
line_starts: Vec<u32>,
len: u32,
}
impl SourceMap {
pub fn new(source: &str) -> Self {
let mut line_starts = Vec::with_capacity(source.len() / 40 + 1);
line_starts.push(0u32);
for pos in memchr::memchr_iter(b'\n', source.as_bytes()) {
line_starts.push((pos + 1) as u32);
}
Self {
line_starts,
len: source.len() as u32,
}
}
pub fn empty() -> Self {
Self {
line_starts: vec![0u32],
len: u32::MAX,
}
}
pub fn line_count(&self) -> usize {
self.line_starts.len()
}
pub fn line_start(&self, line: u32) -> Option<u32> {
self.line_starts.get(line as usize).copied()
}
pub fn offset_to_line_col(&self, offset: u32) -> LineCol {
let line = match self.line_starts.binary_search(&offset) {
Ok(exact) => exact,
Err(after) => after - 1,
};
let col = offset - self.line_starts[line];
LineCol {
line: line as u32,
col,
}
}
pub fn span_to_line_col(&self, span: Span) -> LineColSpan {
LineColSpan {
start: self.offset_to_line_col(span.start),
end: self.offset_to_line_col(span.end),
}
}
pub fn line_col_to_offset(&self, lc: LineCol) -> Option<u32> {
let line = lc.line as usize;
let start = *self.line_starts.get(line)?;
let end = match self.line_starts.get(line + 1) {
Some(next_start) => next_start - 1,
None => self.len,
};
start.checked_add(lc.col).filter(|&offset| offset <= end)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_source() {
let map = SourceMap::new("");
assert_eq!(map.line_count(), 1);
assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
}
#[test]
fn single_line_no_newline() {
let map = SourceMap::new("<?php echo 1;");
assert_eq!(map.line_count(), 1);
assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
assert_eq!(map.offset_to_line_col(6), LineCol { line: 0, col: 6 });
}
#[test]
fn multiple_lines() {
let src = "<?php\necho 'hi';\nreturn;\n";
let map = SourceMap::new(src);
assert_eq!(map.line_count(), 4);
assert_eq!(map.offset_to_line_col(0), LineCol { line: 0, col: 0 });
assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
assert_eq!(map.offset_to_line_col(6), LineCol { line: 1, col: 0 });
assert_eq!(map.offset_to_line_col(17), LineCol { line: 2, col: 0 });
}
#[test]
fn span_conversion() {
let src = "<?php\necho 'hi';\n";
let map = SourceMap::new(src);
let span = Span::new(6, 10); let lc = map.span_to_line_col(span);
assert_eq!(lc.start, LineCol { line: 1, col: 0 });
assert_eq!(lc.end, LineCol { line: 1, col: 4 });
}
#[test]
fn round_trip() {
let src = "<?php\necho 'hi';\nreturn;\n";
let map = SourceMap::new(src);
let lc = LineCol { line: 1, col: 5 };
let offset = map.line_col_to_offset(lc).unwrap();
assert_eq!(map.offset_to_line_col(offset), lc);
}
#[test]
fn column_past_line_end_is_rejected() {
let map = SourceMap::new("ab\ncd");
assert_eq!(map.line_col_to_offset(LineCol { line: 0, col: 2 }), Some(2));
assert_eq!(map.line_col_to_offset(LineCol { line: 0, col: 3 }), None);
assert_eq!(map.line_col_to_offset(LineCol { line: 1, col: 2 }), Some(5));
assert_eq!(map.line_col_to_offset(LineCol { line: 1, col: 3 }), None);
assert_eq!(
map.line_col_to_offset(LineCol {
line: 1,
col: u32::MAX
}),
None
);
}
#[test]
fn empty_map_accepts_any_column() {
let map = SourceMap::empty();
assert_eq!(
map.line_col_to_offset(LineCol { line: 0, col: 42 }),
Some(42)
);
assert_eq!(map.line_col_to_offset(LineCol { line: 1, col: 0 }), None);
}
#[test]
fn one_based() {
let lc = LineCol { line: 0, col: 0 };
assert_eq!(lc.to_one_based(), (1, 1));
let lc = LineCol { line: 2, col: 5 };
assert_eq!(lc.to_one_based(), (3, 6));
}
#[test]
fn line_start_lookup() {
let src = "aaa\nbbb\nccc";
let map = SourceMap::new(src);
assert_eq!(map.line_start(0), Some(0));
assert_eq!(map.line_start(1), Some(4));
assert_eq!(map.line_start(2), Some(8));
assert_eq!(map.line_start(3), None);
}
#[test]
fn crlf_treated_as_two_bytes() {
let src = "a\r\nb";
let map = SourceMap::new(src);
assert_eq!(map.line_count(), 2);
assert_eq!(map.offset_to_line_col(3), LineCol { line: 1, col: 0 });
}
}