diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index d17281e6bee0b3bb9ffdd812da982153840d6f7d..92ed6fee39ccafdbcc24a099eeffb4182e05b18d 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1715,18 +1715,14 @@ pub(crate) fn next_word_start( point } -pub(crate) fn next_word_end( +fn next_end_impl( map: &DisplaySnapshot, mut point: DisplayPoint, - ignore_punctuation: bool, times: usize, allow_cross_newline: bool, always_advance: bool, + mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); for _ in 0..times { let mut need_next_char = false; let new_point = if always_advance { @@ -1739,8 +1735,6 @@ pub(crate) fn next_word_end( new_point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); let at_newline = right == '\n'; if !allow_cross_newline && at_newline { @@ -1748,7 +1742,7 @@ pub(crate) fn next_word_end( return true; } - left_kind != right_kind && left_kind != CharKind::Whitespace + is_boundary(left, right) }, ); let new_point = if need_next_char { @@ -1765,6 +1759,64 @@ pub(crate) fn next_word_end( point } +pub(crate) fn next_word_end( + map: &DisplaySnapshot, + point: DisplayPoint, + ignore_punctuation: bool, + times: usize, + allow_cross_newline: bool, + always_advance: bool, +) -> DisplayPoint { + let classifier = map + .buffer_snapshot() + .char_classifier_at(point.to_point(map)) + .ignore_punctuation(ignore_punctuation); + + next_end_impl( + map, + point, + times, + allow_cross_newline, + always_advance, + |left, right| { + let left_kind = classifier.kind(left); + let right_kind = classifier.kind(right); + left_kind != right_kind && left_kind != CharKind::Whitespace + }, + ) +} + +pub(crate) fn next_subword_end( + map: &DisplaySnapshot, + point: DisplayPoint, + ignore_punctuation: bool, + times: usize, + allow_cross_newline: bool, +) -> DisplayPoint { + let classifier = map + .buffer_snapshot() + .char_classifier_at(point.to_point(map)) + .ignore_punctuation(ignore_punctuation); + + next_end_impl( + map, + point, + times, + allow_cross_newline, + true, + |left, right| { + let left_kind = classifier.kind(left); + let right_kind = classifier.kind(right); + let is_stopping_punct = |c: char| ".\"'{}[]()<>".contains(c); + let found_subword_end = is_subword_end(left, right, "_-"); + let is_word_end = (left_kind != right_kind) + && (!left.is_ascii_punctuation() || is_stopping_punct(left)); + + !left.is_whitespace() && (is_word_end || found_subword_end) + }, + ) +} + fn previous_word_start( map: &DisplaySnapshot, mut point: DisplayPoint, @@ -1870,11 +1922,10 @@ fn next_subword_start( let left_kind = classifier.kind(left); let right_kind = classifier.kind(right); let at_newline = right == '\n'; - let is_stopping_punct = |c: char| "\"'{}[]()<>".contains(c); + let found_subword_start = is_subword_start(left, right, "._-"); let is_word_start = (left_kind != right_kind) && (!right.is_ascii_punctuation() || is_stopping_punct(right)); - let found_subword_start = is_subword_start(left, right, "._-"); let found = (!right.is_whitespace() && (is_word_start || found_subword_start)) || at_newline && crossed_newline || at_newline && left == '\n'; // Prevents skipping repeated empty lines @@ -1890,60 +1941,6 @@ fn next_subword_start( point } -pub(crate) fn next_subword_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, - allow_cross_newline: bool, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let new_point = next_char(map, point, allow_cross_newline); - - let mut crossed_newline = false; - let mut need_backtrack = false; - let new_point = - movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - if !allow_cross_newline && at_newline { - return true; - } - - let is_stopping_punct = |c: char| ".\"'{}[]()<>".contains(c); - let is_word_end = (left_kind != right_kind) - && (!left.is_ascii_punctuation() || is_stopping_punct(left)); - let found_subword_end = is_subword_end(left, right, "_-"); - - let found = - !left.is_whitespace() && !at_newline && (is_word_end || found_subword_end); - - if found { - need_backtrack = true; - } - - crossed_newline |= at_newline; - found - }); - let mut new_point = map.clip_point(new_point, Bias::Left); - if need_backtrack { - *new_point.column_mut() -= 1; - } - let new_point = map.clip_point(new_point, Bias::Left); - if point == new_point { - break; - } - point = new_point; - } - point -} - fn previous_subword_start( map: &DisplaySnapshot, mut point: DisplayPoint, diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 699a920704a97a444380ce21a09f8fbff31f7029..27662fc20c28bb983d16d9824aeaaa85e42af975 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1950,6 +1950,19 @@ mod test { cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"}); + // Subword end should stop at EOL + cx.assert_binding_normal("e", indoc! {"foo_bˇar\nbaz"}, indoc! {"foo_baˇr\nbaz"}); + + // Already at subword end, should move to next subword on next line + cx.assert_binding_normal( + "e", + indoc! {"foo_barˇ\nbaz_qux"}, + indoc! {"foo_bar\nbaˇz_qux"}, + ); + + // CamelCase at EOL + cx.assert_binding_normal("e", indoc! {"fooˇBar\nbaz"}, indoc! {"fooBaˇr\nbaz"}); + cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"}); cx.assert_binding_normal(