mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 10:56:19 +04:00
Merge d3927d8da3
into b8313da5a8
This commit is contained in:
commit
4fbe46d424
@ -3,3 +3,30 @@ # Commands
|
|||||||
Command mode can be activated by pressing `:`. The built-in commands are:
|
Command mode can be activated by pressing `:`. The built-in commands are:
|
||||||
|
|
||||||
{{#include ./generated/typable-cmd.md}}
|
{{#include ./generated/typable-cmd.md}}
|
||||||
|
|
||||||
|
## Using variables in typed commands and mapped shortcuts
|
||||||
|
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}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
@ -88,3 +88,4 @@
|
|||||||
| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
|
| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
|
||||||
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
|
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
|
||||||
| `:read`, `:r` | Load a file into buffer |
|
| `:read`, `:r` | Load a file into buffer |
|
||||||
|
| `:echo` | Print the processed input to the editor status |
|
||||||
|
@ -40,93 +40,120 @@ pub struct Shellwords<'a> {
|
|||||||
impl<'a> From<&'a str> for Shellwords<'a> {
|
impl<'a> From<&'a str> for Shellwords<'a> {
|
||||||
fn from(input: &'a str) -> Self {
|
fn from(input: &'a str) -> Self {
|
||||||
use State::*;
|
use State::*;
|
||||||
|
|
||||||
let mut state = Unquoted;
|
let mut state = Unquoted;
|
||||||
let mut words = Vec::new();
|
let mut words = Vec::new();
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
let mut escaped = String::with_capacity(input.len());
|
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 part_start = 0;
|
||||||
let mut unescaped_start = 0;
|
let mut unescaped_start = 0;
|
||||||
let mut end = 0;
|
let mut end = 0;
|
||||||
|
|
||||||
for (i, c) in input.char_indices() {
|
for (i, c) in input.char_indices() {
|
||||||
state = match state {
|
if c == '%' {
|
||||||
OnWhitespace => match c {
|
//%sh{this "should" be escaped}
|
||||||
'"' => {
|
if let Some(t) = input.get(i + 1..i + 3) {
|
||||||
end = i;
|
if t == "sh" {
|
||||||
Dquoted
|
nested_variable_expansion_count += 1;
|
||||||
|
inside_variable_expansion = true;
|
||||||
}
|
}
|
||||||
'\'' => {
|
}
|
||||||
end = i;
|
//%{this "should" be escaped}
|
||||||
Quoted
|
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]);
|
if c == '}' {
|
||||||
unescaped_start = i + 1;
|
nested_variable_expansion_count -= 1;
|
||||||
UnquotedEscaped
|
if nested_variable_expansion_count == 0 {
|
||||||
} else {
|
inside_variable_expansion = false;
|
||||||
OnWhitespace
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
c if c.is_ascii_whitespace() => {
|
state = if !inside_variable_expansion {
|
||||||
end = i;
|
match state {
|
||||||
OnWhitespace
|
OnWhitespace => match c {
|
||||||
}
|
'"' => {
|
||||||
_ => Unquoted,
|
end = i;
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'\'' => {
|
|
||||||
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
|
Dquoted
|
||||||
}
|
}
|
||||||
}
|
'\'' => {
|
||||||
'"' => {
|
end = i;
|
||||||
end = i;
|
Quoted
|
||||||
OnWhitespace
|
}
|
||||||
}
|
'\\' => {
|
||||||
_ => Dquoted,
|
if cfg!(unix) {
|
||||||
},
|
escaped.push_str(&input[unescaped_start..i]);
|
||||||
DquoteEscaped => Dquoted,
|
unescaped_start = i + 1;
|
||||||
|
UnquotedEscaped
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'\'' => {
|
||||||
|
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();
|
let c_len = c.len_utf8();
|
||||||
@ -235,6 +262,38 @@ fn test_normal() {
|
|||||||
// TODO test is_owned and is_borrowed, once they get stabilized.
|
// TODO test is_owned and is_borrowed, once they get stabilized.
|
||||||
assert_eq!(expected, result);
|
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]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
@ -226,13 +226,28 @@ pub fn execute(&self, cx: &mut Context) {
|
|||||||
match &self {
|
match &self {
|
||||||
Self::Typable { name, args, doc: _ } => {
|
Self::Typable { name, args, doc: _ } => {
|
||||||
let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
|
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) {
|
||||||
|
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()) {
|
if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
|
||||||
let mut cx = compositor::Context {
|
let mut cx = compositor::Context {
|
||||||
editor: cx.editor,
|
editor: cx.editor,
|
||||||
jobs: cx.jobs,
|
jobs: cx.jobs,
|
||||||
scroll: None,
|
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));
|
cx.editor.set_error(format!("{}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2520,6 +2520,18 @@ fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
|
|||||||
Ok(())
|
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] = &[
|
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "quit",
|
name: "quit",
|
||||||
@ -3141,6 +3153,13 @@ fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
|
|||||||
fun: read,
|
fun: read,
|
||||||
signature: CommandSignature::positional(&[completers::filename]),
|
signature: CommandSignature::positional(&[completers::filename]),
|
||||||
},
|
},
|
||||||
|
TypableCommand {
|
||||||
|
name: "echo",
|
||||||
|
aliases: &[],
|
||||||
|
doc: "Print the processed input to the editor status",
|
||||||
|
fun: echo,
|
||||||
|
signature: CommandSignature::none()
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
|
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
|
||||||
@ -3220,8 +3239,18 @@ pub(super) fn command_mode(cx: &mut Context) {
|
|||||||
// Handle typable commands
|
// Handle typable commands
|
||||||
if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
|
if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) {
|
||||||
let shellwords = Shellwords::from(input);
|
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) {
|
||||||
|
Ok(args) => args,
|
||||||
|
Err(e) => {
|
||||||
|
cx.editor.set_error(format!("{}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
words
|
||||||
|
};
|
||||||
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
|
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
|
||||||
cx.editor.set_error(format!("{}", e));
|
cx.editor.set_error(format!("{}", e));
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
mod movement;
|
mod movement;
|
||||||
|
mod variable_expansion;
|
||||||
mod write;
|
mod write;
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
183
helix-term/tests/test/commands/variable_expansion.rs
Normal file
183
helix-term/tests/test/commands/variable_expansion.rs
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
use super::*;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn test_variable_expansion() -> anyhow::Result<()> {
|
||||||
|
{
|
||||||
|
let mut app = AppBuilder::new().build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %{filename}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut app = AppBuilder::new().build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %{basename}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut app = AppBuilder::new().build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %{dirname}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let file = tempfile::NamedTempFile::new()?;
|
||||||
|
let mut app = AppBuilder::new().with_file(file.path(), None).build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %{filename}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
helix_stdx::path::canonicalize(file.path())
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut app = AppBuilder::new().with_file(file.path(), None).build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %{basename}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
file.path().file_name().unwrap().to_str().unwrap()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut app = AppBuilder::new().with_file(file.path(), None).build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %{dirname}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
helix_stdx::path::canonicalize(file.path().parent().unwrap())
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let file = tempfile::NamedTempFile::new()?;
|
||||||
|
let mut app = AppBuilder::new().with_file(file.path(), None).build()?;
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("ihelix<esc>%:echo %{selection}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(app.editor.get_status().unwrap().0, "helix");
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let file = tempfile::NamedTempFile::new()?;
|
||||||
|
let mut app = AppBuilder::new().with_file(file.path(), None).build()?;
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("ihelix<ret>helix<ret>helix<ret><esc>:echo %{linenumber}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(app.editor.get_status().unwrap().0, "4");
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut app = AppBuilder::new().build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %sh{echo %{filename}}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut app = AppBuilder::new().build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %sh{echo %{filename} %{linenumber}}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
&Cow::from(format!(
|
||||||
|
"{} {}",
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME,
|
||||||
|
1
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut app = AppBuilder::new().build()?;
|
||||||
|
|
||||||
|
test_key_sequence(
|
||||||
|
&mut app,
|
||||||
|
Some("<esc>:echo %sh{echo %{filename} %sh{echo %{filename}}}<ret>"),
|
||||||
|
Some(&|app| {
|
||||||
|
assert_eq!(
|
||||||
|
app.editor.get_status().unwrap().0,
|
||||||
|
&Cow::from(format!(
|
||||||
|
"{} {}",
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME,
|
||||||
|
helix_view::document::SCRATCH_BUFFER_NAME
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,3 +1,5 @@
|
|||||||
|
mod variable_expansion;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
|
annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
|
||||||
clipboard::ClipboardProvider,
|
clipboard::ClipboardProvider,
|
||||||
|
204
helix-view/src/editor/variable_expansion.rs
Normal file
204
helix-view/src/editor/variable_expansion.rs
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
use helix_core::coords_at_pos;
|
||||||
|
|
||||||
|
use crate::Editor;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
impl Editor {
|
||||||
|
pub fn expand_variables_in_vec<'a>(
|
||||||
|
&self,
|
||||||
|
args: &'a Vec<Cow<'a, str>>,
|
||||||
|
) -> anyhow::Result<Vec<Cow<'a, str>>> {
|
||||||
|
let mut output = Vec::with_capacity(args.len());
|
||||||
|
for arg in args {
|
||||||
|
if let Ok(s) = self.expand_variable_in_string(arg) {
|
||||||
|
output.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result<Cow<'a, str>> {
|
||||||
|
let (view, doc) = current_ref!(self);
|
||||||
|
let shell = &self.config().shell;
|
||||||
|
|
||||||
|
let mut output: Option<String> = None;
|
||||||
|
|
||||||
|
let mut chars = input.char_indices();
|
||||||
|
let mut last_push_end: usize = 0;
|
||||||
|
|
||||||
|
while let Some((index, char)) = chars.next() {
|
||||||
|
if char == '%' {
|
||||||
|
if let Some((_, char)) = chars.next() {
|
||||||
|
if char == '{' {
|
||||||
|
for (end, char) in chars.by_ref() {
|
||||||
|
if char == '}' {
|
||||||
|
if output.is_none() {
|
||||||
|
output = Some(String::with_capacity(input.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(o) = output.as_mut() {
|
||||||
|
o.push_str(&input[last_push_end..index]);
|
||||||
|
last_push_end = end + 1;
|
||||||
|
|
||||||
|
let value = match &input[index + 2..end] {
|
||||||
|
"basename" | "b" => doc
|
||||||
|
.path()
|
||||||
|
.and_then(|it| {
|
||||||
|
it.file_name().and_then(|it| it.to_str())
|
||||||
|
})
|
||||||
|
.unwrap_or(crate::document::SCRATCH_BUFFER_NAME)
|
||||||
|
.to_owned(),
|
||||||
|
"filename" | "f" => doc
|
||||||
|
.path()
|
||||||
|
.and_then(|it| it.to_str())
|
||||||
|
.unwrap_or(crate::document::SCRATCH_BUFFER_NAME)
|
||||||
|
.to_owned(),
|
||||||
|
"filename:repo_rel" => {
|
||||||
|
// This will get repo root or cwd if not inside a git repo.
|
||||||
|
let workspace_path = helix_loader::find_workspace().0;
|
||||||
|
doc.path()
|
||||||
|
.and_then(|p| {
|
||||||
|
p.strip_prefix(workspace_path)
|
||||||
|
.unwrap_or(p)
|
||||||
|
.to_str()
|
||||||
|
})
|
||||||
|
.unwrap_or(crate::document::SCRATCH_BUFFER_NAME)
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
"filename:rel" => {
|
||||||
|
let cwd = helix_stdx::env::current_working_dir();
|
||||||
|
doc.path()
|
||||||
|
.and_then(|p| {
|
||||||
|
p.strip_prefix(cwd).unwrap_or(p).to_str()
|
||||||
|
})
|
||||||
|
.unwrap_or(crate::document::SCRATCH_BUFFER_NAME)
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
"dirname" | "d" => doc
|
||||||
|
.path()
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.and_then(std::path::Path::to_str)
|
||||||
|
.unwrap_or(crate::document::SCRATCH_BUFFER_NAME)
|
||||||
|
.to_owned(),
|
||||||
|
"repo" => helix_loader::find_workspace()
|
||||||
|
.0
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_owned(),
|
||||||
|
"cwd" => helix_stdx::env::current_working_dir()
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.to_owned(),
|
||||||
|
"linenumber" => (doc
|
||||||
|
.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor_line(doc.text().slice(..))
|
||||||
|
+ 1)
|
||||||
|
.to_string(),
|
||||||
|
"cursorcolumn" => (coords_at_pos(
|
||||||
|
doc.text().slice(..),
|
||||||
|
doc.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.cursor(doc.text().slice(..)),
|
||||||
|
)
|
||||||
|
.col + 1)
|
||||||
|
.to_string(),
|
||||||
|
"lang" => doc.language_name().unwrap_or("text").to_string(),
|
||||||
|
"ext" => doc
|
||||||
|
.relative_path()
|
||||||
|
.and_then(|p| {
|
||||||
|
p.extension()?.to_os_string().into_string().ok()
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
"selection" => doc
|
||||||
|
.selection(view.id)
|
||||||
|
.primary()
|
||||||
|
.fragment(doc.text().slice(..))
|
||||||
|
.to_string(),
|
||||||
|
_ => anyhow::bail!("Unknown variable"),
|
||||||
|
};
|
||||||
|
|
||||||
|
o.push_str(value.trim());
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if char == 's' {
|
||||||
|
if let (Some((_, 'h')), Some((_, '{'))) = (chars.next(), chars.next()) {
|
||||||
|
let mut right_bracket_remaining = 1;
|
||||||
|
for (end, char) in chars.by_ref() {
|
||||||
|
if char == '}' {
|
||||||
|
right_bracket_remaining -= 1;
|
||||||
|
|
||||||
|
if right_bracket_remaining == 0 {
|
||||||
|
if output.is_none() {
|
||||||
|
output = Some(String::with_capacity(input.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(o) = output.as_mut() {
|
||||||
|
let body = self.expand_variable_in_string(
|
||||||
|
&input[index + 4..end],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let output = tokio::task::block_in_place(move || {
|
||||||
|
helix_lsp::block_on(async move {
|
||||||
|
let mut command =
|
||||||
|
tokio::process::Command::new(&shell[0]);
|
||||||
|
command.args(&shell[1..]).arg(&body[..]);
|
||||||
|
|
||||||
|
let output =
|
||||||
|
command.output().await.map_err(|_| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Shell command failed: {body}"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
String::from_utf8(output.stdout).map_err(
|
||||||
|
|_| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Process did not output valid UTF-8"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else if output.stderr.is_empty() {
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"Shell command failed: {body}"
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let stderr =
|
||||||
|
String::from_utf8_lossy(&output.stderr);
|
||||||
|
|
||||||
|
Err(anyhow::anyhow!("{stderr}"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
o.push_str(&input[last_push_end..index]);
|
||||||
|
last_push_end = end + 1;
|
||||||
|
|
||||||
|
o.push_str(output?.trim());
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if char == '{' {
|
||||||
|
right_bracket_remaining += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(o) = output.as_mut() {
|
||||||
|
o.push_str(&input[last_push_end..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Some(o) => Ok(Cow::Owned(o)),
|
||||||
|
None => Ok(Cow::Borrowed(input)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user