Unnamed repository; edit this file 'description' to name the repository.
insert double whitespace inside pair
Skyler Hawthorne 6 months ago
parent bb0198e · commit 631c3e3
-rw-r--r--helix-core/src/auto_pairs.rs49
-rw-r--r--helix-term/tests/test/auto_pairs.rs247
2 files changed, 280 insertions, 16 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index fb918a9a..37d3a2dd 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<char> {
if pos == 0 {
return None;
diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs
index 04fec1c4..452292a1 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<Item = &'static (char, char)> {
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?;
}
@@ -597,6 +706,112 @@ async fn delete_multi() -> anyhow::Result<()> {
}
#[tokio::test(flavor = "multi_thread")]
+async fn delete_whitespace() -> anyhow::Result<()> {
+ for pair in DEFAULT_PAIRS {
+ test((
+ format!("{} #[| ]#{}", pair.0, pair.1),
+ "i<backspace>",
+ 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<backspace>",
+ 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<backspace>",
+ 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<backspace>",
+ 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<backspace>",
+ 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
let pairs = hashmap!('„' => '“', '‚' => '‘', '「' => '」');
@@ -827,6 +1042,7 @@ async fn delete_at_end_of_document() -> anyhow::Result<()> {
in_keys: String::from("i<backspace>"),
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<backspace>"),
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<backspace>",
- 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<backspace>",
- 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<backspace>",
- helpers::platform_line(indoc! {"\
+ indoc! {"\
bar = #[woop\n|]#
#(w|)#ord
#(fo|)#
- "}),
+ "},
))
.await?;
}