use crate::positions::SingleLineSpan;
use std::{
cmp::{max, Ordering},
fmt,
};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LineNumber(pub usize);
impl LineNumber {
pub fn one_indexed(&self) -> usize {
self.0 + 1
}
}
impl fmt::Debug for LineNumber {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!(
"LineNumber: {} (raw: {})",
self.one_indexed(),
self.0
))
}
}
impl From<usize> for LineNumber {
fn from(number: usize) -> Self {
Self(number)
}
}
pub fn format_line_num(line_num: LineNumber) -> String {
format!("{} ", line_num.one_indexed())
}
#[derive(Debug, PartialEq, Clone, Copy)]
struct LinePosition {
pub line: LineNumber,
column: usize,
}
#[derive(Debug)]
pub struct NewlinePositions {
positions: Vec<(usize, usize)>,
}
impl From<&str> for NewlinePositions {
fn from(s: &str) -> Self {
let mut line_start = 0;
let mut positions = vec![];
for line in s.split('\n') {
let line_end = line_start + line.len() + "\n".len();
positions.push((line_start, line_end - 1));
line_start = line_end;
}
NewlinePositions { positions }
}
}
impl NewlinePositions {
fn from_offset(&self, offset: usize) -> usize {
let idx = self.positions.binary_search_by(|(line_start, line_end)| {
if *line_end < offset {
return Ordering::Less;
}
if *line_start > offset {
return Ordering::Greater;
}
Ordering::Equal
});
idx.expect("line should be present")
}
pub fn from_offsets(&self, region_start: usize, region_end: usize) -> Vec<SingleLineSpan> {
assert!(region_start <= region_end);
let first_idx = self.from_offset(region_start);
let last_idx = self.from_offset(region_end);
let mut res = vec![];
for idx in first_idx..=last_idx {
let (line_start, line_end) = self.positions[idx];
res.push(SingleLineSpan {
line: idx.into(),
start_col: if line_start > region_start {
0
} else {
region_start - line_start
},
end_col: if region_end < line_end {
region_end - line_start
} else {
line_end - line_start
},
});
}
res
}
pub fn from_offsets_relative_to(
&self,
start: SingleLineSpan,
region_start: usize,
region_end: usize,
) -> Vec<SingleLineSpan> {
assert!(region_start <= region_end);
let mut res = vec![];
for pos in self.from_offsets(region_start, region_end) {
if pos.line.0 == 0 {
res.push(SingleLineSpan {
line: start.line,
start_col: start.start_col + pos.start_col,
end_col: start.start_col + pos.end_col,
});
} else {
res.push(SingleLineSpan {
line: (start.line.0 + pos.line.0).into(),
start_col: pos.start_col,
end_col: pos.end_col,
});
}
}
res
}
}
pub fn codepoint_len(s: &str) -> usize {
s.chars().count()
}
pub trait MaxLine {
fn max_line(&self) -> LineNumber;
}
impl<S: AsRef<str>> MaxLine for S {
fn max_line(&self) -> LineNumber {
(max(1, self.as_ref().split('\n').count()) - 1).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn from_offsets_first_line() {
let newline_positions: NewlinePositions = "foo".into();
let line_spans = newline_positions.from_offsets(1, 3);
assert_eq!(
line_spans,
vec![SingleLineSpan {
line: 0.into(),
start_col: 1,
end_col: 3
}]
);
}
#[test]
fn from_offsets_first_char() {
let newline_positions: NewlinePositions = "foo".into();
let line_spans = newline_positions.from_offsets(0, 0);
assert_eq!(
line_spans,
vec![SingleLineSpan {
line: 0.into(),
start_col: 0,
end_col: 0
}]
);
}
#[test]
fn from_offsets_split_over_multiple_lines() {
let newline_positions: NewlinePositions = "foo\nbar\nbaz\naaaaaaaaaaa".into();
let line_spans = newline_positions.from_offsets(5, 10);
assert_eq!(
line_spans,
vec![
SingleLineSpan {
line: 1.into(),
start_col: 1,
end_col: 3
},
SingleLineSpan {
line: 2.into(),
start_col: 0,
end_col: 2
}
]
);
}
#[test]
fn str_max_line() {
let line: String = "foo\nbar".into();
assert_eq!(line.max_line().0, 1);
}
#[test]
fn empty_str_max_line() {
let line: String = "".into();
assert_eq!(line.max_line().0, 0);
}
#[test]
fn str_max_line_trailing_newline() {
let line: String = "foo\nbar\n".into();
assert_eq!(line.max_line().0, 2);
}
#[test]
fn from_offsets_relative_to() {
let newline_positions: NewlinePositions = "foo\nbar".into();
let pos = SingleLineSpan {
line: 1.into(),
start_col: 1,
end_col: 1,
};
let line_spans = newline_positions.from_offsets_relative_to(pos, 1, 2);
assert_eq!(
line_spans,
vec![SingleLineSpan {
line: 1.into(),
start_col: 2,
end_col: 3
}]
);
}
#[test]
fn codepoint_len_non_ascii() {
assert_eq!(codepoint_len("ƒoo"), 3);
}
}