From fdfe5fc7de52880ed19e621cd9a005d8f3c77c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20R?= Date: Sat, 6 May 2023 08:17:55 -0600 Subject: [PATCH] Add command expansions %key{body} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on: https://github.com/helix-editor/helix/pull/3393 --- Squashed commit of the following: commit 4e16272eb27fde410022785ffcdb6828ac61aee9 Merge: 07b109d2 5ae30f19 Author: Jesús R Date: Sat May 6 07:56:28 2023 -0600 Merge branch 'master' into cmd-expansions commit 07b109d289e429d4058480f3087301d07f3001a6 Author: Jesús R Date: Mon Apr 24 19:14:58 2023 -0600 fix: Not parsing sh body commit 8bf040f37aeb39077072bb6acf0a61c2085f6086 Author: Jesús R Date: Sat Apr 22 22:46:17 2023 -0600 Use %key{body} commit f6fea44ecfb203efe3d56814304fef190371d090 Merge: ffb40def b7c62e20 Author: Jesús R Date: Sat Apr 22 17:34:07 2023 -0600 Merge branch 'master' into cmd-expansions commit ffb40defc6077509e7e459b86c78636d5a22d365 Merge: 2be5d34c b9b4ed5c Author: Jesús R Date: Tue Apr 11 15:48:15 2023 -0600 Merge branch 'master' into cmd-expansions commit 2be5d34cd937e0569f3415b77e86dc76624b9657 Author: Jesús R Date: Tue Apr 11 15:46:58 2023 -0600 Use #{} for variables and #key [] for commands commit 7e7c0dcaccf05f365df750cedb702703ad7fa90c Merge: 22d17f98 531b745c Author: Bob Date: Wed Apr 5 09:11:13 2023 +0800 Merge branch 'master' into cmd-expansions commit 22d17f9850fa150d0342a463a8c05958d33e92e2 Author: Bob Date: Thu Jan 26 09:43:13 2023 +0800 Update helix-term/src/commands/typed.rs Co-authored-by: Ivan Tham commit 85d38a714fcb6595ec795340fb7f5706bd54d4fa Author: Bob Qi Date: Tue Jan 24 14:49:16 2023 +0800 remove command group function commit 784380f37de9d1d83ce1c3a5c35437cf58f688b0 Merge: 6f6cb3cc 64ec0256 Author: Bob Qi Date: Tue Jan 24 14:42:31 2023 +0800 Merge remote-tracking branch 'origin/master' commit 6f6cb3ccbf7087042c70a61b9d57a087eaa09592 Author: Bob Qi Date: Mon Nov 21 10:39:52 2022 +0800 support commmand expansion --- helix-term/src/commands.rs | 21 ++--- helix-term/src/commands/typed.rs | 149 +++++++++++++++++++++++++------ 2 files changed, 135 insertions(+), 35 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1bd736523edc..280229482453 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -193,16 +193,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 5198e5bdf5cf..316dae70addb 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2270,6 +2270,53 @@ fn clear_register( Ok(()) } +pub fn process_cmd( + cx: &mut compositor::Context, + 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); + } + } + } else { + input.to_owned() + }; + + 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", @@ -2924,31 +2971,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| { @@ -2971,6 +2994,82 @@ 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(); + + 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() { + "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()) + }), + "filedir" => doc + .path() + .and_then(|p| p.parent()) + .and_then(|p| p.to_str()) + .map_or( + Err(anyhow::anyhow!("Current buffer has no path or parent")), + |v| Ok(v.to_owned()), + ), + "line_number" => Ok((doc + .selection(view.id) + .primary() + .cursor_line(doc.text().slice(..)) + + 1) + .to_string()), + _ => anyhow::bail!("Unknown variable: {body}"), + }, + "sh" => { + let result = shell_impl(shell, &expand_args(editor, body)?, None)?; + + 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( + regex: ®ex::Regex, + text: &str, + matcher: impl Fn(®ex::Captures) -> anyhow::Result, +) -> anyhow::Result { + let mut it = regex.captures_iter(text).peekable(); + + if it.peek().is_none() { + return Ok(String::from(text)); + } + + let mut new = String::with_capacity(text.len()); + let mut last_match = 0; + + for cap in it { + let m = cap.get(0).unwrap(); + new.push_str(&text[last_match..m.start()]); + + let replace = matcher(&cap)?; + + new.push_str(&replace); + + last_match = m.end(); + } + + new.push_str(&text[last_match..]); + + replace_all(regex, &new, matcher) +} + fn argument_number_of(shellwords: &Shellwords) -> usize { if shellwords.ends_with_whitespace() { shellwords.words().len().saturating_sub(1)