diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index 773df59f8..786d4ccc9 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -14,17 +14,20 @@ use crate::{surround, Syntax}; /// # Arguments /// /// * `pos` - index of the character -/// * `is_long` - whether it's a word or a WORD +/// * `long` - whether it's a word or a WORD fn find_word_boundary( slice: RopeSlice, mut pos: usize, direction: Direction, - is_long: bool, + long: bool, + is_subword: bool, ) -> usize { use CharCategory::{Eol, Whitespace}; let iter = match direction { + // create forward iterator Direction::Forward => slice.chars_at(pos), + // create reverse iterator, if we iterate over it we will be advancing in the opposite direction Direction::Backward => { let mut iter = slice.chars_at(pos); iter.reverse(); @@ -32,46 +35,52 @@ fn find_word_boundary( } }; - // first/last relative to the entire document - let is_first_char = pos == 0; - let is_last_char = pos == slice.len_chars(); - - // the previous character relative to the direction we are going - let prev_char_forward = slice.char(pos - 1); - let prev_char_backward = slice.char(pos); - - // this needs to be updated to account for the fact that wordly characters are not _ or - - let mut prev_char_category = match direction { - Direction::Forward if is_first_char => Whitespace, - Direction::Backward if is_last_char => Whitespace, - Direction::Forward => categorize_char(prev_char_forward), - Direction::Backward => categorize_char(prev_char_backward), + let mut prev_category = match direction { + // if we are at the beginning or end of the document + Direction::Forward if pos == 0 => Whitespace, + Direction::Backward if pos == slice.len_chars() => Whitespace, + Direction::Forward => categorize_char(slice.char(pos - 1)), + Direction::Backward => categorize_char(slice.char(pos)), }; - let is_subword = true; + let mut prev_ch = match direction { + // if we are at the beginning or end of the document + Direction::Forward if pos == 0 => ' ', + Direction::Backward if pos == slice.len_chars() => ' ', + Direction::Forward => slice.char(pos - 1), + Direction::Backward => slice.char(pos), + }; for ch in iter { match categorize_char(ch) { - // when we hit whitespace, stop iterating + // When we find the first whitespace, that's going to be our position that we jump to Eol | Whitespace => return pos, - char_category => { - // compare current char to the previous char, if we are - // iterating forwards e.g.: - // a_ => true, a and _ are Word chars - // a+ => false, a is Word char, + is a MathSymbol - let did_category_change = char_category != prev_char_category; - - if !is_long && !is_subword && did_category_change && !is_first_char && !is_last_char - { + // every character other than a whitespace + category => { + let matches_short_word = !long + && !is_subword + && category != prev_category + && pos != 0 + && pos != slice.len_chars(); + + let matches_subword = is_subword + && ((prev_ch == '_' || ch == '_') + || match direction { + Direction::Forward => prev_ch.is_lowercase() && ch.is_uppercase(), + Direction::Backward => prev_ch.is_uppercase() && ch.is_lowercase(), + }); + + if matches_subword { return pos; - } else if is_subword && ch == '_' { + } else if matches_short_word { return pos; } else { match direction { Direction::Forward => pos += 1, Direction::Backward => pos = pos.saturating_sub(1), } - prev_char_category = char_category; + prev_category = category; + prev_ch = ch; } } } @@ -105,13 +114,14 @@ pub fn textobject_word( textobject: TextObject, _count: usize, long: bool, + is_subword: bool, ) -> Range { let pos = range.cursor(slice); - let word_start = find_word_boundary(slice, pos, Direction::Backward, long); + let word_start = find_word_boundary(slice, pos, Direction::Backward, long, is_subword); let word_end = match slice.get_char(pos).map(categorize_char) { None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, - _ => find_word_boundary(slice, pos + 1, Direction::Forward, long), + _ => find_word_boundary(slice, pos + 1, Direction::Forward, long, is_subword), }; // Special case. @@ -431,7 +441,7 @@ mod test { let (pos, objtype, expected_range) = case; // cursor is a single width selection let range = Range::new(pos, pos + 1); - let result = textobject_word(slice, range, objtype, 1, false); + let result = textobject_word(slice, range, objtype, 1, false, false); assert_eq!( result, expected_range.into(), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ee2949fa0..65a25f4ff 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5567,8 +5567,15 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let selection = doc.selection(view.id).clone().transform(|range| { match ch { - 'w' => textobject::textobject_word(text, range, objtype, count, false), - 'W' => textobject::textobject_word(text, range, objtype, count, true), + 'w' => { + textobject::textobject_word(text, range, objtype, count, false, false) + } + 'W' => { + textobject::textobject_word(text, range, objtype, count, true, false) + } + 's' => { + textobject::textobject_word(text, range, objtype, count, false, true) + } 't' => textobject_treesitter("class", range), 'f' => textobject_treesitter("function", range), 'a' => textobject_treesitter("parameter", range), diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index fcc0333e8..fc8135db8 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1068,7 +1068,7 @@ pub fn rename_symbol(cx: &mut Context) { primary_selection } else { use helix_core::textobject::{textobject_word, TextObject}; - textobject_word(text, primary_selection, TextObject::Inside, 1, false) + textobject_word(text, primary_selection, TextObject::Inside, 1, false, false) } .fragment(text) .into() diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 6ba2fcb9e..609fbbba0 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -586,6 +586,7 @@ impl Component for Prompt { textobject::TextObject::Inside, 1, false, + false, ); let line = text.slice(range.from()..range.to()).to_string(); if !line.is_empty() {