diff --git a/crates/vim/src/normal/select.rs b/crates/vim/src/normal/select.rs index 9b20e59faf..56f1cfdaaf 100644 --- a/crates/vim/src/normal/select.rs +++ b/crates/vim/src/normal/select.rs @@ -16,7 +16,7 @@ impl Vim { self.update_editor(window, cx, |_, editor, window, cx| { editor.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { - let Some(range) = object.range(map, selection.clone(), around, None) else { + let Some(range) = object.helix_range(map, selection.clone(), around) else { return; }; diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 93a19b9158..fdc4357ab2 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -714,6 +714,27 @@ impl Object { } } + /// Returns the range the object spans if the cursor is over it. + /// Follows helix convention. + pub fn helix_range( + self, + map: &DisplaySnapshot, + selection: Selection, + around: bool, + ) -> Option> { + let relative_to = selection.head(); + match self { + Object::Word { ignore_punctuation } => { + if around { + helix_around_word(map, relative_to, ignore_punctuation) + } else { + helix_in_word(map, relative_to, ignore_punctuation) + } + } + _ => self.range(map, selection, around, None), + } + } + pub fn expand_selection( self, map: &DisplaySnapshot, @@ -759,6 +780,42 @@ fn in_word( Some(start..end) } +/// Returns a range that surrounds the word `relative_to` is in. +/// +/// If `relative_to` is between words, return `None`. +fn helix_in_word( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + ignore_punctuation: bool, +) -> Option> { + // Use motion::right so that we consider the character under the cursor when looking for the start + let classifier = map + .buffer_snapshot + .char_classifier_at(relative_to.to_point(map)) + .ignore_punctuation(ignore_punctuation); + let char = map + .buffer_chars_at(relative_to.to_offset(map, Bias::Left)) + .next()? + .0; + + if classifier.kind(char) == CharKind::Whitespace { + return None; + } + + let start = movement::find_preceding_boundary_display_point( + map, + right(map, relative_to, 1), + movement::FindRange::SingleLine, + |left, right| classifier.kind(left) != classifier.kind(right), + ); + + let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { + classifier.kind(left) != classifier.kind(right) + }); + + Some(start..end) +} + fn in_subword( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -927,6 +984,18 @@ fn around_word( } } +/// Returns the range of the word the cursor is over and all the whitespace on one side. +/// If there is whitespace after that is included, otherwise it's whitespace before the word if any. +fn helix_around_word( + map: &DisplaySnapshot, + relative_to: DisplayPoint, + ignore_punctuation: bool, +) -> Option> { + let word_range = helix_in_word(map, relative_to, ignore_punctuation)?; + + Some(expand_to_include_whitespace(map, word_range, true)) +} + fn around_subword( map: &DisplaySnapshot, relative_to: DisplayPoint,