Skip to content

Commit

Permalink
feat: add support for ^ prefix escaping
Browse files Browse the repository at this point in the history
  • Loading branch information
RoloEdits committed Dec 25, 2024
1 parent 9d145ac commit 430498e
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 113 deletions.
237 changes: 126 additions & 111 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3145,119 +3145,136 @@ pub(super) fn command_mode(cx: &mut Context) {
);
}

// Checking for custom commands first priotizes custom commands over built-in/
if let Some(custom) = cx.editor.config().commands.get(command) {
for command in custom.iter() {
if let Some(typed_command) =
typed::TYPABLE_COMMAND_MAP.get(Shellwords::from(command).command())
{
// TODO: Expand variables: #11164
//
// let args = match variables::expand(...) {
// Ok(args: Cow<'_, str>) => args,
// Err(err) => {
// cx.editor.set_error(format!("{err}"));
// // Short circuit on error
// return;
// }
// }
//

// TEST: should allow for an option `%{arg}` even if no path is path is provided and work as if
// the `%{arg}` eas never present.
//
// Assume that if the command contains an `%{arg[:NUMBER]}` it will be accepting arguments from
// input and therefore not standalone.
//
// If `false`, then will use any arguments the command itself may have been written
// with and ignore any typed-in arguments.
//
// This is a special case for when config has simplest usage:
//
// "ww" = ":write --force"
//
// It will only use: `--force` as arguments when running `:write`.
//
// This also means that users dont have to explicitly use `%{arg}` every time:
//
// "ww" = ":write --force %{arg}"
//
// Though in the case of `:write`, they probably should.
//
// Regardless, some commands explicitly take zero arguments and this check should prevent
// input arguments being passed when they shouldnt.
//
// If `true`, then will assume that the command was passed arguments in expansion and that
// whats left is the full argument list to be sent run.
let args = if contains_arg_variable(command) {
// Input args
// TODO: Args::from(&args) from the expanded variables.
shellwords.args()
} else {
Shellwords::from(command).args()
};

if let Err(err) = (typed_command.fun)(cx, args, event) {
cx.editor.set_error(format!("{err}"));
// Short circuit on error
return;
}
// Handle static commands
} else if let Some(static_command) = super::MappableCommand::STATIC_COMMAND_LIST
.iter()
.find(|mappable| mappable.name() == Shellwords::from(command).command())
{
let mut cx = super::Context {
register: None,
count: None,
editor: cx.editor,
callback: vec![],
on_next_key_callback: None,
jobs: cx.jobs,
};

let MappableCommand::Static { fun, .. } = static_command else {
unreachable!("should only be able to get a static command from `STATIC_COMMAND_LIST`")
};

(fun)(&mut cx);
// Handle macro
} else if let Some(suffix) = command.strip_prefix('@') {
let keys = match helix_view::input::parse_macro(suffix) {
Ok(keys) => keys,
Err(err) => {
cx.editor
.set_error(format!("failed to parse macro `{command}`: {err}"));
let is_escaped = command.starts_with('^');
let command = command.trim_start_matches('^');

// Checking for custom commands first priotizes custom commands over built-in.
//
// Custom commands can be escaped with a `^`.
//
// TODO: When let chains are stable reduce nestedness
if !is_escaped {
if let Some(custom) = cx.editor.config().commands.get(command) {
for command in custom.iter() {
if let Some(typed_command) =
typed::TYPABLE_COMMAND_MAP.get(Shellwords::from(command).command())
{
// TODO: Expand variables: #11164
//
// let args = match variables::expand(...) {
// Ok(args: Cow<'_, str>) => args,
// Err(err) => {
// cx.editor.set_error(format!("{err}"));
// // Short circuit on error
// return;
// }
// }
//

// TEST: should allow for an option `%{arg}` even if no path is path is provided and work as if
// the `%{arg}` eas never present.
//
// Assume that if the command contains an `%{arg[:NUMBER]}` it will be accepting arguments from
// input and therefore not standalone.
//
// If `false`, then will use any arguments the command itself may have been written
// with and ignore any typed-in arguments.
//
// This is a special case for when config has simplest usage:
//
// "ww" = ":write --force"
//
// It will only use: `--force` as arguments when running `:write`.
//
// This also means that users dont have to explicitly use `%{arg}` every time:
//
// "ww" = ":write --force %{arg}"
//
// Though in the case of `:write`, they probably should.
//
// Regardless, some commands explicitly take zero arguments and this check should prevent
// input arguments being passed when they shouldnt.
//
// If `true`, then will assume that the command was passed arguments in expansion and that
// whats left is the full argument list to be sent run.
let args = if contains_arg_variable(command) {
// Input args
// TODO: Args::from(&args) from the expanded variables.
shellwords.args()
} else {
Shellwords::from(command).args()
};

if let Err(err) = (typed_command.fun)(cx, args, event) {
cx.editor.set_error(format!("{err}"));
// Short circuit on error
return;
}
// Handle static commands
} else if let Some(static_command) =
super::MappableCommand::STATIC_COMMAND_LIST
.iter()
.find(|mappable| {
mappable.name() == Shellwords::from(command).command()
})
{
let mut cx = super::Context {
register: None,
count: None,
editor: cx.editor,
callback: vec![],
on_next_key_callback: None,
jobs: cx.jobs,
};

let MappableCommand::Static { fun, .. } = static_command else {
unreachable!("should only be able to get a static command from `STATIC_COMMAND_LIST`")
};

(fun)(&mut cx);
// Handle macro
} else if let Some(suffix) = command.strip_prefix('@') {
let keys = match helix_view::input::parse_macro(suffix) {
Ok(keys) => keys,
Err(err) => {
cx.editor.set_error(format!(
"failed to parse macro `{command}`: {err}"
));
return;
}
};

// Protect against recursive macros.
if cx.editor.macro_replaying.contains(&'@') {
cx.editor.set_error("Cannot execute macro because the [@] register is already playing a macro");
return;
}
};

// Protect against recursive macros.
if cx.editor.macro_replaying.contains(&'@') {
cx.editor.set_error("Cannot execute macro because the [@] register is already playing a macro");
let mut cx = super::Context {
register: None,
count: None,
editor: cx.editor,
callback: vec![],
on_next_key_callback: None,
jobs: cx.jobs,
};

cx.editor.macro_replaying.push('@');
cx.callback.push(Box::new(move |compositor, cx| {
for key in keys {
compositor.handle_event(&compositor::Event::Key(key), cx);
}
cx.editor.macro_replaying.pop();
}));
} else if event == PromptEvent::Validate {
cx.editor.set_error(format!("no such command: '{command}'"));
// Short circuit on error
return;
}

let mut cx = super::Context {
register: None,
count: None,
editor: cx.editor,
callback: vec![],
on_next_key_callback: None,
jobs: cx.jobs,
};

cx.editor.macro_replaying.push('@');
cx.callback.push(Box::new(move |compositor, cx| {
for key in keys {
compositor.handle_event(&compositor::Event::Key(key), cx);
}
cx.editor.macro_replaying.pop();
}));
} else if event == PromptEvent::Validate {
cx.editor.set_error(format!("no such command: '{command}'"));
// Short circuit on error
return;
}
} else if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(command) {
if let Err(err) = (cmd.fun)(cx, shellwords.args(), event) {
cx.editor.set_error(format!("{err}"));
}
}
}
Expand All @@ -3277,9 +3294,7 @@ pub(super) fn command_mode(cx: &mut Context) {
let shellwords = Shellwords::from(input);

if let Some(command) = commands.clone().get(input) {
if let Some(desc) = &command.desc {
return Some(desc.clone().into());
}
return Some(command.prompt().into());
} else if let Some(typed::TypableCommand { doc, aliases, .. }) =
typed::TYPABLE_COMMAND_MAP.get(shellwords.command())
{
Expand Down
3 changes: 1 addition & 2 deletions helix-view/src/commands/custom.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO: When adding custom aliases to the command prompt list, must priotize the custom over the built-in.
// - Should include removing the alias from the aliases command?
//
//
// TODO: Need to get access to a new table in the config: [commands].
// TODO: Could add an `aliases` to `CustomTypableCommand` and then add those as well?

Expand Down Expand Up @@ -70,4 +70,3 @@ impl CustomTypableCommand {
.map(|command| command.trim_start_matches(':'))
}
}

0 comments on commit 430498e

Please sign in to comment.