From d2b31b47b65f84db73becbf4eb4ddcbbb7dd5cdb Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:13:15 -0500 Subject: [PATCH] vim: Allow trailing whitespace for `:norm` command (#46403) 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 --- crates/vim/src/command.rs | 33 ++++++++++++++++--- crates/vim/test_data/test_normal_command.json | 14 ++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 2e639a0dd501d4e85c034f7d1e3a88851a85ff4d..5f53ebaec68a6a1869c9830a493a859ad0aae9de 100644 --- a/crates/vim/src/command.rs +++ b/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, String) -> Option> + 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, String) -> Option> + 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 } diff --git a/crates/vim/test_data/test_normal_command.json b/crates/vim/test_data/test_normal_command.json index 1f248f5e77e37cb9aaf56eb33f1a3cd561fe45bc..634acdea6fc33adeaa08a06f6bd4628df76c1200 100644 --- a/crates/vim/test_data/test_normal_command.json +++ b/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"}}