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 <dinojoaocosta@gmail.com>
This commit is contained in:
AidanV 2026-01-13 09:13:15 -05:00 committed by GitHub
parent 4e368d485c
commit d2b31b47b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 43 additions and 4 deletions

View file

@ -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
}

View file

@ -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"}}