From de15d7017186cb735cdd31d12510f61a1707d5fb Mon Sep 17 00:00:00 2001 From: Gokul Soumya Date: Sun, 3 Apr 2022 21:31:43 +0530 Subject: [PATCH] Add `m` textobject to select closest surround pair --- book/src/usage.md | 1 + helix-core/src/surround.rs | 39 ++++++++++++++++++++++++++++++++++++ helix-core/src/textobject.rs | 26 +++++++++++++++++++++++- helix-term/src/commands.rs | 9 +-------- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/book/src/usage.md b/book/src/usage.md index 010e30f5..7871e2b9 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -66,6 +66,7 @@ ## Textobjects | `w` | Word | | `W` | WORD | | `(`, `[`, `'`, etc | Specified surround pairs | +| `m` | Closest surround pair | | `f` | Function | | `c` | Class | | `a` | Argument/parameter | diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index c14456b7..4cba81bf 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -52,6 +52,45 @@ pub fn get_pair(ch: char) -> (char, char) { .unwrap_or((ch, ch)) } +pub fn find_nth_closest_pairs_pos( + text: RopeSlice, + range: Range, + n: usize, +) -> Result<(usize, usize)> { + let is_open_pair = |ch| PAIRS.iter().any(|(open, _)| *open == ch); + let is_close_pair = |ch| PAIRS.iter().any(|(_, close)| *close == ch); + + let mut stack = Vec::with_capacity(2); + let pos = range.cursor(text); + + for ch in text.chars_at(pos) { + if is_open_pair(ch) { + // Track open pairs encountered so that we can step over + // the correspoding close pairs that will come up further + // down the loop. We want to find a lone close pair whose + // open pair is before the cursor position. + stack.push(ch); + continue; + } else if is_close_pair(ch) { + let (open, _) = get_pair(ch); + if stack.last() == Some(&open) { + stack.pop(); + continue; + } else { + // In the ideal case the stack would be empty here and the + // current character would be the close pair that we are + // looking for. It could also be the case that the pairs + // are unbalanced and we encounter a close pair that doesn't + // close the last seen open pair. In either case use this + // char as the auto-detected closest pair. + return find_nth_pairs_pos(text, ch, range, n); + } + } + } + + Err(Error::PairNotFound) +} + /// 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) diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index ab418792..ee06bf47 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -205,7 +205,31 @@ pub fn textobject_surround( ch: char, count: usize, ) -> Range { - surround::find_nth_pairs_pos(slice, ch, range, count) + textobject_surround_impl(slice, range, textobject, Some(ch), count) +} + +pub fn textobject_surround_closest( + slice: RopeSlice, + range: Range, + textobject: TextObject, + count: usize, +) -> Range { + textobject_surround_impl(slice, range, textobject, None, count) +} + +fn textobject_surround_impl( + slice: RopeSlice, + range: Range, + textobject: TextObject, + ch: Option, + count: usize, +) -> Range { + let pair_pos = match ch { + Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count), + // Automatically find the closest surround pairs + None => surround::find_nth_closest_pairs_pos(slice, range, count), + }; + pair_pos .map(|(anchor, head)| match textobject { TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head), TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d1c1724a..75c84b74 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4040,14 +4040,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { 'a' => textobject_treesitter("parameter", range), 'o' => textobject_treesitter("comment", range), 'p' => textobject::textobject_paragraph(text, range, objtype, count), - 'm' => { - let ch = text.char(range.cursor(text)); - if !ch.is_ascii_alphanumeric() { - textobject::textobject_surround(text, range, objtype, ch, count) - } else { - range - } - } + 'm' => textobject::textobject_surround_closest(text, range, objtype, count), // TODO: cancel new ranges if inconsistent surround matches across lines ch if !ch.is_ascii_alphanumeric() => { textobject::textobject_surround(text, range, objtype, ch, count)