diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 66a304f6c2d8e..a016bccbad0b8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -190,12 +190,21 @@ impl MappableCommand { jobs: cx.jobs, scroll: None, }; - if let Err(e) = typed::process_cmd( - &mut cx, - &format!("{} {}", name, args.join(" ")), - PromptEvent::Validate, - ) { - cx.editor.set_error(format!("{}", e)); + if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { + let args = args.join(" "); + let args = match typed::expand_args(cx.editor, &args) { + Ok(a) => a, + Err(e) => { + cx.editor.set_error(format!("{}", e)); + return; + } + }; + + let args: Vec> = args.split_whitespace().map(Cow::from).collect(); + + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { + cx.editor.set_error(format!("{}", e)); + } } } Self::Static { fun, .. } => (fun)(cx), diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 9bdcfb71cf785..f68edcb0f9205 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2204,16 +2204,10 @@ pub fn process_cmd( input: &str, event: PromptEvent, ) -> anyhow::Result<()> { - let input: String = if event == PromptEvent::Validate { - match expand_args(cx.editor, input) { - Ok(expanded) => expanded, - Err(e) => { - cx.editor.set_error(format!("{e}")); - return Err(e); - } - } + let input: Cow = if event == PromptEvent::Validate { + expand_args(cx.editor, input)? } else { - input.to_owned() + Cow::Borrowed(input) }; let parts = input.split_whitespace().collect::>(); @@ -2905,25 +2899,24 @@ pub(super) fn command_mode(cx: &mut Context) { cx.push_layer(Box::new(prompt)); } -fn expand_args(editor: &Editor, args: &str) -> anyhow::Result { - let regexp = regex::Regex::new(r"%(\w+)\s*\{([^{}]*(\{[^{}]*\}[^{}]*)*)\}").unwrap(); +static EXPAND_ARGS_REGEXP: Lazy = + Lazy::new(|| Regex::new(r"%(\w+)\{([^{}]*(\{[^{}]*\}[^{}]*)*)\}").unwrap()); +pub fn expand_args<'a>(editor: &Editor, args: &'a str) -> anyhow::Result> { let view = editor.tree.get(editor.tree.focus); let doc = editor.documents.get(&view.doc).unwrap(); let shell = &editor.config().shell; - replace_all(®exp, args, move |captures| { - let keyword = captures.get(1).unwrap().as_str(); - let body = captures.get(2).unwrap().as_str(); - - match keyword.trim() { + replace_all( + Lazy::force(&EXPAND_ARGS_REGEXP), + Cow::Borrowed(args), + move |keyword, body| match keyword.trim() { "val" => match body.trim() { - "filename" => doc - .path() - .and_then(|p| p.to_str()) - .map_or(Err(anyhow::anyhow!("Current buffer has no path")), |v| { - Ok(v.to_owned()) - }), + "filename" => Ok((match doc.path() { + Some(p) => p.to_str().unwrap(), + None => SCRATCH_BUFFER_NAME, + }) + .to_owned()), "filedir" => doc .path() .and_then(|p| p.parent()) @@ -2946,20 +2939,20 @@ fn expand_args(editor: &Editor, args: &str) -> anyhow::Result { Ok(result.0.trim().to_string()) } _ => anyhow::bail!("Unknown keyword {keyword}"), - } - }) + }, + ) } // Copy of regex::Regex::replace_all to allow using result in the replacer function -fn replace_all( +fn replace_all<'a>( regex: ®ex::Regex, - text: &str, - matcher: impl Fn(®ex::Captures) -> anyhow::Result, -) -> anyhow::Result { - let mut it = regex.captures_iter(text).peekable(); + text: Cow<'a, str>, + matcher: impl Fn(&str, &str) -> anyhow::Result, +) -> anyhow::Result> { + let mut it = regex.captures_iter(&text).peekable(); if it.peek().is_none() { - return Ok(String::from(text)); + return Ok(text); } let mut new = String::with_capacity(text.len()); @@ -2969,7 +2962,7 @@ fn replace_all( let m = cap.get(0).unwrap(); new.push_str(&text[last_match..m.start()]); - let replace = matcher(&cap)?; + let replace = matcher(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str())?; new.push_str(&replace); @@ -2978,7 +2971,7 @@ fn replace_all( new.push_str(&text[last_match..]); - replace_all(regex, &new, matcher) + replace_all(regex, Cow::Owned(new), matcher) } fn argument_number_of(shellwords: &Shellwords) -> usize {