diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b55f1ab72bd7..2ffb2f0bcf4a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -185,16 +185,17 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { - let args: Vec> = args.iter().map(Cow::from).collect(); - if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) { - let mut cx = compositor::Context { - editor: cx.editor, - jobs: cx.jobs, - scroll: None, - }; - if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { - cx.editor.set_error(format!("{}", e)); - } + let mut cx = compositor::Context { + editor: cx.editor, + 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)); } } Self::Static { fun, .. } => (fun)(cx), diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index afc3d7069894..842cf97d1b32 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2165,6 +2165,42 @@ fn reset_diff_change( Ok(()) } +pub fn process_cmd( + cx: &mut compositor::Context, + input: &str, + event: PromptEvent, +) -> anyhow::Result<()> { + let input = expand_args(cx.editor, input); + let parts = input.split_whitespace().collect::>(); + if parts.is_empty() { + return Ok(()); + } + + // If command is numeric, interpret as line number and go there. + if parts.len() == 1 && parts[0].parse::().ok().is_some() { + if let Err(e) = typed::goto_line_number(cx, &[Cow::from(parts[0])], event) { + cx.editor.set_error(format!("{}", e)); + return Err(e); + } + return Ok(()); + } + + // Handle typable commands + if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { + let shellwords = shellwords::Shellwords::from(input.as_ref()); + let args = shellwords.words(); + + if let Err(e) = (cmd.fun)(cx, &args[1..], event) { + cx.editor.set_error(format!("{}", e)); + return Err(e); + } + } else if event == PromptEvent::Validate { + cx.editor + .set_error(format!("no such command: '{}'", parts[0])); + } + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2794,31 +2830,7 @@ pub(super) fn command_mode(cx: &mut Context) { } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let parts = input.split_whitespace().collect::>(); - if parts.is_empty() { - return; - } - - // If command is numeric, interpret as line number and go there. - if parts.len() == 1 && parts[0].parse::().ok().is_some() { - if let Err(e) = typed::goto_line_number(cx, &[Cow::from(parts[0])], event) { - cx.editor.set_error(format!("{}", e)); - } - return; - } - - // Handle typable commands - if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { - let shellwords = Shellwords::from(input); - let args = shellwords.words(); - - if let Err(e) = (cmd.fun)(cx, &args[1..], event) { - cx.editor.set_error(format!("{}", e)); - } - } else if event == PromptEvent::Validate { - cx.editor - .set_error(format!("no such command: '{}'", parts[0])); - } + let _ = process_cmd(cx, input, event); }, ); prompt.doc_fn = Box::new(|input: &str| { @@ -2841,6 +2853,58 @@ pub(super) fn command_mode(cx: &mut Context) { cx.push_layer(Box::new(prompt)); } +fn expand_args<'a>(editor: &mut Editor, args: &'a str) -> Cow<'a, str> { + let reg = Regex::new(r"%(\w+)\s*\{(.*)").unwrap(); + reg.replace(args, |caps: ®ex::Captures| { + let remaining = &caps[2]; + let end = find_first_open_right_braces(remaining); + let exp = expand_args(editor, &remaining[..end]); + let doc = doc!(editor); + let rep = match &caps[1] { + "val" => match exp.trim() { + "filename" => doc.path().and_then(|p| p.to_str()).unwrap_or("").to_owned(), + "dirname" => doc + .path() + .and_then(|p| p.parent()) + .and_then(|p| p.to_str()) + .unwrap_or("") + .to_owned(), + _ => "".into(), + }, + "sh" => { + let shell = &editor.config().shell; + if let Ok((output, _)) = shell_impl(shell, &exp, None) { + output.trim().into() + } else { + "".into() + } + } + _ => "".into(), + }; + let next = expand_args(editor, remaining.get(end + 1..).unwrap_or("")); + format!("{rep} {next}") + }) +} + +fn find_first_open_right_braces(str: &str) -> usize { + let mut left_count = 1; + for (i, &b) in str.as_bytes().iter().enumerate() { + match char::from_u32(b as u32) { + Some('}') => { + left_count -= 1; + if left_count == 0 { + return i; + } + } + Some('{') => { + left_count += 1; + } + _ => {} + } + } + str.len() +} + fn argument_number_of(shellwords: &Shellwords) -> usize { if shellwords.ends_with_whitespace() { shellwords.words().len().saturating_sub(1)