@@ -2,6 +2,7 @@
22use crate::{Range, Selection};
33use smallvec::SmallVec;
44use std::cmp::Reverse;
5+use unicode_segmentation::UnicodeSegmentation;
56
67/// Convert annotated test string to test string and selection.
78///
@@ -10,6 +11,10 @@ use std::cmp::Reverse;
1011/// `#[` for primary selection with head after anchor followed by `|]#`.
1112/// `#(` for secondary selection with head after anchor followed by `|)#`.
1213///
14+/// If the selection contains any LF or CRLF sequences, which are immediately
15+/// followed by the same grapheme, then the subsequent one is removed. This is
16+/// to allow representing having the cursor over the end of the line.
17+///
1318/// # Examples
1419///
1520/// ```
@@ -30,23 +35,23 @@ use std::cmp::Reverse;
3035pub fn print(s: &str) -> (String, Selection) {
3136 let mut primary_idx = None;
3237 let mut ranges = SmallVec::new();
33− let mut iter = s.chars().peekable();
38+ let mut iter = UnicodeSegmentation::graphemes(s, true).peekable();
3439 let mut left = String::with_capacity(s.len());
3540
3641 'outer: while let Some(c) = iter.next() {
3742 let start = left.chars().count();
3843
39− if c != '#' {
40− left.push(c);
44+ if c != "#" {
45+ left.push_str(c);
4146 continue;
4247 }
4348
4449 let (is_primary, close_pair) = match iter.next() {
45− Some('[') => (true, ']'),
46− Some('(') => (false, ')'),
50+ Some("[") => (true, "]"),
51+ Some("(") => (false, ")"),
4752 Some(ch) => {
4853 left.push('#');
49− left.push(ch);
54+ left.push_str(ch);
5055 continue;
5156 }
5257 None => break,
@@ -56,24 +61,45 @@ pub fn print(s: &str) -> (String, Selection) {
5661 panic!("primary `#[` already appeared {:?} {:?}", left, s);
5762 }
5863
59− let head_at_beg = iter.next_if_eq(&'|').is_some();
64+ let head_at_beg = iter.next_if_eq(&"|").is_some();
65+ let last_grapheme = |s: &str| {
66+ UnicodeSegmentation::graphemes(s, true)
67+ .last()
68+ .map(String::from)
69+ };
6070
6171 while let Some(c) = iter.next() {
62− if !(c == close_pair && iter.peek() == Some(&'#')) {
63− left.push(c);
72+ let next = iter.peek();
73+ let mut prev = last_grapheme(left.as_str());
74+
75+ if !(c == close_pair && next == Some(&"#")) {
76+ left.push_str(c);
6477 continue;
6578 }
6679
6780 if !head_at_beg {
68− let prev = left.pop().unwrap();
69− if prev != '|' {
70− left.push(prev);
71− left.push(c);
72− continue;
81+ match &prev {
82+ Some(p) if p != "|" => {
83+ left.push_str(c);
84+ continue;
85+ }
86+ Some(p) if p == "|" => {
87+ left.pop().unwrap(); // pop the |
88+ prev = last_grapheme(left.as_str());
89+ }
90+ _ => (),
7391 }
7492 }
7593
7694 iter.next(); // skip "#"
95+ let next = iter.peek();
96+
97+ // skip explicit line end inside selection
98+ if (prev == Some(String::from("\r\n")) || prev == Some(String::from("\n")))
99+ && next.map(|n| String::from(*n)) == prev
100+ {
101+ iter.next();
102+ }
77103
78104 if is_primary {
79105 primary_idx = Some(ranges.len());
@@ -118,11 +144,11 @@ pub fn print(s: &str) -> (String, Selection) {
118144/// use smallvec::smallvec;
119145///
120146/// assert_eq!(
121−/// plain("abc", Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)),
147+/// plain("abc", &Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)),
122148/// "#[a|]#b#(|c)#".to_owned()
123149/// );
124150/// ```
125−pub fn plain(s: &str, selection: Selection) -> String {
151+pub fn plain(s: &str, selection: &Selection) -> String {
126152 let primary = selection.primary_index();
127153 let mut out = String::with_capacity(s.len() + 5 * selection.len());
128154 out.push_str(s);
@@ -147,6 +173,7 @@ pub fn plain(s: &str, selection: Selection) -> String {
147173 out
148174}
149175
176+#[allow(clippy::module_inception)]
150177#[cfg(test)]
151178#[allow(clippy::module_inception)]
152179mod test {