use super::{AsciiFile, PositionIterator, ReversePositionIterator, Span};
use std::cmp::Ordering;
#[derive(Copy, Clone)]
pub struct Position<'t> {
byte_offset: usize,
file: &'t AsciiFile<'t>,
}
impl PartialOrd for Position<'_> {
fn partial_cmp(&self, other: &Position<'_>) -> Option<Ordering> {
if !std::ptr::eq(self.file, other.file) {
return None;
}
Some(self.byte_offset.cmp(&other.byte_offset))
}
}
impl Ord for Position<'_> {
fn cmp(&self, other: &Position<'_>) -> Ordering {
if !std::ptr::eq(self.file, other.file) {
panic!(
"comparing positions of different files: {:?} {:?}",
self.file as *const _, other.file as *const _
)
}
self.byte_offset.cmp(&other.byte_offset)
}
}
impl<'t> Position<'t> {
pub fn at_file_start(file: &'t AsciiFile<'t>) -> Option<Self> {
match file.mapping.get(0).map(|byte| *byte as char) {
None => {
None
}
Some(_) => Some(Self {
byte_offset: 0,
file,
}),
}
}
pub fn to_single_char_span(self) -> Span<'t> {
Span::from_single_position(self)
}
pub fn byte_offset(&self) -> usize {
self.byte_offset
}
pub fn file(&self) -> &AsciiFile<'t> {
self.file
}
pub fn chr(&self) -> char {
self.file.mapping[self.byte_offset] as char
}
pub fn byte(&self) -> u8 {
self.file.mapping[self.byte_offset]
}
pub fn row(&self) -> usize {
self.file.lookup_cache().row(self.byte_offset)
}
pub fn line_number(&self) -> usize {
self.file.lookup_cache().line_number(self.byte_offset)
}
pub fn column(&self) -> usize {
self.file.lookup_cache().column(self.byte_offset)
}
pub fn row_and_column(&self) -> (usize, usize) {
self.file.lookup_cache().row_and_column(self.byte_offset)
}
pub fn next(&self) -> Option<Position<'t>> {
self.next_mut().ok()
}
pub fn next_mut(mut self) -> Result<Self, Self> {
let next_byte = self.file.mapping.get(self.byte_offset + 1);
match next_byte {
None => Err(self),
Some(_) => {
self.byte_offset += 1;
Ok(self)
}
}
}
pub fn is_last(&self) -> bool {
self.next().is_none()
}
pub fn prev(&self) -> Option<Position<'t>> {
self.prev_mut().ok()
}
pub fn prev_mut(mut self) -> Result<Self, Self> {
if self.byte_offset == 0 {
return Err(self);
}
self.byte_offset -= 1;
Ok(self)
}
pub fn line(&self) -> Span<'t> {
let start = self
.reverse_iter()
.find(|position| match position.prev() {
Some(next) if next.chr() == '\n' => true,
None => true,
_ => false,
})
.unwrap();
let end = self
.iter()
.find(|position| position.chr() == '\n')
.unwrap_or_else(|| self.iter().last().unwrap());
Span::new(start, end)
}
pub fn iter(&self) -> PositionIterator<'t> {
PositionIterator::new(Some(*self))
}
pub fn reverse_iter(&self) -> ReversePositionIterator<'t> {
ReversePositionIterator::new(Some(*self))
}
}
use std::fmt::{self, Debug, Display};
impl Display for Position<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{}:{}", self.line_number(), self.column())
}
}
impl Debug for Position<'_> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
fmt,
"Position{{row: {:?}, col: {:?}, byte_offset: {:?}, file: {:?}}}",
self.row(),
self.column(),
self.byte_offset,
self.file as *const _
)
}
}
impl PartialEq for Position<'_> {
fn eq(&self, rhs: &Position<'_>) -> bool {
self.byte_offset == rhs.byte_offset && self.file as *const _ == (rhs.file as *const _)
}
}
impl Eq for Position<'_> {}
#[cfg(test)]
#[allow(clippy::print_stdout)]
mod tests {
use super::*;
fn positions_to_tuples<'f>(
positions: Vec<Position<'f>>,
) -> Vec<((usize, char), (usize, usize))> {
positions
.into_iter()
.map(|position| {
(
(position.byte_offset(), position.chr()),
position.row_and_column(),
)
})
.collect()
}
#[test]
fn peeking_works() {
let input = b"one\ntwo three\nfour\n\n";
let file = AsciiFile::new(input).unwrap();
let mut i = file.iter();
while i.next().is_some() {
let peeked = i.peek();
let consumed = i.next();
assert_eq!(peeked, consumed);
}
assert_eq!(i.peek(), None);
}
#[test]
fn peeking_multiple_times_works() {
let input = b"abc";
let file = AsciiFile::new(input).unwrap();
let mut iter = file.iter();
let peeked_once = iter.peek();
let peeked_twice = iter.peek();
assert_eq!(peeked_once, peeked_twice);
let consumed = iter.next();
assert_eq!(peeked_once, consumed);
}
#[test]
fn peeking_multiple_chars_at_once_works() {
let input = "one\ntwo three\nfour\n\n";
let file = AsciiFile::new(input.as_bytes()).unwrap();
let iter = file.iter();
let peeked_once = iter.peek_exactly(input.len()).unwrap();
assert_eq!(input, peeked_once.as_str());
let peeked_twice = iter.peek_exactly(2).unwrap();
assert_eq!(&input[..2], peeked_twice.as_str());
}
#[test]
fn peeking_multiple_chars_with_at_most_works() {
let input = "one\ntwo three\nfour\n\n";
let file = AsciiFile::new(input.as_bytes()).unwrap();
let iter = file.iter();
let peeked_once = iter.peek_at_most(input.len() + 1).unwrap();
assert_eq!(input, peeked_once.as_str());
let peeked_twice = iter.peek_at_most(2).unwrap();
assert_eq!(&input[..2], peeked_twice.as_str());
}
#[test]
fn iterator_works() {
let input = b"one\ntwo three\nfour\n\n";
let file = AsciiFile::new(input).unwrap();
let actual = positions_to_tuples(file.iter().collect());
#[rustfmt::skip]
let expected = vec![
(( 0, 'o'), (0, 0), ),
(( 1, 'n'), (0, 1), ),
(( 2, 'e'), (0, 2), ),
(( 3, '\n'), (0, 3),),
(( 4, 't'), (1, 0), ),
(( 5, 'w'), (1, 1), ),
(( 6, 'o'), (1, 2), ),
(( 7, ' '), (1, 3), ),
(( 8, 't'), (1, 4), ),
(( 9, 'h'), (1, 5), ),
((10, 'r'), (1, 6), ),
((11, 'e'), (1, 7), ),
((12, 'e'), (1, 8), ),
((13, '\n'), (1, 9),),
((14, 'f'), (2, 0), ),
((15, 'o'), (2, 1), ),
((16, 'u'), (2, 2), ),
((17, 'r'), (2, 3), ),
((18, '\n'), (2, 4),),
((19, '\n'), (3, 0),),
];
assert_eq!(expected, actual);
}
#[test]
fn file_starting_with_newline() {
let input = b"\none\ntwo three\nfour\n\n";
let file = AsciiFile::new(input).unwrap();
let start = Position::at_file_start(&file).unwrap();
let end = file.iter().last().unwrap();
let span = Span::new(start, end);
let actual = span
.lines()
.enumerate()
.map(|(real_row, line)| {
(
real_row + 1,
line.start_position().line_number(),
line.as_str().to_string(),
)
})
.collect::<Vec<_>>();
let expected: Vec<(usize, usize, String)> = vec![
(1, 1, "\n".to_string()),
(2, 2, "one\n".to_string()),
(3, 3, "two three\n".to_string()),
(4, 4, "four\n".to_string()),
(5, 5, "\n".to_string()),
];
assert_eq!(expected, actual);
}
#[test]
fn file_starting_with_two_newlines() {
let input = b"\n\none\ntwo three\nfour\n\n";
let file = AsciiFile::new(input).unwrap();
let start = Position::at_file_start(&file).unwrap();
let end = file.iter().last().unwrap();
let span = Span::new(start, end);
let actual = span
.lines()
.enumerate()
.map(|(real_row, line)| {
(
real_row + 1,
line.start_position().line_number(),
line.as_str().to_string(),
)
})
.collect::<Vec<_>>();
let expected: Vec<(usize, usize, String)> = vec![
(1, 1, "\n".to_string()),
(2, 2, "\n".to_string()),
(3, 3, "one\n".to_string()),
(4, 4, "two three\n".to_string()),
(5, 5, "four\n".to_string()),
(6, 6, "\n".to_string()),
];
assert_eq!(expected, actual);
}
#[test]
fn all_indexing_works() {
file_index_roundtrip(b"one\ntwo three\nfour\n\n");
file_index_roundtrip(b"a");
file_index_roundtrip(b"\n");
file_index_roundtrip(b"\n\n\n\n");
file_index_roundtrip(b"aksjdaklsd");
file_index_roundtrip(b"aksjd\naklsd");
file_index_roundtrip(b"aksjdaklsd\n");
file_index_roundtrip(b"");
}
fn file_index_roundtrip(input: &[u8]) {
println!("INPUT=```{}```", String::from_utf8_lossy(input));
let file = AsciiFile::new(input).unwrap();
let front_to_back = {
println!("FRONT TO BACK");
let mut vec = Vec::new();
let mut pos = Position::at_file_start(&file);
while let Some(next) = pos {
println!("{:?}: '{}'", next, next.chr());
vec.push(next);
pos = next.next();
}
vec
};
let back_to_front = {
println!("BACK TO FRONT");
let mut vec = Vec::new();
let mut pos = file.iter().last();
while let Some(prev) = pos {
println!("{:?}: '{}'", prev, prev.chr());
vec.push(prev);
pos = prev.prev();
}
vec.reverse();
vec
};
assert_eq!(back_to_front.len(), input.len());
assert_eq!(
positions_to_tuples(back_to_front),
positions_to_tuples(front_to_back)
);
}
macro_rules! same_position_different_files {
($input:expr, $f1pos:ident, $f2pos:ident) => {
let input1 = $input.as_ref().clone();
let file1 = AsciiFile::new(input1).unwrap();
let input2 = $input.as_ref().clone();
let file2 = AsciiFile::new(input2).unwrap();
let $f1pos: Position<'_> = file1.iter().next().unwrap();
let $f2pos: Position<'_> = file2.iter().next().unwrap();
};
}
#[test]
fn position_partial_cmp_from_different_files_no_ordering() {
same_position_different_files!(b"samestring", p1, p2);
let res = p1.partial_cmp(&p2);
assert_eq!(res, None);
}
#[test]
#[should_panic]
fn position_cmp_of_different_files_panics() {
same_position_different_files!(b"samestring", p1, p2);
p1.cmp(&p2);
}
}