use std::ops::Range;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Position {
pub line: usize,
pub column: usize,
pub offset: usize,
}
impl Position {
#[must_use]
pub const fn start() -> Position {
Position {
line: 0,
column: 0,
offset: 0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Location<'a> {
pub file: Option<&'a str>,
pub start: Position,
pub end: Position,
pub text: &'a str,
}
impl Location<'_> {
#[must_use]
pub const fn span(&self) -> Range<usize> {
self.start.offset..self.end.offset
}
#[must_use]
pub const fn len(&self) -> usize {
self.end.offset.saturating_sub(self.start.offset)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug)]
pub struct Lines<'a> {
source: &'a str,
starts: Vec<usize>,
}
impl<'a> Lines<'a> {
#[must_use]
pub fn new(source: &'a str) -> Lines<'a> {
let mut starts = vec![0];
starts.extend(
source
.bytes()
.enumerate()
.filter(|&(_, byte)| byte == b'\n')
.map(|(index, _)| index + 1),
);
Lines { source, starts }
}
#[must_use]
pub const fn source(&self) -> &'a str {
self.source
}
#[must_use]
pub fn position(&self, offset: usize) -> Position {
let line = self.starts.partition_point(|&start| start <= offset);
let index = line.saturating_sub(1);
let start = self.starts.get(index).copied().unwrap_or(0);
Position {
line: index,
column: offset.saturating_sub(start),
offset,
}
}
#[must_use]
pub fn locate(&self, range: Range<usize>, file: Option<&'a str>) -> Location<'a> {
let start = range.start.min(self.source.len());
let end = range.end.clamp(start, self.source.len());
let text = self.source.get(start..end).unwrap_or("");
Location {
file,
start: self.position(start),
end: self.position(end),
text,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn positions_are_zero_based() {
let lines = Lines::new("ab\ncd\n");
assert_eq!(
lines.position(0),
Position {
line: 0,
column: 0,
offset: 0
}
);
assert_eq!(
lines.position(1),
Position {
line: 0,
column: 1,
offset: 1
}
);
assert_eq!(
lines.position(3),
Position {
line: 1,
column: 0,
offset: 3
}
);
assert_eq!(
lines.position(5),
Position {
line: 1,
column: 2,
offset: 5
}
);
}
#[test]
fn a_trailing_newline_opens_a_line() {
let lines = Lines::new("ab\n");
assert_eq!(lines.position(3).line, 1);
}
#[test]
fn locate_borrows_the_span() {
let lines = Lines::new("# heading\n");
let location = lines.locate(2..9, Some("foo.md"));
assert_eq!(location.text, "heading");
assert_eq!(location.file, Some("foo.md"));
assert_eq!(location.span(), 2..9);
assert_eq!(location.len(), 7);
assert!(!location.is_empty());
}
#[test]
fn out_of_range_spans_are_clamped_rather_than_panicking() {
let lines = Lines::new("abc");
assert_eq!(lines.locate(1..99, None).text, "bc");
let reversed = std::ops::Range { start: 9, end: 1 };
assert_eq!(lines.locate(reversed, None).text, "");
assert_eq!(lines.position(99).line, 0);
}
#[test]
fn multibyte_columns_count_bytes() {
let lines = Lines::new("\u{e9}x\n");
assert_eq!(
lines.position(2),
Position {
line: 0,
column: 2,
offset: 2
}
);
}
}