From 6f6cb3ccbf7087042c70a61b9d57a087eaa09592 Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Mon, 21 Nov 2022 10:39:52 +0800 Subject: [PATCH 1/3] support commmand expansion --- book/src/generated/typable-cmd.md | 1 + helix-term/src/commands.rs | 21 +++-- helix-term/src/commands/typed.rs | 149 +++++++++++++++++++++++++----- 3 files changed, 136 insertions(+), 35 deletions(-) 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() +} From 85d38a714fcb6595ec795340fb7f5706bd54d4fa Mon Sep 17 00:00:00 2001 From: Bob Qi Date: Tue, 24 Jan 2023 14:49:16 +0800 Subject: [PATCH 2/3] remove command group function --- book/src/generated/typable-cmd.md | 1 - helix-term/src/commands/typed.rs | 32 ------------------------------- 2 files changed, 33 deletions(-) diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 0aae18857558..66e6ac039c26 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -73,4 +73,3 @@ | `:pipe` | Pipe each selection to the shell command. | | `:pipe-to` | Pipe each selection to the shell command, ignoring output. | | `:run-shell-command`, `:sh` | Run a shell command | -| `:commands`, `:cmds` | Run commands together, use && to sepearte them | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 1a20e070b9d7..2f4f10e4ec20 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1822,31 +1822,6 @@ 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, @@ -2398,13 +2373,6 @@ 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> = From 22d17f9850fa150d0342a463a8c05958d33e92e2 Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 26 Jan 2023 09:43:13 +0800 Subject: [PATCH 3/3] Update helix-term/src/commands/typed.rs Co-authored-by: Ivan Tham --- helix-term/src/commands/typed.rs | 47 +++++++++++++++----------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 2f4f10e4ec20..fc11a8fe015e 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2479,33 +2479,30 @@ fn expand_args<'a>(editor: &mut Editor, args: &'a str) -> Cow<'a, str> { 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() - } - } + 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(), }, - next - ) + "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}") }) }