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,
}
pub struct SourceMap {
line_starts: Vec<u32>,
}
impl SourceMap {
pub fn new(source: &str) -> Self {
let mut line_starts = vec![0u32];
for (i, byte) in source.bytes().enumerate() {
if byte == b'\n' {
line_starts.push((i + 1) as u32);
}
}
Self { line_starts }
}
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> {
self.line_starts
.get(lc.line as usize)
.map(|start| start + lc.col)
}
}
#[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 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 });
}
}