chore(vim): introduce enum to control object scope

dino created

In order to avoid having to pass around the values that determine
whether an operator should be applied to inside the object or include
the object as well as the surrounding whitespace, this commit introduces
a new enum, `vim::state::ObjectScope`, which we'll use to pass this
information around.

As such, `vim::state::Operator::Object` has already been updated so that
it now only has a `scope` field with the `ObejctScope`. Existing
functionality has been migrated so as to calculate the `around` and
`whitespace` values from this scope, but a future commit will update it
so that all dependencies start operating on the `ObjectScope` instead.

Change summary

crates/vim/src/normal.rs | 134 +++++++++++++++++++++++------------------
crates/vim/src/object.rs |   5 
crates/vim/src/state.rs  |  33 +++++++++-
crates/vim/src/vim.rs    |  17 ++--
crates/vim/src/visual.rs |   9 ++
5 files changed, 122 insertions(+), 76 deletions(-)

Detailed changes

crates/vim/src/normal.rs 🔗

@@ -19,7 +19,7 @@ use crate::{
     indent::IndentDirection,
     motion::{self, Motion, first_non_whitespace, next_line_end, right},
     object::Object,
-    state::{Mark, Mode, Operator},
+    state::{Mark, Mode, ObjectScope, Operator},
     surrounds::SurroundsType,
 };
 use collections::BTreeSet;
@@ -454,64 +454,82 @@ impl Vim {
     ) {
         let mut waiting_operator: Option<Operator> = None;
         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, 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)
-                }
-                Some(Operator::Outdent) => {
-                    self.indent_object(object, around, IndentDirection::Out, times, window, cx)
-                }
-                Some(Operator::AutoIndent) => {
-                    self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
-                }
-                Some(Operator::ShellCommand) => {
-                    self.shell_command_object(object, around, window, cx);
-                }
-                Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
-                Some(Operator::Lowercase) => {
-                    self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
-                }
-                Some(Operator::Uppercase) => {
-                    self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
-                }
-                Some(Operator::OppositeCase) => self.convert_object(
-                    object,
-                    around,
-                    ConvertTarget::OppositeCase,
-                    times,
-                    window,
-                    cx,
-                ),
-                Some(Operator::Rot13) => {
-                    self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
-                }
-                Some(Operator::Rot47) => {
-                    self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
-                }
-                Some(Operator::AddSurrounds { target: None }) => {
-                    waiting_operator = Some(Operator::AddSurrounds {
-                        target: Some(SurroundsType::Object(object, around)),
-                    });
-                }
-                Some(Operator::ToggleComments) => {
-                    self.toggle_comments_object(object, around, times, window, cx)
-                }
-                Some(Operator::ReplaceWithRegister) => {
-                    self.replace_with_register_object(object, around, window, cx)
-                }
-                Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
-                Some(Operator::HelixMatch) => {
-                    self.select_current_object(object, around, window, cx)
-                }
-                _ => {
-                    // Can't do anything for namespace operators. Ignoring
+            Some(Operator::Object { scope }) => {
+                let (around, whitespace) = match scope {
+                    ObjectScope::Inside => (false, false),
+                    ObjectScope::Around => (true, true),
+                    ObjectScope::AroundTrimmed => (true, false),
+                };
+
+                match self.maybe_pop_operator() {
+                    Some(Operator::Change) => self.change_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)
+                    }
+                    Some(Operator::Outdent) => {
+                        self.indent_object(object, around, IndentDirection::Out, times, window, cx)
+                    }
+                    Some(Operator::AutoIndent) => {
+                        self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
+                    }
+                    Some(Operator::ShellCommand) => {
+                        self.shell_command_object(object, around, window, cx);
+                    }
+                    Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
+                    Some(Operator::Lowercase) => self.convert_object(
+                        object,
+                        around,
+                        ConvertTarget::LowerCase,
+                        times,
+                        window,
+                        cx,
+                    ),
+                    Some(Operator::Uppercase) => self.convert_object(
+                        object,
+                        around,
+                        ConvertTarget::UpperCase,
+                        times,
+                        window,
+                        cx,
+                    ),
+                    Some(Operator::OppositeCase) => self.convert_object(
+                        object,
+                        around,
+                        ConvertTarget::OppositeCase,
+                        times,
+                        window,
+                        cx,
+                    ),
+                    Some(Operator::Rot13) => {
+                        self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
+                    }
+                    Some(Operator::Rot47) => {
+                        self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
+                    }
+                    Some(Operator::AddSurrounds { target: None }) => {
+                        waiting_operator = Some(Operator::AddSurrounds {
+                            target: Some(SurroundsType::Object(object, around)),
+                        });
+                    }
+                    Some(Operator::ToggleComments) => {
+                        self.toggle_comments_object(object, around, times, window, cx)
+                    }
+                    Some(Operator::ReplaceWithRegister) => {
+                        self.replace_with_register_object(object, around, window, cx)
+                    }
+                    Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
+                    Some(Operator::HelixMatch) => {
+                        self.select_current_object(object, around, window, cx)
+                    }
+                    _ => {
+                        // Can't do anything for namespace operators. Ignoring
+                    }
                 }
-            },
+            }
             Some(Operator::HelixNext { around }) => {
                 self.select_next_object(object, around, window, cx);
             }

crates/vim/src/object.rs 🔗

@@ -3,7 +3,7 @@ use std::ops::Range;
 use crate::{
     Vim,
     motion::right,
-    state::{Mode, Operator},
+    state::{Mode, ObjectScope, Operator},
 };
 use editor::{
     Bias, DisplayPoint, Editor, ToOffset,
@@ -408,8 +408,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
         if !matches!(vim.active_operator(), Some(Operator::Object { .. })) {
             vim.push_operator(
                 Operator::Object {
-                    around: true,
-                    whitespace: true,
+                    scope: ObjectScope::Around,
                 },
                 window,
                 cx,

crates/vim/src/state.rs 🔗

@@ -87,8 +87,7 @@ pub enum Operator {
     Yank,
     Replace,
     Object {
-        around: bool,
-        whitespace: bool,
+        scope: ObjectScope,
     },
     FindForward {
         before: bool,
@@ -150,6 +149,28 @@ pub enum Operator {
     },
 }
 
+/// Controls how the object interacts with its delimiters and the surrounding
+/// whitespace.
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) enum ObjectScope {
+    /// Inside the delimiters, excluding whitespace.
+    ///
+    /// Used by the `i` operator (e.g., `diw` for "delete inner word").
+    /// Selects only the content between delimiters without including
+    /// the delimiters themselves or surrounding whitespace.
+    Inside,
+    /// Around the delimiters, including surrounding whitespace.
+    ///
+    /// Used by the `a` operator (e.g., `daw` for "delete a word").
+    /// Selects the content, the delimiters, and any surrounding whitespace.
+    Around,
+    /// Around the delimiters, excluding surrounding whitespace.
+    ///
+    /// Similar to `Around`, but does not include whitespace adjacent to
+    /// the delimiters.
+    AroundTrimmed,
+}
+
 #[derive(Default, Clone, Debug)]
 pub enum RecordedSelection {
     #[default]
@@ -997,8 +1018,12 @@ 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 {
+                scope: ObjectScope::Inside,
+            } => "i",
+            Operator::Object {
+                scope: ObjectScope::Around | ObjectScope::AroundTrimmed,
+            } => "a",
             Operator::Change => "c",
             Operator::Delete => "d",
             Operator::Yank => "y",

crates/vim/src/vim.rs 🔗

@@ -42,7 +42,7 @@ use serde::Deserialize;
 pub use settings::{
     ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file,
 };
-use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals};
+use state::{Mode, ObjectScope, Operator, RecordedSelection, SearchState, VimGlobals};
 use std::{mem, ops::Range, sync::Arc};
 use surrounds::SurroundsType;
 use theme::ThemeSettings;
@@ -662,14 +662,13 @@ impl Vim {
                 Vim::globals(cx).forced_motion = true;
             });
             Vim::action(editor, cx, |vim, action: &PushObject, window, cx| {
-                vim.push_operator(
-                    Operator::Object {
-                        around: action.around,
-                        whitespace: action.whitespace,
-                    },
-                    window,
-                    cx,
-                )
+                let scope = match (action.around, action.whitespace) {
+                    (false, _) => ObjectScope::Inside,
+                    (true, true) => ObjectScope::Around,
+                    (true, false) => ObjectScope::AroundTrimmed,
+                };
+
+                vim.push_operator(Operator::Object { scope }, window, cx)
             });
 
             Vim::action(editor, cx, |vim, action: &PushFindForward, window, cx| {

crates/vim/src/visual.rs 🔗

@@ -17,7 +17,7 @@ use crate::{
     Vim,
     motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line},
     object::Object,
-    state::{Mark, Mode, Operator},
+    state::{Mark, Mode, ObjectScope, Operator},
 };
 
 actions!(
@@ -426,7 +426,12 @@ impl Vim {
         window: &mut Window,
         cx: &mut Context<Vim>,
     ) {
-        if let Some(Operator::Object { around, .. }) = self.active_operator() {
+        if let Some(Operator::Object { scope }) = self.active_operator() {
+            let around = match scope {
+                ObjectScope::Around | ObjectScope::AroundTrimmed => true,
+                ObjectScope::Inside => false,
+            };
+
             self.pop_operator(window, cx);
             let current_mode = self.mode;
             let target_mode = object.target_visual_mode(current_mode, around);