feat(vim): allow ignoring whitespace in around motion

dino created

- Update `PushObject` with `whitespace: bool` field to control whether,
when `around: true`, whitespace should be included in the selection
(vim's default) or not (neovim's default).
- Update `Object` with a `whitespace: bool` field, reflecting the
changes in `PushObject`.
- Update `vim::normal::delete::Vim.delete_object` to correctly leverage
this new `whitespace` value. This is meant as a proof of concept and
we'd need to implement this for other operations like, for example, the
change operator.

Change summary

crates/vim/src/command.rs                |   2 
crates/vim/src/helix/select.rs           |   3 
crates/vim/src/indent.rs                 |   2 
crates/vim/src/normal.rs                 |   6 
crates/vim/src/normal/change.rs          |   3 
crates/vim/src/normal/convert.rs         |   2 
crates/vim/src/normal/delete.rs          |   3 
crates/vim/src/normal/paste.rs           |   2 
crates/vim/src/normal/toggle_comments.rs |   2 
crates/vim/src/normal/yank.rs            |   2 
crates/vim/src/object.rs                 | 127 ++++++++++++++++++++-----
crates/vim/src/replace.rs                |   2 
crates/vim/src/rewrap.rs                 |   2 
crates/vim/src/state.rs                  |   5 
crates/vim/src/surrounds.rs              |   9 +
crates/vim/src/vim.rs                    |   4 
crates/vim/src/visual.rs                 |  14 +-
17 files changed, 138 insertions(+), 52 deletions(-)

Detailed changes

crates/vim/src/command.rs 🔗

@@ -2057,7 +2057,7 @@ impl Vim {
                 .selections
                 .newest_display(&editor.display_snapshot(cx));
             let range = object
-                .range(&snapshot, start.clone(), around, None)
+                .range(&snapshot, start.clone(), around, true, None)
                 .unwrap_or(start.range());
             if range.start != start.start {
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {

crates/vim/src/helix/select.rs 🔗

@@ -20,7 +20,8 @@ impl Vim {
                     let Some(range) = object
                         .helix_range(map, selection.clone(), around)
                         .unwrap_or({
-                            let vim_range = object.range(map, selection.clone(), around, None);
+                            let vim_range =
+                                object.range(map, selection.clone(), around, true, None);
                             vim_range.filter(|r| r.start <= cursor_range(selection, map).start)
                         })
                     else {

crates/vim/src/indent.rs 🔗

@@ -150,7 +150,7 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
                         original_positions.insert(selection.id, anchor);
-                        object.expand_selection(map, selection, around, times);
+                        object.expand_selection(map, selection, around, true, times);
                     });
                 });
                 match dir {

crates/vim/src/normal.rs 🔗

@@ -454,9 +454,11 @@ impl Vim {
     ) {
         let mut waiting_operator: Option<Operator> = None;
         match self.maybe_pop_operator() {
-            Some(Operator::Object { around }) => match self.maybe_pop_operator() {
+            Some(Operator::Object { around, whitespace }) => match self.maybe_pop_operator() {
                 Some(Operator::Change) => self.change_object(object, around, times, window, cx),
-                Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
+                Some(Operator::Delete) => {
+                    self.delete_object(object, around, whitespace, times, window, cx)
+                }
                 Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
                 Some(Operator::Indent) => {
                     self.indent_object(object, around, IndentDirection::In, times, window, cx)

crates/vim/src/normal/change.rs 🔗

@@ -117,7 +117,8 @@ impl Vim {
             editor.transact(window, cx, |editor, window, cx| {
                 editor.change_selections(Default::default(), window, cx, |s| {
                     s.move_with(|map, selection| {
-                        objects_found |= object.expand_selection(map, selection, around, times);
+                        objects_found |=
+                            object.expand_selection(map, selection, around, true, times);
                     });
                 });
                 if objects_found {

crates/vim/src/normal/convert.rs 🔗

@@ -93,7 +93,7 @@ impl Vim {
                 let mut original_positions: HashMap<_, _> = Default::default();
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                     s.move_with(|map, selection| {
-                        object.expand_selection(map, selection, around, times);
+                        object.expand_selection(map, selection, around, true, times);
                         original_positions.insert(
                             selection.id,
                             map.display_point_to_anchor(selection.start, Bias::Left),

crates/vim/src/normal/delete.rs 🔗

@@ -93,6 +93,7 @@ impl Vim {
         &mut self,
         object: Object,
         around: bool,
+        whitespace: bool,
         times: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -118,7 +119,7 @@ impl Vim {
                             column_before_move.insert(selection.id, cursor_point.column);
                         }
 
-                        object.expand_selection(map, selection, around, times);
+                        object.expand_selection(map, selection, around, whitespace, times);
                         let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
                         let mut move_selection_start_to_previous_line =
                             |map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {

crates/vim/src/normal/paste.rs 🔗

@@ -244,7 +244,7 @@ impl Vim {
                 editor.set_clip_at_line_ends(false, cx);
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                     s.move_with(|map, selection| {
-                        object.expand_selection(map, selection, around, None);
+                        object.expand_selection(map, selection, around, true, None);
                     });
                 });
 

crates/vim/src/normal/toggle_comments.rs 🔗

@@ -58,7 +58,7 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
                         original_positions.insert(selection.id, anchor);
-                        object.expand_selection(map, selection, around, times);
+                        object.expand_selection(map, selection, around, true, times);
                     });
                 });
                 editor.toggle_comments(&Default::default(), window, cx);

crates/vim/src/normal/yank.rs 🔗

@@ -76,7 +76,7 @@ impl Vim {
                 let mut start_positions: HashMap<_, _> = Default::default();
                 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
                     s.move_with(|map, selection| {
-                        object.expand_selection(map, selection, around, times);
+                        object.expand_selection(map, selection, around, true, times);
                         let start_position = (selection.start, selection.goal);
                         start_positions.insert(selection.id, start_position);
                     });

crates/vim/src/object.rs 🔗

@@ -406,7 +406,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
     });
     Vim::action(editor, cx, |vim, _: &Comment, window, cx| {
         if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
-            vim.push_operator(Operator::Object { around: true }, window, cx);
+            vim.push_operator(
+                Operator::Object {
+                    around: true,
+                    whitespace: true,
+                },
+                window,
+                cx,
+            );
         }
         vim.object(Object::Comment, window, cx)
     });
@@ -547,6 +554,7 @@ impl Object {
         map: &DisplaySnapshot,
         selection: Selection<DisplayPoint>,
         around: bool,
+        whitespace: bool,
         times: Option<usize>,
     ) -> Option<Range<DisplayPoint>> {
         let relative_to = selection.head();
@@ -568,12 +576,24 @@ impl Object {
             Object::Sentence => sentence(map, relative_to, around),
             //change others later
             Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
-            Object::Quotes => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
-            }
-            Object::BackQuotes => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`')
-            }
+            Object::Quotes => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '\'',
+                '\'',
+            ),
+            Object::BackQuotes => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '`',
+                '`',
+            ),
             Object::AnyQuotes => {
                 let quote_types = ['\'', '"', '`'];
                 let cursor_offset = relative_to.to_offset(map, Bias::Left);
@@ -588,6 +608,7 @@ impl Object {
                         map,
                         relative_to,
                         around,
+                        whitespace,
                         self.is_multiline(),
                         quote,
                         quote,
@@ -617,6 +638,7 @@ impl Object {
                             map,
                             relative_to,
                             around,
+                            whitespace,
                             self.is_multiline(),
                             quote,
                             quote,
@@ -635,15 +657,33 @@ impl Object {
                     })
             }
             Object::MiniQuotes => find_mini_quotes(map, relative_to, around),
-            Object::DoubleQuotes => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"')
-            }
-            Object::VerticalBars => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|')
-            }
-            Object::Parentheses => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')')
-            }
+            Object::DoubleQuotes => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '"',
+                '"',
+            ),
+            Object::VerticalBars => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '|',
+                '|',
+            ),
+            Object::Parentheses => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '(',
+                ')',
+            ),
             Object::Tag => {
                 let head = selection.head();
                 let range = selection.range();
@@ -662,6 +702,7 @@ impl Object {
                         map,
                         relative_to,
                         around,
+                        whitespace,
                         self.is_multiline(),
                         open,
                         close,
@@ -691,6 +732,7 @@ impl Object {
                             map,
                             relative_to,
                             around,
+                            whitespace,
                             self.is_multiline(),
                             open,
                             close,
@@ -709,15 +751,33 @@ impl Object {
                     })
             }
             Object::MiniBrackets => find_mini_brackets(map, relative_to, around),
-            Object::SquareBrackets => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']')
-            }
-            Object::CurlyBrackets => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}')
-            }
-            Object::AngleBrackets => {
-                surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>')
-            }
+            Object::SquareBrackets => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '[',
+                ']',
+            ),
+            Object::CurlyBrackets => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '{',
+                '}',
+            ),
+            Object::AngleBrackets => surrounding_markers(
+                map,
+                relative_to,
+                around,
+                whitespace,
+                self.is_multiline(),
+                '<',
+                '>',
+            ),
             Object::Method => text_object(
                 map,
                 relative_to,
@@ -756,9 +816,10 @@ impl Object {
         map: &DisplaySnapshot,
         selection: &mut Selection<DisplayPoint>,
         around: bool,
+        whitespace: bool,
         times: Option<usize>,
     ) -> bool {
-        if let Some(range) = self.range(map, selection.clone(), around, times) {
+        if let Some(range) = self.range(map, selection.clone(), around, whitespace, times) {
             selection.start = range.start;
             selection.end = range.end;
             true
@@ -1559,6 +1620,7 @@ pub fn surrounding_markers(
     map: &DisplaySnapshot,
     relative_to: DisplayPoint,
     around: bool,
+    whitespace: bool,
     search_across_lines: bool,
     open_marker: char,
     close_marker: char,
@@ -1669,7 +1731,12 @@ pub fn surrounding_markers(
         for (ch, range) in movement::chars_after(map, closing.end) {
             if ch.is_whitespace() && ch != '\n' {
                 found = true;
-                closing.end = range.end;
+
+                // Only update closing range's `end` value if whitespace is
+                // meant to be included.
+                if whitespace {
+                    closing.end = range.end;
+                }
             } else {
                 break;
             }
@@ -1678,7 +1745,11 @@ pub fn surrounding_markers(
         if !found {
             for (ch, range) in movement::chars_before(map, opening.start) {
                 if ch.is_whitespace() && ch != '\n' {
-                    opening.start = range.start
+                    // Only update closing range's `start` value if whitespace
+                    // is meant to be included.
+                    if whitespace {
+                        opening.start = range.start
+                    }
                 } else {
                     break;
                 }

crates/vim/src/replace.rs 🔗

@@ -154,7 +154,7 @@ impl Vim {
                 .selections
                 .newest_display(&editor.display_snapshot(cx));
             let snapshot = editor.snapshot(window, cx);
-            object.expand_selection(&snapshot, &mut selection, around, None);
+            object.expand_selection(&snapshot, &mut selection, around, true, None);
             let start = snapshot
                 .buffer_snapshot()
                 .anchor_before(selection.start.to_point(&snapshot));

crates/vim/src/rewrap.rs 🔗

@@ -107,7 +107,7 @@ impl Vim {
                     s.move_with(|map, selection| {
                         let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
                         original_positions.insert(selection.id, anchor);
-                        object.expand_selection(map, selection, around, times);
+                        object.expand_selection(map, selection, around, true, times);
                     });
                 });
                 editor.rewrap_impl(

crates/vim/src/state.rs 🔗

@@ -88,6 +88,7 @@ pub enum Operator {
     Replace,
     Object {
         around: bool,
+        whitespace: bool,
     },
     FindForward {
         before: bool,
@@ -996,8 +997,8 @@ pub struct SearchState {
 impl Operator {
     pub fn id(&self) -> &'static str {
         match self {
-            Operator::Object { around: false } => "i",
-            Operator::Object { around: true } => "a",
+            Operator::Object { around: false, .. } => "i",
+            Operator::Object { around: true, .. } => "a",
             Operator::Change => "c",
             Operator::Delete => "d",
             Operator::Yank => "y",

crates/vim/src/surrounds.rs 🔗

@@ -53,7 +53,7 @@ impl Vim {
                 for selection in &display_selections {
                     let range = match &target {
                         SurroundsType::Object(object, around) => {
-                            object.range(&display_map, selection.clone(), *around, None)
+                            object.range(&display_map, selection.clone(), *around, true, None)
                         }
                         SurroundsType::Motion(motion) => {
                             motion
@@ -153,7 +153,7 @@ impl Vim {
                 for selection in &display_selections {
                     let start = selection.start.to_offset(&display_map, Bias::Left);
                     if let Some(range) =
-                        pair_object.range(&display_map, selection.clone(), true, None)
+                        pair_object.range(&display_map, selection.clone(), true, true, None)
                     {
                         // If the current parenthesis object is single-line,
                         // then we need to filter whether it is the current line or not
@@ -266,7 +266,7 @@ impl Vim {
                     for selection in &selections {
                         let start = selection.start.to_offset(&display_map, Bias::Left);
                         if let Some(range) =
-                            target.range(&display_map, selection.clone(), true, None)
+                            target.range(&display_map, selection.clone(), true, true, None)
                         {
                             if !target.is_multiline() {
                                 let is_same_row = selection.start.row() == range.start.row()
@@ -392,7 +392,7 @@ impl Vim {
                     for selection in &selections {
                         let start = selection.start.to_offset(&display_map, Bias::Left);
                         if let Some(range) =
-                            object.range(&display_map, selection.clone(), true, None)
+                            object.range(&display_map, selection.clone(), true, true, None)
                         {
                             // If the current parenthesis object is single-line,
                             // then we need to filter whether it is the current line or not
@@ -534,6 +534,7 @@ impl Vim {
                                 &display_map,
                                 relative_to,
                                 true,
+                                true,
                                 false,
                                 open,
                                 close,

crates/vim/src/vim.rs 🔗

@@ -47,6 +47,7 @@ use std::{mem, ops::Range, sync::Arc};
 use surrounds::SurroundsType;
 use theme::ThemeSettings;
 use ui::{IntoElement, SharedString, px};
+use util::serde::default_true;
 use vim_mode_setting::HelixModeSetting;
 use vim_mode_setting::VimModeSetting;
 use workspace::{self, Pane, Workspace};
@@ -71,6 +72,8 @@ struct SelectRegister(String);
 #[serde(deny_unknown_fields)]
 struct PushObject {
     around: bool,
+    #[serde(default = "default_true")]
+    whitespace: bool,
 }
 
 #[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
@@ -662,6 +665,7 @@ impl Vim {
                 vim.push_operator(
                     Operator::Object {
                         around: action.around,
+                        whitespace: action.whitespace,
                     },
                     window,
                     cx,

crates/vim/src/visual.rs 🔗

@@ -426,7 +426,7 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Vim>,
     ) {
-        if let Some(Operator::Object { around }) = self.active_operator() {
+        if let Some(Operator::Object { around, .. }) = self.active_operator() {
             self.pop_operator(window, cx);
             let current_mode = self.mode;
             let target_mode = object.target_visual_mode(current_mode, around);
@@ -453,7 +453,7 @@ impl Vim {
 
                         let original_point = selection.tail().to_point(map);
 
-                        if let Some(range) = object.range(map, mut_selection, around, count) {
+                        if let Some(range) = object.range(map, mut_selection, around, true, count) {
                             if !range.is_empty() {
                                 let expand_both_ways = object.always_expands_both_ways()
                                     || selection.is_empty()
@@ -464,9 +464,13 @@ impl Vim {
                                         && selection.end == range.end
                                         && object.always_expands_both_ways()
                                     {
-                                        if let Some(range) =
-                                            object.range(map, selection.clone(), around, count)
-                                        {
+                                        if let Some(range) = object.range(
+                                            map,
+                                            selection.clone(),
+                                            around,
+                                            true,
+                                            count,
+                                        ) {
                                             selection.start = range.start;
                                             selection.end = range.end;
                                         }