Skip to content

Commit

Permalink
support commmand expansion
Browse files Browse the repository at this point in the history
  • Loading branch information
QiBaobin committed Nov 21, 2022
1 parent 7e99087 commit 6f6cb3c
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 35 deletions.
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
21 changes: 11 additions & 10 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,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
149 changes: 124 additions & 25 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,67 @@ fn run_shell_command(
Ok(())
}

fn cmds(cx: &mut compositor::Context, args: &[Cow<str>], 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::<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(())
}

pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
Expand Down Expand Up @@ -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<HashMap<&'static str, &'static TypableCommand>> =
Expand Down Expand Up @@ -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::<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 @@ -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: &regex::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()
}

0 comments on commit 6f6cb3c

Please sign in to comment.