feat: add basic MVP for subword textobject

currently the "outer" variant does not take into account underscores
properly
pull/12026/head
Nikita Revenco 2 weeks ago
parent 173f14e0e5
commit 26438eadb8

@ -14,17 +14,20 @@ use crate::{surround, Syntax};
/// # Arguments /// # Arguments
/// ///
/// * `pos` - index of the character /// * `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( fn find_word_boundary(
slice: RopeSlice, slice: RopeSlice,
mut pos: usize, mut pos: usize,
direction: Direction, direction: Direction,
is_long: bool, long: bool,
is_subword: bool,
) -> usize { ) -> usize {
use CharCategory::{Eol, Whitespace}; use CharCategory::{Eol, Whitespace};
let iter = match direction { let iter = match direction {
// create forward iterator
Direction::Forward => slice.chars_at(pos), Direction::Forward => slice.chars_at(pos),
// create reverse iterator, if we iterate over it we will be advancing in the opposite direction
Direction::Backward => { Direction::Backward => {
let mut iter = slice.chars_at(pos); let mut iter = slice.chars_at(pos);
iter.reverse(); iter.reverse();
@ -32,46 +35,52 @@ fn find_word_boundary(
} }
}; };
// first/last relative to the entire document let mut prev_category = match direction {
let is_first_char = pos == 0; // if we are at the beginning or end of the document
let is_last_char = pos == slice.len_chars(); Direction::Forward if pos == 0 => Whitespace,
Direction::Backward if pos == slice.len_chars() => Whitespace,
// the previous character relative to the direction we are going Direction::Forward => categorize_char(slice.char(pos - 1)),
let prev_char_forward = slice.char(pos - 1); Direction::Backward => categorize_char(slice.char(pos)),
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 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 { for ch in iter {
match categorize_char(ch) { 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, Eol | Whitespace => return pos,
char_category => { // every character other than a whitespace
// compare current char to the previous char, if we are category => {
// iterating forwards e.g.: let matches_short_word = !long
// a_ => true, a and _ are Word chars && !is_subword
// a+ => false, a is Word char, + is a MathSymbol && category != prev_category
let did_category_change = char_category != prev_char_category; && pos != 0
&& pos != slice.len_chars();
if !is_long && !is_subword && did_category_change && !is_first_char && !is_last_char
{ 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; return pos;
} else if is_subword && ch == '_' { } else if matches_short_word {
return pos; return pos;
} else { } else {
match direction { match direction {
Direction::Forward => pos += 1, Direction::Forward => pos += 1,
Direction::Backward => pos = pos.saturating_sub(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, textobject: TextObject,
_count: usize, _count: usize,
long: bool, long: bool,
is_subword: bool,
) -> Range { ) -> Range {
let pos = range.cursor(slice); 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) { let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, 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. // Special case.
@ -431,7 +441,7 @@ mod test {
let (pos, objtype, expected_range) = case; let (pos, objtype, expected_range) = case;
// cursor is a single width selection // cursor is a single width selection
let range = Range::new(pos, pos + 1); 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!( assert_eq!(
result, result,
expected_range.into(), expected_range.into(),

@ -5567,8 +5567,15 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
let selection = doc.selection(view.id).clone().transform(|range| { let selection = doc.selection(view.id).clone().transform(|range| {
match ch { match ch {
'w' => textobject::textobject_word(text, range, objtype, count, false), 'w' => {
'W' => textobject::textobject_word(text, range, objtype, count, true), 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), 't' => textobject_treesitter("class", range),
'f' => textobject_treesitter("function", range), 'f' => textobject_treesitter("function", range),
'a' => textobject_treesitter("parameter", range), 'a' => textobject_treesitter("parameter", range),

@ -1068,7 +1068,7 @@ pub fn rename_symbol(cx: &mut Context) {
primary_selection primary_selection
} else { } else {
use helix_core::textobject::{textobject_word, TextObject}; 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) .fragment(text)
.into() .into()

@ -586,6 +586,7 @@ impl Component for Prompt {
textobject::TextObject::Inside, textobject::TextObject::Inside,
1, 1,
false, false,
false,
); );
let line = text.slice(range.from()..range.to()).to_string(); let line = text.slice(range.from()..range.to()).to_string();
if !line.is_empty() { if !line.is_empty() {

Loading…
Cancel
Save