use core::ops::Range;
#[derive(Debug)]
pub struct PositionMap {
lines: Vec<usize>,
}
impl PositionMap {
pub fn new(source: &str) -> Self {
let mut lines = vec![0];
for (index, &character) in source.as_bytes().iter().enumerate() {
if character == b'\n' {
lines.push(index + 1);
}
}
if source.len() > *lines.iter().last().unwrap() {
lines.push(source.len());
}
Self { lines }
}
pub fn line_index(&self, offset: usize) -> Option<usize> {
let line = match self.lines.binary_search(&offset) {
Ok(line) => line,
Err(line) => line - 1,
};
if line >= self.lines.len() - 1 {
None
} else {
Some(line)
}
}
pub fn column_index(&self, offset: usize) -> Option<usize> {
self.line_index(offset)
.map(|line| offset - self.lines[line])
}
pub fn line_range(&self, offset: usize) -> Option<Range<usize>> {
self.line_index(offset)
.map(|line| self.lines[line]..self.lines[line + 1])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slice() {
assert_eq!([0].binary_search(&0), Ok(0));
assert_eq!([0, 1].binary_search(&0), Ok(0));
assert_eq!([0, 1].binary_search(&1), Ok(1));
assert_eq!([3].binary_search(&0), Err(0));
assert_eq!([3].binary_search(&1), Err(0));
assert_eq!([3].binary_search(&2), Err(0));
assert_eq!([3].binary_search(&3), Ok(0));
}
mod line_index {
use super::*;
#[test]
fn get_line_of_empty_source() {
let source = "";
let map = PositionMap::new(source);
assert_eq!(map.line_index(0), None);
assert_eq!(map.line_index(1), None);
}
#[test]
fn get_line_in_line() {
let source = "foo";
let map = PositionMap::new(source);
assert_eq!(map.line_index(0), Some(0));
assert_eq!(map.line_index(1), Some(0));
assert_eq!(map.line_index(2), Some(0));
assert_eq!(map.line_index(3), None);
}
#[test]
fn get_line_in_two_lines() {
let source = "foo\nbar\n";
let map = PositionMap::new(source);
assert_eq!(map.line_index(0), Some(0));
assert_eq!(map.line_index(1), Some(0));
assert_eq!(map.line_index(2), Some(0));
assert_eq!(map.line_index(3), Some(0));
assert_eq!(map.line_index(4), Some(1));
assert_eq!(map.line_index(5), Some(1));
assert_eq!(map.line_index(6), Some(1));
assert_eq!(map.line_index(7), Some(1));
assert_eq!(map.line_index(8), None);
}
}
mod line_range {
use super::*;
#[test]
fn get_in_line() {
let source = "foo";
let map = PositionMap::new(source);
assert_eq!(map.line_range(0), Some(0..3));
assert_eq!(map.line_range(1), Some(0..3));
assert_eq!(map.line_range(2), Some(0..3));
assert_eq!(map.line_range(3), None);
}
#[test]
fn get_in_line_with_newline() {
let source = "foo\n";
let map = PositionMap::new(source);
assert_eq!(map.line_range(0), Some(0..4));
assert_eq!(map.line_range(1), Some(0..4));
assert_eq!(map.line_range(2), Some(0..4));
assert_eq!(map.line_range(3), Some(0..4));
assert_eq!(map.line_range(4), None);
}
#[test]
fn get_in_lines() {
let source = "foo\nbar";
let map = PositionMap::new(source);
assert_eq!(map.line_range(0), Some(0..4));
assert_eq!(map.line_range(1), Some(0..4));
assert_eq!(map.line_range(2), Some(0..4));
assert_eq!(map.line_range(3), Some(0..4));
assert_eq!(map.line_range(4), Some(4..7));
assert_eq!(map.line_range(5), Some(4..7));
assert_eq!(map.line_range(6), Some(4..7));
assert_eq!(map.line_range(7), None);
}
}
}