#![cfg(feature = "tui")]
use mkutils::{Atom, Rope, TextSummary, Utils};
#[cfg(feature = "async")]
use std::io::Error as IoError;
#[cfg(feature = "async")]
use tokio_test::io::Builder as MockBuilder;
fn test_atoms<'a>(mut atoms: impl Iterator<Item = Atom<'a>>, expected_string: &str) {
let actual_string = atoms.collect_atoms();
assert_eq!(expected_string, actual_string);
}
fn test_rope(rope: &Rope, expected_string: &str) {
test_atoms(rope.atoms(), expected_string);
}
fn test_equality_impl(expected_string: &str) {
let rope = Rope::from(expected_string);
test_rope(&rope, expected_string);
}
fn collect_lines(rope: &Rope, lines: std::ops::Range<usize>, cols: std::ops::Range<usize>) -> Vec<String> {
rope.lines(lines, cols).into_vec()
}
fn test_collect_impl(strings: &[&str]) {
let rope = strings.iter().copied().collect::<Rope>();
let expected_string = strings.iter().copied().collect::<String>();
test_rope(&rope, &expected_string);
}
fn to_letter(i: u32) -> Option<char> {
char::from_u32(b'a'.convert::<u32>() + (i % 26))
}
#[test]
fn test_equality() {
test_equality_impl("hello world");
test_equality_impl("");
test_equality_impl("\n\n\n");
}
#[test]
fn test_multi_chunk() {
let string = (0u32..2048).filter_map(to_letter).collect::<String>();
test_equality_impl(&string);
}
#[test]
fn test_multiple_pushes() {
test_collect_impl(&["abc", "def"]);
}
#[test]
fn atoms_seek_line_zero() {
test_equality_impl("aaa\nbbb\nccc");
}
#[test]
fn atoms_seek_middle_line() {
let rope = Rope::from("aaa\nbbb\nccc");
let atoms = rope.atoms_at_line(1);
test_atoms(atoms, "bbb\nccc");
}
#[test]
fn atoms_seek_last_line() {
let rope = Rope::from("aaa\nbbb\nccc");
let atoms = rope.atoms_at_line(2);
test_atoms(atoms, "ccc");
}
#[test]
fn atoms_seek_past_end() {
let rope = Rope::from("aaa\nbbb");
let atoms = rope.atoms_at_line(99);
test_atoms(atoms, "");
}
#[test]
fn atoms_seek_line_at_chunk_boundary() {
let text = std::format!("{:-^1023}\nsecond line", "");
let rope = Rope::from(text.as_str());
let atoms = rope.atoms_at_line(1);
test_atoms(atoms, "second line");
}
#[test]
fn lines_horizontally_scrolled_text_is_visible() {
let rope = Rope::from("abcdef\nuvwxyz");
assert_eq!(collect_lines(&rope, 0..2, 2..5), ["cde", "wxy"]);
}
#[cfg(feature = "async")]
#[tokio::test(start_paused = true)]
async fn test_rope_builder() -> Result<(), IoError> {
let expected_str = "hello, world!";
let rope = MockBuilder::new()
.read(expected_str.as_bytes())
.build()
.to_rope_async()
.await?;
let actual_str = rope.atoms().collect_atoms();
assert_eq!(actual_str, expected_str);
().ok()
}
#[test]
fn atoms_offset_increases_monotonically() {
let rope = Rope::from("ab\ncd");
let offsets: Vec<TextSummary> = rope.atoms().map(|atom| atom.offset).collect();
for [curr_offset, next_offset] in offsets.array_windows() {
assert!(
curr_offset.length.extended_graphemes < next_offset.length.extended_graphemes,
"extended_graphemes offset should strictly increase: {curr_offset:?} vs {next_offset:?}",
);
}
}
#[test]
fn atoms_offset_tracks_newlines() {
let rope = Rope::from("a\nb");
let atoms: Vec<Atom<'_>> = rope.atoms().collect();
assert_eq!(atoms[0].extended_grapheme, "a");
assert_eq!(atoms[0].offset.length.newlines, 0);
assert_eq!(atoms[1].extended_grapheme, "\n");
assert_eq!(atoms[1].offset.length.newlines, 0);
assert_eq!(atoms[2].extended_grapheme, "b");
assert_eq!(atoms[2].offset.length.newlines, 1);
}
#[test]
fn atoms_offset_across_chunk_boundary() {
let mut input = "x".repeat(1020);
input.push_str("yz\nabc");
let rope = Rope::from(input.as_str());
let atoms: Vec<Atom<'_>> = rope.atoms().collect();
let total = atoms.len();
assert_eq!(atoms[total - 1].offset.length.extended_graphemes, total - 1);
}
#[test]
fn lines_all_lines() {
let rope = Rope::from("aaa\nbbb\nccc");
let lines = collect_lines(&rope, 0..3, 0..80);
assert_eq!(lines, vec!["aaa\n", "bbb\n", "ccc"]);
}
#[test]
fn lines_sub_range() {
let rope = Rope::from("aaa\nbbb\nccc");
let lines = collect_lines(&rope, 1..2, 0..80);
assert_eq!(lines, vec!["bbb\n"]);
}
#[test]
fn lines_empty_range() {
let rope = Rope::from("aaa\nbbb\nccc");
let lines = collect_lines(&rope, 0..0, 0..80);
assert!(lines.is_empty());
}
#[test]
fn lines_grapheme_sub_range() {
let rope = Rope::from("abcde\nfghij");
let lines = collect_lines(&rope, 0..2, 2..4);
assert_eq!(lines, vec!["cd", "hi"]);
}
#[test]
fn lines_grapheme_start_beyond_line_length() {
let rope = Rope::from("ab\ncd");
let lines = collect_lines(&rope, 0..2, 99..120);
assert_eq!(lines, vec!["", ""]);
}
#[test]
fn lines_grapheme_zero_width() {
let rope = Rope::from("abcde\nfghij");
let lines = collect_lines(&rope, 0..2, 0..0);
assert_eq!(lines, vec!["", ""]);
}
#[test]
fn lines_partial_consume_then_next_line() {
let rope = Rope::from("abcdefghij\nklmnopqrst\nuvwxyz");
let mut lines = rope.lines(0..3, 0..80);
{
let mut line = lines.next_line().unwrap();
let first = line.next().unwrap();
let second = line.next().unwrap();
assert_eq!(first.extended_grapheme, "a");
assert_eq!(second.extended_grapheme, "b");
}
let second_line: String = lines.next_line().unwrap().collect_atoms();
assert_eq!(second_line, "klmnopqrst\n");
}
#[test]
fn lines_drop_without_consuming() {
let rope = Rope::from("aaa\nbbb\nccc\nddd");
let mut lines = rope.lines(0..4, 0..80);
let _ = lines.next_line().unwrap();
let _ = lines.next_line().unwrap();
let _ = lines.next_line().unwrap();
let _ = lines.next_line().unwrap();
assert!(lines.next_line().is_none());
}
#[test]
fn lines_full_consume_does_not_skip() {
let rope = Rope::from("aaa\nbbb\nccc");
let mut lines = rope.lines(0..3, 0..80);
let first: String = lines.next_line().unwrap().collect_atoms();
assert_eq!(first, "aaa\n");
let second: String = lines.next_line().unwrap().collect_atoms();
assert_eq!(second, "bbb\n");
let third: String = lines.next_line().unwrap().collect_atoms();
assert_eq!(third, "ccc");
assert!(lines.next_line().is_none());
}
#[test]
fn lines_mixed_partial_and_full_consumption() {
let rope = Rope::from("111\n222\n333\n444");
let mut lines = rope.lines(0..4, 0..80);
let line_0: String = lines.next_line().unwrap().collect_atoms();
assert_eq!(line_0, "111\n");
let _ = lines.next_line().unwrap();
{
let mut line_2 = lines.next_line().unwrap();
let atom = line_2.next().unwrap();
assert_eq!(atom.extended_grapheme, "3");
}
let line_3: String = lines.next_line().unwrap().collect_atoms();
assert_eq!(line_3, "444");
assert!(lines.next_line().is_none());
}
#[test]
fn lines_line_spanning_chunks() {
let long_line: String = (0..1500).filter_map(to_letter).collect();
let rope = Rope::from(long_line.as_str());
let lines = collect_lines(&rope, 0..1, 0..2000);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0], long_line);
}
#[test]
fn lines_newline_at_chunk_boundary() {
let mut input = "x".repeat(1023);
input.push('\n');
input.push_str("second line");
let rope = Rope::from(input.as_str());
let lines = collect_lines(&rope, 0..2, 0..2000);
assert_eq!(lines.len(), 2);
let expected_first: String = "x".repeat(1023) + "\n";
assert_eq!(lines[0], expected_first);
assert_eq!(lines[1], "second line");
}
#[test]
fn unicode_multibyte_codepoints() {
let input = "héllo\nwörld";
let rope = Rope::from(input);
let lines = collect_lines(&rope, 0..2, 0..80);
assert_eq!(lines, vec!["héllo\n", "wörld"]);
}
#[test]
fn unicode_emoji() {
let input = "a😀b\nc😎d";
let rope = Rope::from(input);
let lines = collect_lines(&rope, 0..2, 0..80);
assert_eq!(lines, vec!["a😀b\n", "c😎d"]);
}
#[test]
fn unicode_crlf_is_single_newline() {
let input = "aaa\r\nbbb\r\nccc";
let rope = Rope::from(input);
let lines = collect_lines(&rope, 0..3, 0..80);
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], "aaa\r\n");
assert_eq!(lines[1], "bbb\r\n");
assert_eq!(lines[2], "ccc");
}
#[test]
fn unicode_flag_emoji() {
let input = "a🇺🇸b\nc🇬🇧d";
let rope = Rope::from(input);
let text: String = rope.atoms().collect_atoms();
assert_eq!(text, input);
}
#[test]
fn edge_no_newlines() {
let rope = Rope::from("hello");
let lines = collect_lines(&rope, 0..1, 0..80);
assert_eq!(lines, vec!["hello"]);
}
#[test]
fn edge_no_trailing_newline() {
let rope = Rope::from("aaa\nbbb");
let lines = collect_lines(&rope, 0..2, 0..80);
assert_eq!(lines, vec!["aaa\n", "bbb"]);
}
#[test]
fn edge_consecutive_newlines() {
let rope = Rope::from("a\n\n\nb");
let lines = collect_lines(&rope, 0..4, 0..80);
assert_eq!(lines, vec!["a\n", "\n", "\n", "b"]);
}
#[test]
fn edge_single_char() {
let rope = Rope::from("x");
let lines = collect_lines(&rope, 0..1, 0..80);
assert_eq!(lines, vec!["x"]);
}
#[test]
fn edge_single_newline() {
let rope = Rope::from("\n");
let lines = collect_lines(&rope, 0..1, 0..80);
assert_eq!(lines, vec!["\n"]);
}
#[test]
fn edge_empty_rope_atoms() {
let rope = Rope::empty();
let text: String = rope.atoms().collect_atoms();
assert_eq!(text, "");
}
#[test]
fn edge_empty_rope_lines() {
let rope = Rope::empty();
let lines = collect_lines(&rope, 0..1, 0..80);
assert!(lines.is_empty() || lines == vec![""]);
}