//! Test helpers. use crate::{Range, Selection}; use ropey::Rope; use smallvec::SmallVec; use std::cmp::Reverse; use unicode_segmentation::UnicodeSegmentation; /// Convert annotated test string to test string and selection. /// /// `#[|` for primary selection with head before anchor followed by `]#`. /// `#(|` for secondary selection with head before anchor followed by `)#`. /// `#[` for primary selection with head after anchor followed by `|]#`. /// `#(` for secondary selection with head after anchor followed by `|)#`. /// /// If the selection contains any LF or CRLF sequences, which are immediately /// followed by the same grapheme, then the subsequent one is removed. This is /// to allow representing having the cursor over the end of the line. /// /// # Examples /// /// ``` /// use helix_core::{Range, Selection, test::print}; /// use smallvec::smallvec; /// /// assert_eq!( /// print("#[a|]#b#(|c)#"), /// ("abc".to_owned(), Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)) /// ); /// ``` /// /// # Panics /// /// Panics when missing primary or appeared more than once. /// Panics when missing head or anchor. /// Panics when head come after head or anchor come after anchor. pub fn print(s: &str) -> (String, Selection) { let mut primary_idx = None; let mut ranges = SmallVec::new(); let mut iter = UnicodeSegmentation::graphemes(s, true).peekable(); let mut left = String::with_capacity(s.len()); 'outer: while let Some(c) = iter.next() { let start = left.chars().count(); if c != "#" { left.push_str(c); continue; } let (is_primary, close_pair) = match iter.next() { Some("[") => (true, "]"), Some("(") => (false, ")"), Some(ch) => { left.push('#'); left.push_str(ch); continue; } None => break, }; if is_primary && primary_idx.is_some() { panic!("primary `#[` already appeared {:?} {:?}", left, s); } let head_at_beg = iter.next_if_eq(&"|").is_some(); let last_grapheme = |s: &str| { UnicodeSegmentation::graphemes(s, true) .last() .map(String::from) }; while let Some(c) = iter.next() { let next = iter.peek(); let mut prev = last_grapheme(left.as_str()); if !(c == close_pair && next == Some(&"#")) { left.push_str(c); continue; } if !head_at_beg { match &prev { Some(p) if p != "|" => { left.push_str(c); continue; } Some(p) if p == "|" => { left.pop().unwrap(); // pop the | prev = last_grapheme(left.as_str()); } _ => (), } } iter.next(); // skip "#" let next = iter.peek(); // skip explicit line end inside selection if (prev == Some(String::from("\r\n")) || prev == Some(String::from("\n"))) && next.map(|n| String::from(*n)) == prev { iter.next(); } if is_primary { primary_idx = Some(ranges.len()); } let (anchor, head) = match head_at_beg { true => (left.chars().count(), start), false => (start, left.chars().count()), }; ranges.push(Range::new(anchor, head)); continue 'outer; } if head_at_beg { panic!("missing end `{}#` {:?} {:?}", close_pair, left, s); } else { panic!("missing end `|{}#` {:?} {:?}", close_pair, left, s); } } let primary = match primary_idx { Some(i) => i, None => panic!("missing primary `#[|]#` {:?}", s), }; let selection = Selection::new(ranges, primary); (left, selection) } /// Convert test string and selection to annotated test string. /// /// `#[|` for primary selection with head before anchor followed by `]#`. /// `#(|` for secondary selection with head before anchor followed by `)#`. /// `#[` for primary selection with head after anchor followed by `|]#`. /// `#(` for secondary selection with head after anchor followed by `|)#`. /// /// # Examples /// /// ``` /// use helix_core::{Range, Selection, test::plain}; /// use smallvec::smallvec; /// /// assert_eq!( /// plain("abc", &Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)), /// "#[a|]#b#(|c)#".to_owned() /// ); /// ``` pub fn plain>(s: R, selection: &Selection) -> String { let s = s.into(); let primary = selection.primary_index(); let mut out = String::with_capacity(s.len_bytes() + 5 * selection.len()); out.push_str(&s.to_string()); let mut insertion: Vec<_> = selection .iter() .enumerate() .flat_map(|(i, range)| { // sort like this before reversed so anchor < head later match (range.anchor < range.head, i == primary) { (true, true) => [(range.anchor, "#["), (range.head, "|]#")], (true, false) => [(range.anchor, "#("), (range.head, "|)#")], (false, true) => [(range.anchor, "]#"), (range.head, "#[|")], (false, false) => [(range.anchor, ")#"), (range.head, "#(|")], } }) .map(|(char_idx, marker)| (s.char_to_byte(char_idx), marker)) .collect(); // insert in reverse order insertion.sort_unstable_by_key(|k| Reverse(k.0)); for (i, s) in insertion { out.insert_str(i, s); } out } #[cfg(test)] #[allow(clippy::module_inception)] mod test { use super::*; #[test] fn print_single() { assert_eq!( (String::from("hello"), Selection::single(1, 0)), print("#[|h]#ello") ); assert_eq!( (String::from("hello"), Selection::single(0, 1)), print("#[h|]#ello") ); assert_eq!( (String::from("hello"), Selection::single(4, 0)), print("#[|hell]#o") ); assert_eq!( (String::from("hello"), Selection::single(0, 4)), print("#[hell|]#o") ); assert_eq!( (String::from("hello"), Selection::single(5, 0)), print("#[|hello]#") ); assert_eq!( (String::from("hello"), Selection::single(0, 5)), print("#[hello|]#") ); } #[test] fn print_multi() { assert_eq!( ( String::from("hello"), Selection::new( SmallVec::from_slice(&[Range::new(1, 0), Range::new(5, 4)]), 0 ) ), print("#[|h]#ell#(|o)#") ); assert_eq!( ( String::from("hello"), Selection::new( SmallVec::from_slice(&[Range::new(0, 1), Range::new(4, 5)]), 0 ) ), print("#[h|]#ell#(o|)#") ); assert_eq!( ( String::from("hello"), Selection::new( SmallVec::from_slice(&[Range::new(2, 0), Range::new(5, 3)]), 0 ) ), print("#[|he]#l#(|lo)#") ); assert_eq!( ( String::from("hello\r\nhello\r\nhello\r\n"), Selection::new( SmallVec::from_slice(&[ Range::new(7, 5), Range::new(21, 19), Range::new(14, 12) ]), 0 ) ), print("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#") ); } #[test] fn print_multi_byte_code_point() { assert_eq!( (String::from("„“"), Selection::single(1, 0)), print("#[|„]#“") ); assert_eq!( (String::from("„“"), Selection::single(2, 1)), print("„#[|“]#") ); assert_eq!( (String::from("„“"), Selection::single(0, 1)), print("#[„|]#“") ); assert_eq!( (String::from("„“"), Selection::single(1, 2)), print("„#[“|]#") ); assert_eq!( (String::from("they said „hello“"), Selection::single(11, 10)), print("they said #[|„]#hello“") ); } #[test] fn print_multi_code_point_grapheme() { assert_eq!( ( String::from("hello 👨‍👩‍👧‍👦 goodbye"), Selection::single(13, 6) ), print("hello #[|👨‍👩‍👧‍👦]# goodbye") ); } #[test] fn plain_single() { assert_eq!("#[|h]#ello", plain("hello", &Selection::single(1, 0))); assert_eq!("#[h|]#ello", plain("hello", &Selection::single(0, 1))); assert_eq!("#[|hell]#o", plain("hello", &Selection::single(4, 0))); assert_eq!("#[hell|]#o", plain("hello", &Selection::single(0, 4))); assert_eq!("#[|hello]#", plain("hello", &Selection::single(5, 0))); assert_eq!("#[hello|]#", plain("hello", &Selection::single(0, 5))); } #[test] fn plain_multi() { assert_eq!( plain( "hello", &Selection::new( SmallVec::from_slice(&[Range::new(1, 0), Range::new(5, 4)]), 0 ) ), String::from("#[|h]#ell#(|o)#") ); assert_eq!( plain( "hello", &Selection::new( SmallVec::from_slice(&[Range::new(0, 1), Range::new(4, 5)]), 0 ) ), String::from("#[h|]#ell#(o|)#") ); assert_eq!( plain( "hello", &Selection::new( SmallVec::from_slice(&[Range::new(2, 0), Range::new(5, 3)]), 0 ) ), String::from("#[|he]#l#(|lo)#") ); assert_eq!( plain( "hello\r\nhello\r\nhello\r\n", &Selection::new( SmallVec::from_slice(&[ Range::new(7, 5), Range::new(21, 19), Range::new(14, 12) ]), 0 ) ), String::from("hello#[|\r\n]#hello#(|\r\n)#hello#(|\r\n)#") ); } #[test] fn plain_multi_byte_code_point() { assert_eq!( plain("„“", &Selection::single(1, 0)), String::from("#[|„]#“") ); assert_eq!( plain("„“", &Selection::single(2, 1)), String::from("„#[|“]#") ); assert_eq!( plain("„“", &Selection::single(0, 1)), String::from("#[„|]#“") ); assert_eq!( plain("„“", &Selection::single(1, 2)), String::from("„#[“|]#") ); assert_eq!( plain("they said „hello“", &Selection::single(11, 10)), String::from("they said #[|„]#hello“") ); } #[test] fn plain_multi_code_point_grapheme() { assert_eq!( plain("hello 👨‍👩‍👧‍👦 goodbye", &Selection::single(13, 6)), String::from("hello #[|👨‍👩‍👧‍👦]# goodbye") ); } }