Skip to content

Commit

Permalink
Command expansion v2 (squashed version of helix-editor#11164)
Browse files Browse the repository at this point in the history
  • Loading branch information
irh committed Dec 23, 2024
1 parent 756b476 commit 6532213
Show file tree
Hide file tree
Showing 10 changed files with 612 additions and 76 deletions.
29 changes: 29 additions & 0 deletions book/src/commands.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Commands

- [Typable commands](#typable-commands)
- [Using variables](#using-variables-in-typable-commands)
- [Static commands](#static-commands)

## Typable commands
Expand All @@ -9,6 +10,34 @@ Typable commands are used from command mode and may take arguments. Command mode

{{#include ./generated/typable-cmd.md}}

### Using variables in typable commands

Helix provides several variables that can be used when typing commands or creating custom shortcuts. These variables are listed below:

| Variable | Description |
| --- | --- |
| `%{basename}` or `%{b}` | The name and extension of the currently focused file. |
| `%{dirname}` or `%{d}` | The absolute path of the parent directory of the currently focused file. |
| `%{cwd}` | The absolute path of the current working directory of Helix. |
| `%{repo}` | The absolute path of the VCS repository helix is opened in. Fallback to `cwd` if not inside a VCS repository|
| `%{filename}` or `%{f}` | The absolute path of the currently focused file. |
| `%{filename:rel}` | The relative path of the file according to `cwd` (will give absolute path if the file is not a child of the current working directory) |
| `%{filename:repo_rel}` | The relative path of the file according to `repo` (will give absolute path if the file is not a child of the VCS directory or the cwd) |
| `%{ext}` | The extension of the current file |
| `%{lang}` | The language of the current file |
| `%{linenumber}` | The line number where the primary cursor is positioned. |
| `%{cursorcolumn}` | The position of the primary cursor inside the current line. |
| `%{selection}` | The text selected by the primary cursor. |
| `%sh{cmd}` | Executes `cmd` with the default shell and returns the command output, if any. |

#### Example

```toml
[keys.normal]
# Print blame info for the line where the main cursor is.
C-b = ":echo %sh{git blame -L %{linenumber} %{filename}}"
```

## Static Commands

Static commands take no arguments and can be bound to keys. Static commands can also be executed from the command picker (`<space>?`). The built-in static commands are:
Expand Down
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
| `:read`, `:r` | Load a file into buffer |
| `:trim-trailing-whitespace`, `:trim` | Delete whitespace |
| `:echo` | Print the processed input to the editor status |
201 changes: 130 additions & 71 deletions helix-core/src/shellwords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,93 +40,120 @@ pub struct Shellwords<'a> {
impl<'a> From<&'a str> for Shellwords<'a> {
fn from(input: &'a str) -> Self {
use State::*;

let mut state = Unquoted;
let mut words = Vec::new();
let mut parts = Vec::new();
let mut escaped = String::with_capacity(input.len());

let mut inside_variable_expansion = false;
let mut nested_variable_expansion_count = 0;
let mut part_start = 0;
let mut unescaped_start = 0;
let mut end = 0;

for (i, c) in input.char_indices() {
state = match state {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
if c == '%' {
//%sh{this "should" be escaped}
if let Some(t) = input.get(i + 1..i + 3) {
if t == "sh" {
nested_variable_expansion_count += 1;
inside_variable_expansion = true;
}
'\'' => {
end = i;
Quoted
}
//%{this "should" be escaped}
if let Some(t) = input.get(i + 1..i + 2) {
if t == "{" {
nested_variable_expansion_count += 1;
inside_variable_expansion = true;
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
}
}
if c == '}' {
nested_variable_expansion_count -= 1;
if nested_variable_expansion_count == 0 {
inside_variable_expansion = false;
}
}

state = if !inside_variable_expansion {
match state {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
}
'\'' => {
end = i;
Quoted
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
OnWhitespace
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
_ => Unquoted,
},
Unquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
Unquoted
_ => Unquoted,
},
Unquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
Unquoted
}
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
QuoteEscaped
} else {
Quoted
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
}
'\'' => {
end = i;
OnWhitespace
}
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
DquoteEscaped
} else {
Dquoted
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
QuoteEscaped
} else {
Quoted
}
}
}
'"' => {
end = i;
OnWhitespace
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
'\'' => {
end = i;
OnWhitespace
}
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
DquoteEscaped
} else {
Dquoted
}
}
'"' => {
end = i;
OnWhitespace
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
}
} else {
state
};

let c_len = c.len_utf8();
Expand Down Expand Up @@ -235,6 +262,38 @@ mod test {
// TODO test is_owned and is_borrowed, once they get stabilized.
assert_eq!(expected, result);
}
#[test]
fn test_expansion() {
let input = r#"echo %{filename} %{linenumber}"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from("echo"),
Cow::from("%{filename}"),
Cow::from("%{linenumber}"),
];
assert_eq!(expected, result);

let input = r#"echo %{filename} 'world' %{something to 'escape}"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from("echo"),
Cow::from("%{filename}"),
Cow::from("world"),
Cow::from("%{something to 'escape}"),
];
assert_eq!(expected, result);
let input = r#"echo %sh{%sh{%{filename}}} cool"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from("echo"),
Cow::from("%sh{%sh{%{filename}}}"),
Cow::from("cool"),
];
assert_eq!(expected, result);
}

#[test]
#[cfg(unix)]
Expand Down
19 changes: 18 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,30 @@ impl MappableCommand {
match &self {
Self::Typable { name, args, doc: _ } => {
let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
let mut joined_args = args.join(" ");
let expanded_args = match args.len() {
0 => vec![],
_ => {
if let Ok(expanded) =
cx.editor.expand_variable_in_string(&joined_args, true)
{
joined_args = expanded.to_string();
joined_args.split(' ').map(Cow::from).collect()
} else {
args
}
}
};
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) {

if let Err(e) =
(command.fun)(&mut cx, &expanded_args[..], PromptEvent::Validate)
{
cx.editor.set_error(format!("{}", e));
}
}
Expand Down
34 changes: 31 additions & 3 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2620,6 +2620,18 @@ fn trim_whitespace(
Ok(())
}

fn echo(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}

let args = args.join(" ");

cx.editor.set_status(args);

Ok(())
}

pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
Expand Down Expand Up @@ -3247,7 +3259,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
doc: "Delete trailing whitespace from the current selections",
fun: trim_whitespace,
signature: CommandSignature::none(),

},
TypableCommand {
name: "echo",
aliases: &[],
doc: "Print the processed input to the editor status",
fun: echo,
signature: CommandSignature::none()
},
];

Expand Down Expand Up @@ -3328,8 +3346,18 @@ pub(super) fn command_mode(cx: &mut Context) {
// Handle typable commands
if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
let shellwords = Shellwords::from(input);
let args = shellwords.words();

let words = shellwords.words().to_vec();
let args = if event == PromptEvent::Validate {
match cx.editor.expand_variables_in_vec(&words, true) {
Ok(args) => args,
Err(e) => {
cx.editor.set_error(format!("{}", e));
return;
}
}
} else {
words
};
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));
}
Expand Down
8 changes: 7 additions & 1 deletion helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,13 @@ pub mod completers {
use std::path::Path;

let is_tilde = input == "~";
let path = helix_stdx::path::expand_tilde(Path::new(input));
let path = editor
.expand_variable_in_string(input, false)
.map_or(input.to_string(), |expanded_input| {
expanded_input.into_owned()
});

let path = helix_stdx::path::expand_tilde(Path::new(path.as_str()));

let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
(path, None)
Expand Down
1 change: 1 addition & 0 deletions helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::*;

mod insert;
mod movement;
mod variable_expansion;
mod write;

#[tokio::test(flavor = "multi_thread")]
Expand Down
Loading

0 comments on commit 6532213

Please sign in to comment.