Skip to content

Commit

Permalink
Add command expansions %key{body}
Browse files Browse the repository at this point in the history
Based on: helix-editor#3393

---

Squashed commit of the following:

commit 4e16272eb27fde410022785ffcdb6828ac61aee9
Merge: 07b109d 5ae30f1
Author: Jesús R <roldanr.jesus@gmail.com>
Date:   Sat May 6 07:56:28 2023 -0600

    Merge branch 'master' into cmd-expansions

commit 07b109d
Author: Jesús R <roldanr.jesus@gmail.com>
Date:   Mon Apr 24 19:14:58 2023 -0600

    fix: Not parsing sh body

commit 8bf040f
Author: Jesús R <roldanr.jesus@gmail.com>
Date:   Sat Apr 22 22:46:17 2023 -0600

    Use %key{body}

commit f6fea44
Merge: ffb40de b7c62e2
Author: Jesús R <roldanr.jesus@gmail.com>
Date:   Sat Apr 22 17:34:07 2023 -0600

    Merge branch 'master' into cmd-expansions

commit ffb40de
Merge: 2be5d34 b9b4ed5
Author: Jesús R <roldanr.jesus@gmail.com>
Date:   Tue Apr 11 15:48:15 2023 -0600

    Merge branch 'master' into cmd-expansions

commit 2be5d34
Author: Jesús R <roldanr.jesus@gmail.com>
Date:   Tue Apr 11 15:46:58 2023 -0600

    Use #{} for variables and #key [] for commands

commit 7e7c0dc
Merge: 22d17f9 531b745
Author: Bob <QiBaobin@users.noreply.github.com>
Date:   Wed Apr 5 09:11:13 2023 +0800

    Merge branch 'master' into cmd-expansions

commit 22d17f9
Author: Bob <QiBaobin@users.noreply.github.com>
Date:   Thu Jan 26 09:43:13 2023 +0800

    Update helix-term/src/commands/typed.rs

    Co-authored-by: Ivan Tham <pickfire@riseup.net>

commit 85d38a7
Author: Bob Qi <baobin.qi@gmail.com>
Date:   Tue Jan 24 14:49:16 2023 +0800

    remove command group function

commit 784380f
Merge: 6f6cb3c 64ec025
Author: Bob Qi <baobin.qi@gmail.com>
Date:   Tue Jan 24 14:42:31 2023 +0800

    Merge remote-tracking branch 'origin/master'

commit 6f6cb3c
Author: Bob Qi <baobin.qi@gmail.com>
Date:   Mon Nov 21 10:39:52 2022 +0800

    support commmand expansion
  • Loading branch information
ksdrar authored and postsolar committed Apr 4, 2024
1 parent d7990bd commit d6bc590
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 35 deletions.
21 changes: 11 additions & 10 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,16 +194,17 @@ impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
match &self {
Self::Typable { name, args, doc: _ } => {
let args: Vec<Cow<str>> = 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),
Expand Down
147 changes: 122 additions & 25 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2451,6 +2451,51 @@ fn yank_diagnostic(
"Yanked {n} diagnostic{} to register {reg}",
if n == 1 { "" } else { "s" }
));

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::<Vec<&str>>();
if parts.is_empty() {
return Ok(());
}

// If command is numeric, interpret as line number and go there.
if parts.len() == 1 && parts[0].parse::<usize>().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(())
}

Expand Down Expand Up @@ -3131,31 +3176,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::<Vec<&str>>();
if parts.is_empty() {
return;
}

// If command is numeric, interpret as line number and go there.
if parts.len() == 1 && parts[0].parse::<usize>().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| {
Expand All @@ -3178,6 +3199,82 @@ pub(super) fn command_mode(cx: &mut Context) {
cx.push_layer(Box::new(prompt));
}

fn expand_args(editor: &Editor, args: &str) -> anyhow::Result<String> {
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(&regexp, 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: &regex::Regex,
text: &str,
matcher: impl Fn(&regex::Captures) -> anyhow::Result<String>,
) -> anyhow::Result<String> {
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)
Expand Down

0 comments on commit d6bc590

Please sign in to comment.