From f9699e77b137d6f6f5a800be30f5925bb6a3f000 Mon Sep 17 00:00:00 2001 From: Andreas Liljeqvist Date: Thu, 24 Jun 2021 10:15:23 +0200 Subject: [PATCH 1/3] WIP surround as selection --- helix-core/src/selection.rs | 5 +++ helix-core/src/surround.rs | 68 ++++++++++++++++++++++++++++++++++- helix-term/src/commands.rs | 70 +++++++++++++++++++++++++++++-------- 3 files changed, 128 insertions(+), 15 deletions(-) diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index e452c2e28efa..53ae2eee1232 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -128,6 +128,11 @@ impl Range { pub fn fragment<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> Cow<'b, str> { Cow::from(text.slice(self.from()..self.to() + 1)) } + + #[inline] + pub fn len(&self) -> usize { + self.from() + 1 - self.to() + } } /// A selection consists of one or more selection ranges. diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index d7314609bfca..386c74ee958b 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -29,6 +29,50 @@ pub fn get_pair(ch: char) -> (char, char) { .unwrap_or((ch, ch)) } +/// Find the position of balanced surround pairs of `ch` which can be either a closing +/// or opening pair. +pub fn find_balanced_pairs_pos(text: RopeSlice, ch: char, pos: usize) -> Option<(usize, usize)> { + let (open, close) = get_pair(ch); + + let starting_pos = pos; + let mut pos = pos; + let mut skip = 0; + let mut chars = text.chars_at(pos); + let open_pos = if text.char(pos) == open { + Some(pos) + } else { + loop { + if let Some(c) = chars.prev() { + pos = pos.saturating_sub(1); + + if c == open { + if skip > 0 { + skip -= 1; + } else { + break Some(pos); + } + } else if c == close { + skip += 1; + } + } else { + break None; + } + } + }?; + let mut count = 1; + for (i, c) in text.slice(open_pos + 1..).chars().enumerate() { + if c == open { + count += 1; + } else if c == close { + count -= 1; + if count == 0 { + return Some((open_pos, open_pos + 1 + i)); + } + } + } + None +} + /// Find the position of surround pairs of `ch` which can be either a closing /// or opening pair. `n` will skip n - 1 pairs (eg. n=2 will discard (only) /// the first pair found and keep looking) @@ -59,7 +103,7 @@ pub fn get_surround_pos( let mut change_pos = Vec::new(); for range in selection { - let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?; + let (open_pos, close_pos) = find_balanced_pairs_pos(text, ch, range.head)?; if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) { return None; } @@ -90,6 +134,28 @@ mod test { assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10))); } + #[test] + fn test_find_balanced_pairs_pos() { + let doc = Rope::from("some ((text) here)"); + let slice = doc.slice(..); + + // cursor on [t]ext + assert_eq!(find_balanced_pairs_pos(slice, '(', 7), Some((6, 11))); + assert_eq!(find_balanced_pairs_pos(slice, ')', 7), Some((6, 11))); + // cursor on so[m]e + assert_eq!(find_balanced_pairs_pos(slice, '(', 2), None); + // cursor on bracket itself + assert_eq!(find_balanced_pairs_pos(slice, '(', 6), Some((6, 11))); + // cursor on outer parens + assert_eq!(find_balanced_pairs_pos(slice, '(', 5), Some((5, 17))); + + let doc = Rope::from("some (text (here))"); + let slice = doc.slice(..); + + // cursor on outer parens + assert_eq!(find_balanced_pairs_pos(slice, '(', 17), Some((5, 17))); + } + #[test] fn test_find_nth_pairs_pos_skip() { let doc = Rope::from("(so (many (good) text) here)"); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6281cfe443ac..5692664a16d7 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -612,7 +612,43 @@ fn replace(cx: &mut Context) { }; if let Some(ch) = ch { - let transaction = + let ranges = doc.selection(view.id).ranges(); + + let text = doc.text(); + let transaction = match ranges { + [open_range, close_range] => { + let open_char = text.char(open_range.from()); + let close_char = text.char(close_range.from()); + let ch = ch.chars().nth(0).unwrap(); + let (open, close) = get_pair(open_char); + if open_char == open + && close_char == close + && open_range.len() == 1 + && close_range.len() == 1 + { + let (open_replace, close_replace) = get_pair(ch); + Some(Transaction::change( + doc.text(), + std::array::IntoIter::new([ + ( + open_range.from(), + open_range.to() + 1, + Some(open_replace.to_string().into()), + ), + ( + close_range.from(), + close_range.to() + 1, + Some(close_replace.to_string().into()), + ), + ]), + )) + } else { + None + } + } + _ => None, + } + .unwrap_or_else(|| { Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { let max_to = doc.text().len_chars().saturating_sub(1); let to = std::cmp::min(max_to, range.to() + 1); @@ -628,7 +664,8 @@ fn replace(cx: &mut Context) { .collect(); (range.from(), to, Some(text.into())) - }); + }) + }); doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -3313,30 +3350,35 @@ fn right_bracket_mode(cx: &mut Context) { } fn match_mode(cx: &mut Context) { - let count = cx.count; cx.on_next_key(move |cx, event| { if let KeyEvent { code: KeyCode::Char(ch), .. } = event { - // FIXME: count gets reset because of cx.on_next_key() - cx.count = count; - match ch { - 'm' => match_brackets(cx), - 's' => surround_add(cx), - 'r' => surround_replace(cx), - 'd' => { - surround_delete(cx); - let (view, doc) = current!(cx.editor); + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let selection = doc.selection(view.id); + let pos = selection.cursor(); + + let change_pos = match surround::get_surround_pos(text, selection, ch, count) { + Some(c) => { + let ranges: SmallVec<_> = c + .chunks(2) + .flat_map(|r| [Range::new(r[0], r[0]), Range::new(r[1], r[1])]) + .collect(); + doc.set_selection(view.id, Selection::new(ranges, 0)); } - _ => (), - } + None => return, + }; } }) } use helix_core::surround; +use helix_core::surround::get_pair; +use std::iter::FromIterator; fn surround_add(cx: &mut Context) { cx.on_next_key(move |cx, event| { From 82e3005c43e6386b3d2317a4c65cd9a32e3b7ed1 Mon Sep 17 00:00:00 2001 From: Andreas Liljeqvist Date: Thu, 24 Jun 2021 10:54:01 +0200 Subject: [PATCH 2/3] Fix clippy --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5692664a16d7..9c51fb46c32c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -619,7 +619,7 @@ fn replace(cx: &mut Context) { [open_range, close_range] => { let open_char = text.char(open_range.from()); let close_char = text.char(close_range.from()); - let ch = ch.chars().nth(0).unwrap(); + let ch = ch.chars().next().unwrap(); let (open, close) = get_pair(open_char); if open_char == open && close_char == close From ee9692eed30004cde7407e9d4f34777f161eaf4a Mon Sep 17 00:00:00 2001 From: Andreas Liljeqvist Date: Thu, 24 Jun 2021 12:00:20 +0200 Subject: [PATCH 3/3] Fix match for '|' and '|' - same open/close --- helix-core/src/surround.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 386c74ee958b..055e08fab9ca 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -61,13 +61,13 @@ pub fn find_balanced_pairs_pos(text: RopeSlice, ch: char, pos: usize) -> Option< }?; let mut count = 1; for (i, c) in text.slice(open_pos + 1..).chars().enumerate() { - if c == open { - count += 1; - } else if c == close { + if c == close { count -= 1; if count == 0 { return Some((open_pos, open_pos + 1 + i)); } + } else if c == open { + count += 1; } } None