From 97910ca0e818445f575f595aefbb383c73c36328 Mon Sep 17 00:00:00 2001 From: thomasschafer Date: Sat, 14 Sep 2024 16:43:28 +0100 Subject: [PATCH] Add fallback command to [ and ] to find in next pair --- helix-core/src/surround.rs | 78 +++++++++++++++++++++++++++++--- helix-core/src/textobject.rs | 47 +++++++++++++++---- helix-term/src/commands.rs | 49 ++++++++++++++++---- helix-term/src/keymap/default.rs | 4 +- 4 files changed, 152 insertions(+), 26 deletions(-) diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index e45346c92..c0a40b51a 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -154,6 +154,12 @@ fn find_nth_closest_pairs_plain( Err(Error::PairNotFound) } +pub enum FindType { + Surround(usize), + Next(usize), + Prev(usize), +} + /// 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) @@ -161,7 +167,7 @@ pub fn find_nth_pairs_pos( text: RopeSlice, ch: char, range: Range, - n: usize, + find_type: FindType, ) -> Result<(usize, usize)> { if text.len_chars() < 2 { return Err(Error::PairNotFound); @@ -172,6 +178,17 @@ pub fn find_nth_pairs_pos( let (open, close) = get_pair(ch); let pos = range.cursor(text); + let (pos, n) = match find_type { + FindType::Surround(n) => (pos, n), + FindType::Next(n) => match search::find_nth_next(text, open, pos, n) { + Some(next_pos) => (next_pos + 1, 1), + None => return Err(Error::PairNotFound), + }, + FindType::Prev(n) => match search::find_nth_prev(text, close, pos, n) { + Some(next_pos) => (next_pos - 1, 1), + None => return Err(Error::PairNotFound), + }, + }; let (open, close) = if open == close { if Some(open) == text.get_char(pos) { @@ -298,7 +315,7 @@ pub fn get_surround_pos( for &range in selection { let (open_pos, close_pos) = { let range_raw = match ch { - Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, + Some(ch) => find_nth_pairs_pos(text, ch, range, FindType::Surround(skip))?, None => find_nth_closest_pairs_pos(syntax, text, range, skip)?, }; let range = Range::new(range_raw.0, range_raw.1); @@ -392,8 +409,13 @@ mod test { assert_eq!(2, expectations.len()); assert_eq!( - find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1) - .expect("find should succeed"), + find_nth_pairs_pos( + doc.slice(..), + '\'', + selection.primary(), + FindType::Surround(1) + ) + .expect("find should succeed"), (expectations[0], expectations[1]) ) } @@ -409,7 +431,46 @@ mod test { assert_eq!(2, expectations.len()); assert_eq!( - find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 2) + find_nth_pairs_pos( + doc.slice(..), + '\'', + selection.primary(), + FindType::Surround(2) + ) + .expect("find should succeed"), + (expectations[0], expectations[1]) + ) + } + + #[test] + fn test_find_inside_third_next_quote() { + #[rustfmt::skip] + let (doc, selection, expectations) = + rope_with_selections_and_expectations( + "some 'nested 'quoted' text' on this 'line'\n'and this one'", + " ^ _ _ \n " + ); + + assert_eq!(2, expectations.len()); + assert_eq!( + find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), FindType::Next(3)) + .expect("find should succeed"), + (expectations[0], expectations[1]) + ) + } + + #[test] + fn test_find_inside_prev_quote() { + #[rustfmt::skip] + let (doc, selection, expectations) = + rope_with_selections_and_expectations( + "some 'nested 'quoted' text' on this 'line'\n'and this one'", + " _ _ ^ \n " + ); + + assert_eq!(2, expectations.len()); + assert_eq!( + find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), FindType::Prev(1)) .expect("find should succeed"), (expectations[0], expectations[1]) ) @@ -425,7 +486,12 @@ mod test { ); assert_eq!( - find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1), + find_nth_pairs_pos( + doc.slice(..), + '\'', + selection.primary(), + FindType::Surround(1) + ), Err(Error::CursorOnAmbiguousPair) ) } diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 7576b3a78..ec0f76c07 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -7,6 +7,7 @@ use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; use crate::line_ending::rope_is_line_ending; use crate::movement::Direction; +use crate::surround::FindType; use crate::syntax::LanguageConfiguration; use crate::Range; use crate::{surround, Syntax}; @@ -204,9 +205,15 @@ pub fn textobject_pair_surround( range: Range, textobject: TextObject, ch: char, - count: usize, + find_type: FindType, ) -> Range { - textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count) + textobject_pair_surround_impl( + syntax, + slice, + range, + textobject, + FindVariant::Char((ch, find_type)), + ) } pub fn textobject_pair_surround_closest( @@ -216,7 +223,18 @@ pub fn textobject_pair_surround_closest( textobject: TextObject, count: usize, ) -> Range { - textobject_pair_surround_impl(syntax, slice, range, textobject, None, count) + textobject_pair_surround_impl( + syntax, + slice, + range, + textobject, + FindVariant::Closest(count), + ) +} + +enum FindVariant { + Char((char, FindType)), + Closest(usize), } fn textobject_pair_surround_impl( @@ -224,12 +242,15 @@ fn textobject_pair_surround_impl( slice: RopeSlice, range: Range, textobject: TextObject, - ch: Option, - count: usize, + find_variant: FindVariant, ) -> Range { - let pair_pos = match ch { - Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count), - None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count), + let pair_pos = match find_variant { + FindVariant::Char((ch, find_type)) => { + surround::find_nth_pairs_pos(slice, ch, range, find_type) + } + FindVariant::Closest(count) => { + surround::find_nth_closest_pairs_pos(syntax, slice, range, count) + } }; pair_pos .map(|(anchor, head)| match textobject { @@ -576,8 +597,14 @@ mod test { let slice = doc.slice(..); for &case in scenario { let (pos, objtype, expected_range, ch, count) = case; - let result = - textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count); + let result = textobject_pair_surround( + None, + slice, + Range::point(pos), + objtype, + ch, + FindType::Surround(count), + ); assert_eq!( result, expected_range.into(), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4b1698e45..861f4b7f8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -22,15 +22,16 @@ use helix_core::{ encoding, find_workspace, graphemes::{self, next_grapheme_boundary, RevRopeGraphemes}, history::UndoKind, - increment, indent, - indent::IndentStyle, + increment, + indent::{self, IndentStyle}, line_ending::{get_line_ending_of_str, line_end_char_index}, match_brackets, movement::{self, move_vertically_visual, Direction}, object, pos_at_coords, regex::{self, Regex}, search::{self, CharMatcher}, - selection, shellwords, surround, + selection, shellwords, + surround::{self, FindType}, syntax::{BlockCommentToken, LanguageServerFeature}, text_annotations::{Overlay, TextAnnotations}, textobject, @@ -725,8 +726,10 @@ impl FallbackCommand { #[rustfmt::skip] static_fallback_commands!( - select_textobject_inside_surrounding_pair, "Select inside any character acting as a pair (tree-sitter)", - select_textobject_around_surrounding_pair, "Select around any character acting as a pair (tree-sitter)", + select_textobject_inside_surrounding_pair, "Select inside any character pair (tree-sitter)", + select_textobject_around_surrounding_pair, "Select around any character pair (tree-sitter)", + select_textobject_inside_prev_pair, "Select inside previous character pair (tree-sitter)", + select_textobject_inside_next_pair, "Select inside next character pair (tree-sitter)", ); } @@ -5693,17 +5696,36 @@ fn textobject_change(cx: &mut Context) { } fn select_textobject_inside_surrounding_pair(cx: &mut Context, ch: char) { - textobject_surrounding_pair(cx, textobject::TextObject::Inside, ch); + textobject_surrounding_pair(cx, textobject::TextObject::Inside, ch, None); } fn select_textobject_around_surrounding_pair(cx: &mut Context, ch: char) { - textobject_surrounding_pair(cx, textobject::TextObject::Around, ch); + textobject_surrounding_pair(cx, textobject::TextObject::Around, ch, None); +} + +fn select_textobject_inside_prev_pair(cx: &mut Context, ch: char) { + textobject_surrounding_pair( + cx, + textobject::TextObject::Inside, + ch, + Some(Direction::Backward), + ); +} + +fn select_textobject_inside_next_pair(cx: &mut Context, ch: char) { + textobject_surrounding_pair( + cx, + textobject::TextObject::Inside, + ch, + Some(Direction::Forward), + ); } fn textobject_surrounding_pair( cx: &mut Context, textobject: textobject::TextObject, pair_char: char, + direction: Option, ) { if pair_char.is_ascii_alphanumeric() { return; @@ -5715,7 +5737,18 @@ fn textobject_surrounding_pair( let text = doc.text().slice(..); let syntax = doc.syntax(); let selection = doc.selection(view.id).clone().transform(|range| { - textobject::textobject_pair_surround(syntax, text, range, textobject, pair_char, count) + let find_type = match direction { + None => FindType::Surround, + Some(Direction::Forward) => FindType::Next, + Some(Direction::Backward) => FindType::Prev, + }(count); + let mut range = textobject::textobject_pair_surround( + syntax, text, range, textobject, pair_char, find_type, + ); + if let Some(direction) = direction { + range = range.with_direction(direction); + } + range }); doc.set_selection(view.id, selection); }; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index fcb1710c3..9ee51fb53 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -131,7 +131,7 @@ pub fn default() -> HashMap { "g" => select_textobject_around_change, }, }, - "[" => { "Left bracket" + "[" => { "Left bracket" fallback=select_textobject_inside_prev_pair "d" => goto_prev_diag, "D" => goto_first_diag, "g" => goto_prev_change, @@ -145,7 +145,7 @@ pub fn default() -> HashMap { "p" => goto_prev_paragraph, "space" => add_newline_above, }, - "]" => { "Right bracket" + "]" => { "Right bracket" fallback=select_textobject_inside_next_pair "d" => goto_next_diag, "D" => goto_last_diag, "g" => goto_next_change,