From 8711b9e4fa6e15d024967e38d87e494f067a8f9e Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 5 Jun 2023 01:16:05 -0400 Subject: [PATCH] insert double whitespace inside pair --- helix-core/src/auto_pairs.rs | 49 +++++- helix-term/tests/test/auto_pairs.rs | 247 ++++++++++++++++++++++++++-- 2 files changed, 280 insertions(+), 16 deletions(-) diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index fb918a9a2..37d3a2ddb 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -127,6 +127,8 @@ pub fn hook_insert( // && char_at pos == close return handle_insert_close(doc, range, pair); } + } else if ch.is_whitespace() { + return handle_insert_whitespace(doc, range, ch, pairs); } None @@ -139,12 +141,33 @@ pub fn hook_delete(doc: &Rope, range: &Range, pairs: &AutoPairs) -> Option<(Dele let cur = doc.get_char(cursor)?; let prev = prev_char(doc, cursor)?; + + // check for whitespace surrounding a pair + if doc.len_chars() >= 4 && prev.is_whitespace() && cur.is_whitespace() { + let second_prev = doc.get_char(graphemes::nth_prev_grapheme_boundary(text, cursor, 2))?; + let second_next = doc.get_char(graphemes::next_grapheme_boundary(text, cursor))?; + log::debug!("second_prev: {}, second_next: {}", second_prev, second_next); + + if let Some(pair) = pairs.get(second_prev) { + if pair.open == second_prev && pair.close == second_next { + return handle_delete(doc, range); + } + } + } + let pair = pairs.get(cur)?; - if pair.open != prev { + if pair.open != prev || pair.close != cur { return None; } + handle_delete(doc, range) +} + +pub fn handle_delete(doc: &Rope, range: &Range) -> Option<(Deletion, Range)> { + let text = doc.slice(..); + let cursor = range.cursor(text); + let end_next = graphemes::next_grapheme_boundary(text, cursor); let end_prev = graphemes::prev_grapheme_boundary(text, cursor); @@ -162,6 +185,30 @@ pub fn hook_delete(doc: &Rope, range: &Range, pairs: &AutoPairs) -> Option<(Dele Some((delete, next_range)) } +fn handle_insert_whitespace( + doc: &Rope, + range: &Range, + ch: char, + pairs: &AutoPairs, +) -> Option<(Change, Range)> { + let text = doc.slice(..); + let cursor = range.cursor(text); + let cur = doc.get_char(cursor)?; + let prev = prev_char(doc, cursor)?; + let pair = pairs.get(cur)?; + + if pair.open != prev || pair.close != cur { + return None; + } + + let whitespace_pair = Pair { + open: ch, + close: ch, + }; + + handle_insert_same(doc, range, &whitespace_pair) +} + fn prev_char(doc: &Rope, pos: usize) -> Option { if pos == 0 { return None; diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs index 04fec1c4e..452292a1b 100644 --- a/helix-term/tests/test/auto_pairs.rs +++ b/helix-term/tests/test/auto_pairs.rs @@ -16,10 +16,119 @@ fn matching_pairs() -> impl Iterator { async fn insert_basic() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( - format!("#[{}|]#", LINE_END), + "#[\n|]#", format!("i{}", pair.0), - format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END), - LineFeedHandling::AsIs, + format!("{}#[|{}]#", pair.0, pair.1), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn insert_whitespace() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("{}#[|{}]#", pair.0, pair.1), + "i ", + format!("{} #[| ]#{}", pair.0, pair.1), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn insert_whitespace_multi() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!( + indoc! {"\ + {open}#[|{close}]# + {open}#(|{open})#{close}{close} + {open}{open}#(|{close}{close})# + foo#(|\n)# + "}, + open = pair.0, + close = pair.1, + ), + "i ", + format!( + indoc! {"\ + {open} #[| ]#{close} + {open} #(|{open})#{close}{close} + {open}{open} #(| {close}{close})# + foo #(|\n)# + "}, + open = pair.0, + close = pair.1, + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn append_whitespace_multi() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!( + indoc! {"\ + #[|{open}]#{close} + #(|{open})#{open}{close}{close} + #(|{open}{open})#{close}{close} + #(|foo)# + "}, + open = pair.0, + close = pair.1, + ), + "a ", + format!( + indoc! {"\ + #[{open} |]#{close} + #({open} {open}|)#{close}{close} + #({open}{open} |)#{close}{close} + #(foo \n|)# + "}, + open = pair.0, + close = pair.1, + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn insert_whitespace_no_pair() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + // sanity check - do not insert extra whitespace unless immediately + // surrounded by a pair + test(( + format!("{} #[|{}]#", pair.0, pair.1), + "i ", + format!("{} #[|{}]#", pair.0, pair.1), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn insert_whitespace_no_matching_pair() -> anyhow::Result<()> { + for pair in differing_pairs() { + // sanity check - verify whitespace does not insert unless both pairs + // are matches, i.e. no two different openers + test(( + format!("{}#[|{}]#", pair.0, pair.0), + "i ", + format!("{} #[|{}]#", pair.0, pair.0), )) .await?; } @@ -596,6 +705,112 @@ async fn delete_multi() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn delete_whitespace() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("{} #[| ]#{}", pair.0, pair.1), + "i", + format!("{}#[{}|]#", pair.0, pair.1), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn delete_whitespace_multi() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!( + indoc! {"\ + {open} #[| ]#{close} + {open} #(|{open})#{close}{close} + {open}{open} #(| {close}{close})# + foo #(|\n)# + "}, + open = pair.0, + close = pair.1, + ), + "i", + format!( + indoc! {"\ + {open}#[{close}|]# + {open}#(|{open})#{close}{close} + {open}{open}#(|{close}{close})# + foo#(|\n)# + "}, + open = pair.0, + close = pair.1, + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn delete_append_whitespace_multi() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!( + indoc! {"\ + #[{open} |]# {close} + #({open} |)#{open}{close}{close} + #({open}{open} |)# {close}{close} + #(foo |)# + "}, + open = pair.0, + close = pair.1, + ), + "a", + format!( + indoc! {"\ + #[{open}{close}|]# + #({open}{open}|)#{close}{close} + #({open}{open}{close}|)#{close} + #(foo\n|)# + "}, + open = pair.0, + close = pair.1, + ), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn delete_whitespace_no_pair() -> anyhow::Result<()> { + for pair in DEFAULT_PAIRS { + test(( + format!("{} #[|{}]#", pair.0, pair.1), + "i", + format!("{} #[|{}]#", pair.0, pair.1), + )) + .await?; + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn delete_whitespace_no_matching_pair() -> anyhow::Result<()> { + for pair in differing_pairs() { + test(( + format!("{} #[|{}]#", pair.0, pair.0), + "i", + format!("{}#[|{}]#", pair.0, pair.0), + )) + .await?; + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread")] async fn delete_configured_multi_byte_chars() -> anyhow::Result<()> { // NOTE: these are multi-byte Unicode characters @@ -827,6 +1042,7 @@ async fn delete_at_end_of_document() -> anyhow::Result<()> { in_keys: String::from("i"), out_text: String::from(LINE_END), out_selection: Selection::single(LINE_END.len(), LINE_END.len()), + line_feed_handling: LineFeedHandling::AsIs, }) .await?; @@ -836,6 +1052,7 @@ async fn delete_at_end_of_document() -> anyhow::Result<()> { in_keys: String::from("i"), out_text: format!("foo{}", LINE_END), out_selection: Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()), + line_feed_handling: LineFeedHandling::AsIs, }) .await?; } @@ -960,57 +1177,57 @@ async fn delete_append_end_of_word() -> anyhow::Result<()> { async fn delete_mixed_dedent() -> anyhow::Result<()> { for pair in DEFAULT_PAIRS { test(( - helpers::platform_line(&format!( + format!( indoc! {"\ bar = {}#[|{}]# #(|\n)# foo#(|\n)# "}, pair.0, pair.1, - )), + ), "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ bar = #[\n|]# #(|\n)# fo#(|\n)# - "}), + "}, )) .await?; test(( - helpers::platform_line(&format!( + format!( indoc! {"\ bar = {}#[|{}woop]# #(|word)# fo#(|o)# "}, pair.0, pair.1, - )), + ), "i", - helpers::platform_line(indoc! {"\ + indoc! {"\ bar = #[|woop]# #(|word)# f#(|o)# - "}), + "}, )) .await?; // delete from the right with append test(( - helpers::platform_line(&format!( + format!( indoc! {"\ bar = #[|woop{}]#{} #(| )#word #(|fo)#o "}, pair.0, pair.1, - )), + ), "a", - helpers::platform_line(indoc! {"\ + indoc! {"\ bar = #[woop\n|]# #(w|)#ord #(fo|)# - "}), + "}, )) .await?; }