use alloc::string::String;
use crate::{ColumnEncoding, LineIndex, Position, PositionError};
const ENCODINGS: [ColumnEncoding; 3] = [
ColumnEncoding::Utf8,
ColumnEncoding::Scalar,
ColumnEncoding::Utf16,
];
fn position(line: usize, column: usize) -> Position {
Position { line, column }
}
#[test]
fn empty_source_has_one_representable_position() {
let index = LineIndex::new("");
assert_eq!(index.line_count(), 1);
for encoding in ENCODINGS {
assert_eq!(index.to_position(0, encoding), Ok(position(0, 0)));
assert_eq!(index.to_offset(position(0, 0), encoding), Ok(0));
assert_eq!(
index.to_position(1, encoding),
Err(PositionError::OutOfBounds)
);
assert_eq!(
index.to_offset(position(0, 1), encoding),
Err(PositionError::OutOfBounds)
);
assert_eq!(
index.to_offset(position(1, 0), encoding),
Err(PositionError::OutOfBounds)
);
}
}
#[test]
fn mixed_line_endings_preserve_exact_boundaries() {
let source = String::from("ab\r\nc\rd\nef");
let index = LineIndex::new(&source);
assert_eq!(index.line_count(), 4);
let boundaries = [
(0, position(0, 0)),
(1, position(0, 1)),
(2, position(0, 2)), (4, position(1, 0)),
(5, position(1, 1)), (6, position(2, 0)),
(7, position(2, 1)), (8, position(3, 0)),
(9, position(3, 1)),
(10, position(3, 2)), ];
for encoding in ENCODINGS {
for (offset, expected) in boundaries {
assert_eq!(index.to_position(offset, encoding), Ok(expected));
assert_eq!(index.to_offset(expected, encoding), Ok(offset));
}
assert_eq!(
index.to_position(3, encoding),
Err(PositionError::InsideCrLf)
);
assert_eq!(
index.to_offset(position(0, 3), encoding),
Err(PositionError::OutOfBounds)
);
assert_eq!(
index.to_offset(position(1, 2), encoding),
Err(PositionError::OutOfBounds)
);
}
}
#[test]
fn each_terminal_ending_creates_an_empty_line() {
for source in ["x\n", "x\r", "x\r\n"] {
let index = LineIndex::new(source);
assert_eq!(index.line_count(), 2);
for encoding in ENCODINGS {
assert_eq!(index.to_position(1, encoding), Ok(position(0, 1)));
assert_eq!(index.to_offset(position(0, 1), encoding), Ok(1));
assert_eq!(
index.to_position(source.len(), encoding),
Ok(position(1, 0))
);
assert_eq!(index.to_offset(position(1, 0), encoding), Ok(source.len()));
assert_eq!(
index.to_offset(position(1, 1), encoding),
Err(PositionError::OutOfBounds)
);
}
}
}
#[test]
fn adjacent_endings_include_empty_physical_lines() {
let index = LineIndex::new("\r\n\r\n\n\r");
assert_eq!(index.line_count(), 5);
for encoding in ENCODINGS {
for (line, offset) in [0, 2, 4, 5, 6].into_iter().enumerate() {
assert_eq!(index.to_position(offset, encoding), Ok(position(line, 0)));
assert_eq!(index.to_offset(position(line, 0), encoding), Ok(offset));
assert_eq!(
index.to_offset(position(line, 1), encoding),
Err(PositionError::OutOfBounds)
);
}
for offset in [1, 3] {
assert_eq!(
index.to_position(offset, encoding),
Err(PositionError::InsideCrLf)
);
}
}
}
#[test]
fn utf8_columns_preserve_interior_byte_offsets() {
let source = "é😀\r\n界";
let index = LineIndex::new(source);
for offset in 0..=source.len() {
if offset == 7 {
assert_eq!(
index.to_position(offset, ColumnEncoding::Utf8),
Err(PositionError::InsideCrLf)
);
} else {
let expected = if offset <= 6 {
position(0, offset)
} else {
position(1, offset - 8)
};
assert_eq!(
index.to_position(offset, ColumnEncoding::Utf8),
Ok(expected)
);
assert_eq!(index.to_offset(expected, ColumnEncoding::Utf8), Ok(offset));
}
}
}
#[test]
fn unicode_columns_are_exact_and_not_display_cell_widths() {
let index = LineIndex::new("\té😀\u{301}界");
let boundaries = [
(0, 0, 0),
(1, 1, 1),
(3, 2, 2),
(7, 3, 4),
(9, 4, 5),
(12, 5, 6),
];
for (offset, scalar, utf16) in boundaries {
assert_eq!(
index.to_position(offset, ColumnEncoding::Scalar),
Ok(position(0, scalar))
);
assert_eq!(
index.to_offset(position(0, scalar), ColumnEncoding::Scalar),
Ok(offset)
);
assert_eq!(
index.to_position(offset, ColumnEncoding::Utf16),
Ok(position(0, utf16))
);
assert_eq!(
index.to_offset(position(0, utf16), ColumnEncoding::Utf16),
Ok(offset)
);
}
for offset in [2, 4, 5, 6, 8, 10, 11] {
for encoding in [ColumnEncoding::Scalar, ColumnEncoding::Utf16] {
assert_eq!(
index.to_position(offset, encoding),
Err(PositionError::InvalidUtf8Boundary)
);
}
}
assert_eq!(
index.to_offset(position(0, 6), ColumnEncoding::Scalar),
Err(PositionError::OutOfBounds)
);
assert_eq!(
index.to_offset(position(0, 7), ColumnEncoding::Utf16),
Err(PositionError::OutOfBounds)
);
}
#[test]
fn utf16_rejects_each_split_surrogate_without_clamping() {
let index = LineIndex::new("😀𐐀z\n😀");
for pos in [position(0, 1), position(0, 3), position(1, 1)] {
assert_eq!(
index.to_offset(pos, ColumnEncoding::Utf16),
Err(PositionError::SplitSurrogate)
);
}
for (pos, offset) in [
(position(0, 0), 0),
(position(0, 2), 4),
(position(0, 4), 8),
(position(0, 5), 9),
(position(1, 0), 10),
(position(1, 2), 14),
] {
assert_eq!(index.to_offset(pos, ColumnEncoding::Utf16), Ok(offset));
assert_eq!(index.to_position(offset, ColumnEncoding::Utf16), Ok(pos));
}
assert_eq!(
index.to_offset(position(1, 3), ColumnEncoding::Utf16),
Err(PositionError::OutOfBounds)
);
}
#[test]
fn non_ascii_line_starts_and_bom_use_source_columns() {
let index = LineIndex::new("\u{feff}é\r\n😀\r界\n");
assert_eq!(index.line_count(), 4);
for encoding in ENCODINGS {
for (offset, line) in [(0, 0), (7, 1), (12, 2), (16, 3)] {
assert_eq!(index.to_position(offset, encoding), Ok(position(line, 0)));
assert_eq!(index.to_offset(position(line, 0), encoding), Ok(offset));
}
assert_eq!(
index.to_position(6, encoding),
Err(PositionError::InsideCrLf)
);
}
assert_eq!(
index.to_position(3, ColumnEncoding::Utf8),
Ok(position(0, 3))
);
assert_eq!(
index.to_position(3, ColumnEncoding::Scalar),
Ok(position(0, 1))
);
assert_eq!(
index.to_position(3, ColumnEncoding::Utf16),
Ok(position(0, 1))
);
}
#[test]
fn conversions_reject_out_of_range_coordinates_without_overflow() {
let index = LineIndex::new("é\n😀");
for encoding in ENCODINGS {
for offset in [8, usize::MAX] {
assert_eq!(
index.to_position(offset, encoding),
Err(PositionError::OutOfBounds)
);
}
for pos in [
position(2, 0),
position(usize::MAX, 0),
position(0, usize::MAX),
position(1, usize::MAX),
] {
assert_eq!(
index.to_offset(pos, encoding),
Err(PositionError::OutOfBounds)
);
}
}
}
#[test]
fn display_coordinates_require_explicit_checked_conversion() {
assert_eq!(position(0, 0).one_based(), Ok(position(1, 1)));
assert_eq!(position(3, 8).one_based(), Ok(position(4, 9)));
assert_eq!(
position(usize::MAX - 1, usize::MAX - 1).one_based(),
Ok(position(usize::MAX, usize::MAX))
);
for pos in [
position(usize::MAX, 0),
position(0, usize::MAX),
position(usize::MAX, usize::MAX),
] {
assert_eq!(pos.one_based(), Err(PositionError::OutOfBounds));
}
let index = LineIndex::new("a\nb");
let raw = index.to_position(2, ColumnEncoding::Scalar).unwrap();
assert_eq!(raw, position(1, 0));
assert_eq!(raw.one_based(), Ok(position(2, 1)));
assert_eq!(index.to_offset(raw, ColumnEncoding::Scalar), Ok(2));
}