From 562b8589e767384c70b7a33ff173770e4fb73ca5 Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 27 Oct 2025 14:19:29 +0000 Subject: [PATCH] feat(vim): allow ignoring whitespace in around motion - 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. --- 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(-) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 9dc4ec999a47e6a0e8ab802761cab474ef81499b..c6be586998fe136c7e6290022b3b036797aa3c7a 100644 --- a/crates/vim/src/command.rs +++ b/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| { diff --git a/crates/vim/src/helix/select.rs b/crates/vim/src/helix/select.rs index d782e8b4505691060b0a0898f9a71047ed7956cf..a230d757c046de7eb5e2fccd6fa6b1b61c6a1649 100644 --- a/crates/vim/src/helix/select.rs +++ b/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 { diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index 927edf4d9aa01502fd2112c1cb5b3fb5af12145f..60ae03214117a4cab7232631fcf263ae0dfbdd58 100644 --- a/crates/vim/src/indent.rs +++ b/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 { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index f80f9be38edbb7fafb0864437c8de2bda4740154..f6e148bcaef40fb2d54345ac05f22e46b58a8c1d 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -454,9 +454,11 @@ impl Vim { ) { let mut waiting_operator: Option = 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) diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 4735c64792f3639b2c0d6581e6179484e842f386..fcdcccf50d1c0b64bf202b3dc42523613c46d152 100644 --- a/crates/vim/src/normal/change.rs +++ b/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 { diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index 0ee132a44d20723970fecbbef4cef13ff31e310c..4484f98a4522f92512063aa7eba83c4097f43417 100644 --- a/crates/vim/src/normal/convert.rs +++ b/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), diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index b1c41315a82df7646531c16a5b701d46a8ba82fd..357afd80da2d3e6299e31861f4c50c8705d3d1e2 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -93,6 +93,7 @@ impl Vim { &mut self, object: Object, around: bool, + whitespace: bool, times: Option, window: &mut Window, cx: &mut Context, @@ -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| { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 74a28322d13b6ab0f563e6953f6b1edbfea66740..3c9aa16b8f79dcb958029244ed45e27f6c799e02 100644 --- a/crates/vim/src/normal/paste.rs +++ b/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); }); }); diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index 17c3b2d363e308f2683a48483286a845e0844ccf..53f7a6ee8d84781d088fd6e11a7f3f64466319be 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/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); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index d5a45fca544d61735f62a8f46e849db2c009847f..b6a7094014f788da8df655a46514230bd8208b7f 100644 --- a/crates/vim/src/normal/yank.rs +++ b/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); }); diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 5d0ac722872f3c39067a668c4ed5d56847c61898..f9d6fb8d107449764b8db622c94d65fe746ed5d6 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -406,7 +406,14 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); 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, around: bool, + whitespace: bool, times: Option, ) -> Option> { 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, around: bool, + whitespace: bool, times: Option, ) -> 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; } diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index c9a9fbdb9ee3428ce80c934a686a73a63ddee714..8141465dbc1b8d6d545c72e2bfd73c642728d313 100644 --- a/crates/vim/src/replace.rs +++ b/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)); diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs index 85e1967af040856f4ba01a2e604c3e637e9b1f2c..1c885319016ad89552a94407157b66443604231f 100644 --- a/crates/vim/src/rewrap.rs +++ b/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( diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 959edff63dd50fa549edcbae1bea213224b923af..1cf777b9319d94e6f309b25c456ad6d2a9e024ce 100644 --- a/crates/vim/src/state.rs +++ b/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", diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index bc817e2d4871a0be07e8c100b332f5630dcec711..68e5035569ffbc1e6d9db79961a994266c37a40b 100644 --- a/crates/vim/src/surrounds.rs +++ b/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, diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 7481d176109907baccf6e742d0b3f3614014dcac..216f63d31ca3268092aa6e702f823590ce69016f 100644 --- a/crates/vim/src/vim.rs +++ b/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, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 59555205d9862e51c2778eec1f321338fd5e7569..74895d13edfe24edf623007ca0fbed3726d180e9 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -426,7 +426,7 @@ impl Vim { window: &mut Window, cx: &mut Context, ) { - 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; }