vim: Allow trailing whitespace for `:norm` command (#46403)

AidanV and dino created

Fix a bug with `:norm` that disallowed trailing whitespace.

Commands that accept generic args (defined with `args()`) now preserve
trailing whitespace, while commands that accept filenames (defined with
`filename()`) have whitespace pre-trimmed. This allows, for example, 
`:norm I  ` to correctly insert spaces, matching NeoVim's behavior.

Release Notes:

- vim: Fixed `:norm` command to preserve trailing whitespace in
arguments (e.g., `:norm I ` now correctly inserts two spaces)

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>

Change summary

crates/vim/src/command.rs                     | 33 ++++++++++++++++++--
crates/vim/test_data/test_normal_command.json | 14 ++++++++
2 files changed, 43 insertions(+), 4 deletions(-)

Detailed changes

crates/vim/src/command.rs 🔗

@@ -1020,6 +1020,7 @@ impl VimCommand {
         self
     }
 
+    /// Set argument handler. Trailing whitespace in arguments will be preserved.
     fn args(
         mut self,
         f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
@@ -1028,6 +1029,8 @@ impl VimCommand {
         self
     }
 
+    /// Set argument handler. Trailing whitespace in arguments will be trimmed.
+    /// Supports filename autocompletion.
     fn filename(
         mut self,
         f: impl Fn(Box<dyn Action>, String) -> Option<Box<dyn Action>> + Send + Sync + 'static,
@@ -1139,11 +1142,11 @@ impl VimCommand {
         let has_bang = rest.starts_with('!');
         let has_space = rest.starts_with("! ") || rest.starts_with(' ');
         let args = if has_bang {
-            rest.strip_prefix('!')?.trim().to_string()
+            rest.strip_prefix('!')?.trim_start().to_string()
         } else if rest.is_empty() {
             "".into()
         } else {
-            rest.strip_prefix(' ')?.trim().to_string()
+            rest.strip_prefix(' ')?.trim_start().to_string()
         };
         Some(ParsedQuery {
             args,
@@ -1173,10 +1176,13 @@ impl VimCommand {
             return None;
         };
 
+        // If the command does not accept args and we have args, we should do no
+        // action.
         let action = if args.is_empty() {
             action
+        } else if self.has_filename {
+            self.args.as_ref()?(action, args.trim().into())?
         } else {
-            // if command does not accept args and we have args then we should do no action
             self.args.as_ref()?(action, args)?
         };
 
@@ -1812,7 +1818,7 @@ pub fn command_interceptor(
     let (range, query) = VimCommand::parse_range(input);
     let range_prefix = input[0..(input.len() - query.len())].to_string();
     let has_trailing_space = query.ends_with(" ");
-    let mut query = query.as_str().trim();
+    let mut query = query.as_str().trim_start();
 
     let on_matching_lines = (query.starts_with('g') || query.starts_with('v'))
         .then(|| {
@@ -3197,6 +3203,25 @@ mod test {
             the lazy dog
         "});
 
+        cx.set_shared_state(indoc! {"
+            ˇquick
+            brown fox
+            jumps over
+            the lazy dog
+        "})
+            .await;
+
+        cx.simulate_shared_keystrokes(": n o r m space I T h e space")
+            .await;
+        cx.simulate_shared_keystrokes("enter").await;
+
+        cx.shared_state().await.assert_eq(indoc! {"
+            Theˇ quick
+            brown fox
+            jumps over
+            the lazy dog
+        "});
+
         // Once ctrl-v to input character literals is added there should be a test for redo
     }
 

crates/vim/test_data/test_normal_command.json 🔗

@@ -76,3 +76,17 @@
 {"Key":"enter"}
 {"Key":"u"}
 {"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}}
+{"Put":{"state":"ˇquick\nbrown fox\njumps over\nthe lazy dog\n"}}
+{"Key":":"}
+{"Key":"n"}
+{"Key":"o"}
+{"Key":"r"}
+{"Key":"m"}
+{"Key":"space"}
+{"Key":"I"}
+{"Key":"T"}
+{"Key":"h"}
+{"Key":"e"}
+{"Key":"space"}
+{"Key":"enter"}
+{"Get":{"state":"Theˇ quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}}