diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index b7496d338c4e..ac562a8541e9 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -71,3 +71,4 @@ | `:append-output` | Run shell command, appending output after each selection. | | `:pipe` | Pipe each selection to the shell command. | | `:run-shell-command`, `:sh` | Run a shell command | +| `:commands`, `:cmds` | Run commands together, use && to sepearte them | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b00e02b9478b..648eab971aa6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -165,16 +165,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 4bbb20824d50..73401aec0374 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1739,6 +1739,67 @@ fn run_shell_command( Ok(()) } +fn cmds(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let mut start; + let mut end = 0; + loop { + start = if end == 0 { 0 } else { end + 1 }; + end = start + 1; + while end < args.len() { + if args[end] == "&&" { + break; + } + end += 1; + } + + if start >= end || start >= args.len() { + break; + } + process_cmd(cx, &args[start..end].join(" "), event)?; + } + 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", @@ -2240,6 +2301,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: run_shell_command, completer: Some(completers::directory), }, + TypableCommand { + name: "commands", + aliases: &["cmds"], + doc: "Run commands together, use && to sepearte them", + fun: cmds, + completer: Some(completers::filename), + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = @@ -2317,31 +2385,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| { @@ -2363,3 +2407,58 @@ pub(super) fn command_mode(cx: &mut Context) { prompt.recalculate_completion(cx.editor); 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 next = expand_args(editor, remaining.get(end + 1..).unwrap_or("")); + let (_view, doc) = current_ref!(editor); + format!( + "{} {}", + 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(), + }, + 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() +}