#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub struct Span {
pub start: u32,
pub len: u32,
}
impl Span {
pub fn new(start: usize, len: usize) -> Self {
Span {
start: start as u32,
len: len as u32,
}
}
pub fn end(self) -> usize {
(self.start + self.len) as usize
}
pub fn merge(self, other: Span) -> Span {
let start = self.start.min(other.start);
let end = (self.end().max(other.end())) as u32;
Span {
start,
len: end - start,
}
}
pub fn to(self, other: Span) -> Span {
self.merge(other)
}
pub fn slice(self, src: &str) -> &str {
&src[self.start as usize..self.end()]
}
}
pub struct LineIndex {
line_starts: Vec<u32>,
}
impl LineIndex {
pub fn new(src: &str) -> Self {
let mut line_starts = vec![0u32];
for (i, b) in src.bytes().enumerate() {
if b == b'\n' {
line_starts.push(i as u32 + 1);
}
}
LineIndex { line_starts }
}
pub fn location(&self, src: &str, byte_offset: usize) -> (usize, usize) {
let byte_offset = byte_offset.min(src.len());
let off = byte_offset as u32;
let line = self.line_starts.partition_point(|&s| s <= off) - 1;
let line_start = self.line_starts[line] as usize;
let col = src[line_start..byte_offset].chars().count() + 1;
(line + 1, col)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_overlapping_spans() {
let a = Span::new(0, 5);
let b = Span::new(3, 5);
let m = a.merge(b);
assert_eq!(m.start, 0);
assert_eq!(m.len, 8);
}
#[test]
fn merge_adjacent_spans() {
let a = Span::new(0, 3);
let b = Span::new(3, 4);
let m = a.merge(b);
assert_eq!(m.start, 0);
assert_eq!(m.len, 7);
}
#[test]
fn merge_gapped_spans() {
let a = Span::new(0, 2);
let b = Span::new(5, 2);
let m = a.merge(b);
assert_eq!(m.start, 0);
assert_eq!(m.len, 7);
}
#[test]
fn merge_is_order_independent() {
let a = Span::new(2, 3); let b = Span::new(7, 4); assert_eq!(a.merge(b), b.merge(a));
let m = a.merge(b);
assert_eq!(m.start, 2);
assert_eq!(m.len, 9);
}
#[test]
fn to_is_an_alias_for_merge() {
let a = Span::new(1, 2);
let b = Span::new(10, 3);
assert_eq!(a.to(b), a.merge(b));
assert_eq!(b.to(a), a.merge(b));
}
#[test]
fn slice_returns_the_covered_source() {
let src = "let x = 42;";
let span = Span::new(4, 1); assert_eq!(span.slice(src), "x");
let span = Span::new(8, 2); assert_eq!(span.slice(src), "42");
let whole = Span::new(0, src.len());
assert_eq!(whole.slice(src), src);
}
#[test]
fn end_is_start_plus_len() {
let s = Span::new(5, 3);
assert_eq!(s.end(), 8);
assert_eq!(Span::new(0, 0).end(), 0);
}
#[test]
fn location_at_offset_zero_is_line_1_col_1() {
let src = "abc\ndef";
let idx = LineIndex::new(src);
assert_eq!(idx.location(src, 0), (1, 1));
}
#[test]
fn location_at_a_newline_is_on_the_line_it_ends() {
let src = "abc\ndef";
let idx = LineIndex::new(src);
assert_eq!(idx.location(src, 3), (1, 4));
}
#[test]
fn location_just_after_a_newline_is_next_line_col_1() {
let src = "abc\ndef";
let idx = LineIndex::new(src);
assert_eq!(idx.location(src, 4), (2, 1));
}
#[test]
fn location_at_end_of_file_is_valid_and_does_not_panic() {
let src = "abc\ndef";
let idx = LineIndex::new(src);
assert_eq!(idx.location(src, src.len()), (2, 4));
let empty = "";
let eidx = LineIndex::new(empty);
assert_eq!(eidx.location(empty, 0), (1, 1));
}
#[test]
fn location_clamps_out_of_range_offset_instead_of_panicking() {
let src = "abc\ndef";
let idx = LineIndex::new(src);
let at_eof = idx.location(src, src.len());
assert_eq!(idx.location(src, src.len() + 1), at_eof);
assert_eq!(idx.location(src, src.len() + 100), at_eof);
let empty = "";
let eidx = LineIndex::new(empty);
assert_eq!(eidx.location(empty, 5), (1, 1));
}
#[test]
fn location_counts_columns_as_characters_so_tabs_are_one_column() {
let src = "fn f() {\n\t\tx\n}";
let idx = LineIndex::new(src);
let x_offset = src.find('x').unwrap();
assert_eq!(idx.location(src, x_offset), (2, 3));
let src2 = "fn f() {\n y\n}";
let idx2 = LineIndex::new(src2);
let y_offset = src2.find('y').unwrap();
assert_eq!(idx2.location(src2, y_offset), (2, 3));
}
#[test]
fn crlf_line_endings_break_on_the_newline_not_the_carriage_return() {
let src = "a\r\nb";
let idx = LineIndex::new(src);
assert_eq!(idx.location(src, 1), (1, 2)); assert_eq!(idx.location(src, 3), (2, 1)); }
}