diff --git a/Cargo.lock b/Cargo.lock index 3a9673749ad61..2b83c41600600 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,6 +413,7 @@ dependencies = [ "encoding_rs", "etcetera", "helix-loader", + "indoc", "log", "once_cell", "quickcheck", diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 45272f9800582..a8705914873fe 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -47,3 +47,4 @@ textwrap = "0.16.0" [dev-dependencies] quickcheck = { version = "1", default-features = false } +indoc = "1.0.6" diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 786d7757d6b0b..f644587c61765 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -463,7 +463,6 @@ pub fn move_parent_node_end( selection.transform(|range| { let start_from = text.char_to_byte(range.from()); let start_to = text.char_to_byte(range.to()); - let start_head = range.head; let mut node = match tree .root_node() @@ -1373,7 +1372,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection.transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Move)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -1396,7 +1395,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection.transform(|r| move_prev_paragraph(text.slice(..), r, 2, Movement::Move)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -1419,7 +1418,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection .transform(|r| move_prev_paragraph(text.slice(..), r, 1, Movement::Extend)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -1439,7 +1438,7 @@ mod test { "a\nb\n\n#[goto\nthird\n\n|]#paragraph", ), ( - "a\nb#[\n|]#\ngoto\nsecond\n\nparagraph", + "a\nb#[\n|]#\n\ngoto\nsecond\n\nparagraph", "a\nb#[\n\n|]#goto\nsecond\n\nparagraph", ), ( @@ -1461,7 +1460,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection.transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Move)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -1484,7 +1483,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection.transform(|r| move_next_paragraph(text.slice(..), r, 2, Movement::Move)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -1507,7 +1506,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection .transform(|r| move_next_paragraph(text.slice(..), r, 1, Movement::Extend)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } diff --git a/helix-core/src/test.rs b/helix-core/src/test.rs index 17523ed761076..1d967f23daf5b 100644 --- a/helix-core/src/test.rs +++ b/helix-core/src/test.rs @@ -2,6 +2,7 @@ use crate::{Range, Selection}; use smallvec::SmallVec; use std::cmp::Reverse; +use unicode_segmentation::UnicodeSegmentation; /// Convert annotated test string to test string and selection. /// @@ -10,6 +11,10 @@ use std::cmp::Reverse; /// `#[` 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 /// /// ``` @@ -30,23 +35,23 @@ use std::cmp::Reverse; pub fn print(s: &str) -> (String, Selection) { let mut primary_idx = None; let mut ranges = SmallVec::new(); - let mut iter = s.chars().peekable(); + 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(c); + if c != "#" { + left.push_str(c); continue; } let (is_primary, close_pair) = match iter.next() { - Some('[') => (true, ']'), - Some('(') => (false, ')'), + Some("[") => (true, "]"), + Some("(") => (false, ")"), Some(ch) => { left.push('#'); - left.push(ch); + left.push_str(ch); continue; } None => break, @@ -56,24 +61,45 @@ pub fn print(s: &str) -> (String, Selection) { panic!("primary `#[` already appeared {:?} {:?}", left, s); } - let head_at_beg = iter.next_if_eq(&'|').is_some(); + 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() { - if !(c == close_pair && iter.peek() == Some(&'#')) { - left.push(c); + 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 { - let prev = left.pop().unwrap(); - if prev != '|' { - left.push(prev); - left.push(c); - continue; + 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()); @@ -118,11 +144,11 @@ pub fn print(s: &str) -> (String, Selection) { /// use smallvec::smallvec; /// /// assert_eq!( -/// plain("abc", Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)), +/// plain("abc", &Selection::new(smallvec![Range::new(0, 1), Range::new(3, 2)], 0)), /// "#[a|]#b#(|c)#".to_owned() /// ); /// ``` -pub fn plain(s: &str, selection: Selection) -> String { +pub fn plain(s: &str, selection: &Selection) -> String { let primary = selection.primary_index(); let mut out = String::with_capacity(s.len() + 5 * selection.len()); out.push_str(s); @@ -147,6 +173,7 @@ pub fn plain(s: &str, selection: Selection) -> String { out } +#[allow(clippy::module_inception)] #[cfg(test)] #[allow(clippy::module_inception)] mod test { diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 76c6d103e6c7c..dcca1673d3bd7 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -425,7 +425,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 1)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -448,7 +448,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Inside, 2)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } @@ -479,7 +479,7 @@ mod test { let text = Rope::from(s.as_str()); let selection = selection .transform(|r| textobject_paragraph(text.slice(..), r, TextObject::Around, 1)); - let actual = crate::test::plain(&s, selection); + let actual = crate::test::plain(&s, &selection); assert_eq!(actual, expected, "\nbefore: `{:?}`", before); } } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index b555288e7c2c8..d4ef671e86dfb 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -138,12 +138,12 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { .as_str(), "|echo foo", platform_line(indoc! {"\ - #[|foo - ]# - #(|foo - )# - #(|foo - )# + #[|foo\n]# + + #(|foo\n)# + + #(|foo\n)# + "}) .as_str(), )) @@ -159,12 +159,12 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { .as_str(), "!echo foo", platform_line(indoc! {"\ - #[|foo - ]#lorem - #(|foo - )#ipsum - #(|foo - )#dolor + #[|foo\n]# + lorem + #(|foo\n)# + ipsum + #(|foo\n)# + dolor "}) .as_str(), )) @@ -180,12 +180,12 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { .as_str(), "echo foo", platform_line(indoc! {"\ - lorem#[|foo - ]# - ipsum#(|foo - )# - dolor#(|foo - )# + lorem#[|foo\n]# + + ipsum#(|foo\n)# + + dolor#(|foo\n)# + "}) .as_str(), )) diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs index 3202069b2f7cb..3737488bc359d 100644 --- a/helix-term/tests/test/commands/movement.rs +++ b/helix-term/tests/test/commands/movement.rs @@ -5,7 +5,7 @@ use helix_view::document::Mode; use super::*; -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_move_parent_node_end() -> anyhow::Result<()> { let keymap = hashmap!( Mode::Normal => Keymap::new(keymap!({ "Normal mode" @@ -32,14 +32,35 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { } } "##}), - "", + "", + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\"#[\n|]# + } + } + "}), + ), + ( + helpers::platform_line(indoc! {"\ + fn foo() { + let result = if true { + \"yes\" + } else { + \"no\"#[\n|]# + } + } + "}), + "", helpers::platform_line(indoc! {"\ fn foo() { let result = if true { \"yes\" } else { \"no\" - }#[\n|]#\ + }#[\n|]# } "}), ), @@ -61,7 +82,7 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { \"yes\" } else { #[\"no\" - }\n|]#\ + }\n|]# } "}), ), @@ -86,7 +107,7 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> { Ok(()) } -#[tokio::test] +#[tokio::test(flavor = "multi_thread")] async fn test_move_parent_node_start() -> anyhow::Result<()> { let keymap = hashmap!( Mode::Normal => Keymap::new(keymap!({ "Normal mode"