From 731985c133a946c4fd75277e49611672fa8652f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Mon, 1 Jul 2024 11:45:15 +0200 Subject: [PATCH 01/14] command expansion --- book/src/commands.md | 20 +++ book/src/generated/typable-cmd.md | 1 + helix-term/src/commands.rs | 14 +- helix-term/src/commands/typed.rs | 32 +++- helix-term/tests/test/commands.rs | 1 + .../tests/test/commands/variable_expansion.rs | 133 ++++++++++++++++ helix-view/src/editor.rs | 2 + helix-view/src/editor/variable_expansion.rs | 147 ++++++++++++++++++ 8 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 helix-term/tests/test/commands/variable_expansion.rs create mode 100644 helix-view/src/editor/variable_expansion.rs diff --git a/book/src/commands.md b/book/src/commands.md index 047a30a91..e4647535e 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -3,3 +3,23 @@ # Commands Command mode can be activated by pressing `:`. The built-in commands are: {{#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}` | The name and extension of the currently focused file. | +| `%{filename}` | The absolute path of the currently focused file. | +| `%{dirname}` | The absolute path of the parent directory of the currently focused file. | +| `%{cwd}` | The absolute path of the current working directory of Helix. | +| `%{linenumber}` | The line number where the primary cursor is positioned. | +| `%{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}}" +``` diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index cf1b550dc..99de84696 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -88,3 +88,4 @@ | `:move` | 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 | | `:read`, `:r` | Load a file into buffer | +| `:echo` | Print the processed input to the editor status | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 67955aec0..48da41835 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -224,8 +224,18 @@ pub fn execute(&self, cx: &mut Context) { jobs: cx.jobs, scroll: None, }; - if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { - cx.editor.set_error(format!("{}", e)); + + let args = args.join(" "); + match cx.editor.expand_variables(&args) { + Ok(args) => { + let args = args.split_whitespace(); + let args: Vec> = args.map(Cow::Borrowed).collect(); + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) + { + cx.editor.set_error(format!("{}", e)); + } + } + Err(err) => cx.editor.set_error(err.to_string()), } } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ab12fab10..25e6efd44 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2491,6 +2491,18 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> Ok(()) } +fn echo(cx: &mut compositor::Context, args: &[Cow], 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", @@ -3112,6 +3124,13 @@ fn read(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> fun: read, 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> = @@ -3175,6 +3194,17 @@ pub(super) fn command_mode(cx: &mut Context) { } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + let input: Cow = if event == PromptEvent::Validate { + match cx.editor.expand_variables(input) { + Ok(args) => args, + Err(e) => { + cx.editor.set_error(format!("{}", e)); + return; + } + } + } else { + Cow::Borrowed(input) + }; let parts = input.split_whitespace().collect::>(); if parts.is_empty() { return; @@ -3190,7 +3220,7 @@ 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 shellwords = Shellwords::from(input.as_ref()); let args = shellwords.words(); if let Err(e) = (cmd.fun)(cx, &args[1..], event) { diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 7f41a2219..9acd6eb2e 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -3,6 +3,7 @@ use super::*; mod movement; +mod variable_expansion; mod write; #[tokio::test(flavor = "multi_thread")] diff --git a/helix-term/tests/test/commands/variable_expansion.rs b/helix-term/tests/test/commands/variable_expansion.rs new file mode 100644 index 000000000..a0935c426 --- /dev/null +++ b/helix-term/tests/test/commands/variable_expansion.rs @@ -0,0 +1,133 @@ +use super::*; + +#[tokio::test(flavor = "multi_thread")] +async fn test_variable_expansion() -> anyhow::Result<()> { + { + let mut app = AppBuilder::new().build()?; + + test_key_sequence( + &mut app, + Some(":echo %{filename}"), + 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(":echo %{basename}"), + 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(":echo %{dirname}"), + 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(":echo %{filename}"), + 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(":echo %{basename}"), + 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(":echo %{dirname}"), + 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%:echo %{selection}"), + 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("ihelixhelixhelix:echo %{linenumber}"), + Some(&|app| { + assert_eq!(app.editor.get_status().unwrap().0, "4"); + }), + false, + ) + .await?; + } + + Ok(()) +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 3eeb4830e..60a349dad 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,3 +1,5 @@ +mod variable_expansion; + use crate::{ align_view, document::{ diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs new file mode 100644 index 000000000..d86a8b1f2 --- /dev/null +++ b/helix-view/src/editor/variable_expansion.rs @@ -0,0 +1,147 @@ +use crate::Editor; +use std::borrow::Cow; + +impl Editor { + pub fn expand_variables<'a>(&self, input: &'a str) -> anyhow::Result> { + let (view, doc) = current_ref!(self); + let shell = &self.config().shell; + + let mut output: Option = 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" => doc + .path() + .and_then(|it| { + it.file_name().and_then(|it| it.to_str()) + }) + .unwrap_or(crate::document::SCRATCH_BUFFER_NAME) + .to_owned(), + "filename" => doc + .path() + .and_then(|it| it.to_str()) + .unwrap_or(crate::document::SCRATCH_BUFFER_NAME) + .to_owned(), + "dirname" => doc + .path() + .and_then(|p| p.parent()) + .and_then(std::path::Path::to_str) + .unwrap_or(crate::document::SCRATCH_BUFFER_NAME) + .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(), + "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_variables(&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)), + } + } +} From 11a11fb4f2ff62502021919b01a46195149475fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Wed, 3 Jul 2024 09:39:58 +0200 Subject: [PATCH 02/14] command expansion: parsing expansions in shellwords accordingly. --- helix-core/src/shellwords.rs | 166 +++++++++++--------- helix-term/src/commands.rs | 3 - helix-term/src/commands/typed.rs | 25 ++- helix-view/src/editor/variable_expansion.rs | 18 ++- 4 files changed, 123 insertions(+), 89 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 9d873c366..9a5ab37cb 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -45,88 +45,114 @@ fn from(input: &'a str) -> Self { 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 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 - } - '\'' => { - end = i; - Quoted - } - '\\' => { - if cfg!(unix) { - escaped.push_str(&input[unescaped_start..i]); - unescaped_start = i + 1; - UnquotedEscaped - } else { - OnWhitespace + if !inside_variable_expansion { + if c == '%' { + //%sh{this "should" be escaped} + if let Some(t) = input.get(i + 1..i + 3) { + if t == "sh" { + inside_variable_expansion = true; } } - 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 + //%{this "should" be escaped} + if let Some(t) = input.get(i + 1..i + 2) { + if t == "{" { + inside_variable_expansion = true; } } - 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 { + } + } else { + if c == '}' { + inside_variable_expansion = false; + } + } + + state = if !inside_variable_expansion { + match state { + OnWhitespace => match c { + '"' => { + end = i; Dquoted } - } - '"' => { - end = i; - OnWhitespace - } - _ => Dquoted, - }, - DquoteEscaped => 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; + 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 + } + } + '\'' => { + 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(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 48da41835..6d8bce9e5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -225,11 +225,8 @@ pub fn execute(&self, cx: &mut Context) { scroll: None, }; - let args = args.join(" "); match cx.editor.expand_variables(&args) { Ok(args) => { - let args = args.split_whitespace(); - let args: Vec> = args.map(Cow::Borrowed).collect(); if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { cx.editor.set_error(format!("{}", e)); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 25e6efd44..c74d82cb8 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3194,17 +3194,6 @@ pub(super) fn command_mode(cx: &mut Context) { } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let input: Cow = if event == PromptEvent::Validate { - match cx.editor.expand_variables(input) { - Ok(args) => args, - Err(e) => { - cx.editor.set_error(format!("{}", e)); - return; - } - } - } else { - Cow::Borrowed(input) - }; let parts = input.split_whitespace().collect::>(); if parts.is_empty() { return; @@ -3221,8 +3210,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.as_ref()); - let args = shellwords.words(); - + let words = shellwords.words().to_vec(); + let args = if event == PromptEvent::Validate { + match cx.editor.expand_variables(&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) { cx.editor.set_error(format!("{}", e)); } diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index d86a8b1f2..f79d24c0e 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -2,7 +2,20 @@ use std::borrow::Cow; impl Editor { - pub fn expand_variables<'a>(&self, input: &'a str) -> anyhow::Result> { + pub fn expand_variables<'a>( + &self, + args: &'a Vec>, + ) -> anyhow::Result>> { + let mut output = Vec::with_capacity(args.len()); + for arg in args { + if let Ok(s) = self.expand_arg(arg) { + output.push(s); + } + } + + Ok(output) + } + fn expand_arg<'a>(&self, input: &'a str) -> anyhow::Result> { let (view, doc) = current_ref!(self); let shell = &self.config().shell; @@ -81,8 +94,7 @@ pub fn expand_variables<'a>(&self, input: &'a str) -> anyhow::Result Date: Sun, 14 Jul 2024 15:39:18 +0200 Subject: [PATCH 03/14] fixing shellwords --- helix-core/src/shellwords.rs | 8 +++----- helix-term/src/commands/typed.rs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 9a5ab37cb..2a1cb0801 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -67,10 +67,8 @@ fn from(input: &'a str) -> Self { } } } - } else { - if c == '}' { - inside_variable_expansion = false; - } + } else if c == '}' { + inside_variable_expansion = false; } state = if !inside_variable_expansion { @@ -95,7 +93,7 @@ fn from(input: &'a str) -> Self { } c if c.is_ascii_whitespace() => { end = i; - Unquoted + OnWhitespace } _ => Unquoted, }, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 428a7cfa9..50d63fdd7 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3241,7 +3241,7 @@ 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.as_ref()); + let shellwords = Shellwords::from(input); let words = shellwords.words().to_vec(); let args = if event == PromptEvent::Validate { match cx.editor.expand_variables(&words) { From 0e6144567c4d6d479f9ac1e851440283242a9ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Mon, 15 Jul 2024 10:07:19 +0200 Subject: [PATCH 04/14] fixing nested expansions --- helix-core/src/shellwords.rs | 11 +++++++++-- helix-term/tests/test/commands/variable_expansion.rs | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 2a1cb0801..0a8689a8b 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -46,7 +46,7 @@ fn from(input: &'a str) -> Self { 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; @@ -57,18 +57,25 @@ fn from(input: &'a str) -> Self { //%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; } } //%{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; } } } } else if c == '}' { - inside_variable_expansion = false; + nested_variable_expansion_count -= 1; + if nested_variable_expansion_count == 0 { + inside_variable_expansion = false; + } + } else if c == '{' { + nested_variable_expansion_count += 1; } state = if !inside_variable_expansion { diff --git a/helix-term/tests/test/commands/variable_expansion.rs b/helix-term/tests/test/commands/variable_expansion.rs index a0935c426..1d644cb6c 100644 --- a/helix-term/tests/test/commands/variable_expansion.rs +++ b/helix-term/tests/test/commands/variable_expansion.rs @@ -17,6 +17,7 @@ async fn test_variable_expansion() -> anyhow::Result<()> { false, ) .await?; + let mut app = AppBuilder::new().build()?; let mut app = AppBuilder::new().build()?; From c2172847b2a55d0204e541ab5521bc15aa7e971b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Tue, 30 Jul 2024 23:03:22 +0200 Subject: [PATCH 05/14] adding tests --- .../tests/test/commands/variable_expansion.rs | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/helix-term/tests/test/commands/variable_expansion.rs b/helix-term/tests/test/commands/variable_expansion.rs index 1d644cb6c..61580d7e4 100644 --- a/helix-term/tests/test/commands/variable_expansion.rs +++ b/helix-term/tests/test/commands/variable_expansion.rs @@ -1,5 +1,5 @@ use super::*; - +use std::borrow::Cow; #[tokio::test(flavor = "multi_thread")] async fn test_variable_expansion() -> anyhow::Result<()> { { @@ -19,8 +19,6 @@ async fn test_variable_expansion() -> anyhow::Result<()> { .await?; let mut app = AppBuilder::new().build()?; - let mut app = AppBuilder::new().build()?; - test_key_sequence( &mut app, Some(":echo %{basename}"), @@ -128,6 +126,57 @@ async fn test_variable_expansion() -> anyhow::Result<()> { false, ) .await?; + + let mut app = AppBuilder::new().build()?; + + test_key_sequence( + &mut app, + Some(":echo %sh{echo %{filename}}"), + 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(":echo %sh{echo %{filename} %{linenumber}}"), + 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(":echo %sh{echo %{filename} %sh{echo %{filename}}}"), + 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(()) From 5bda25cf30d3db2d67eba2c91bb818b7cc65f007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Wed, 31 Jul 2024 13:37:19 +0200 Subject: [PATCH 06/14] fixing shellwords + correct command expansion in mappablecommands --- helix-core/src/shellwords.rs | 66 +++++++++++++++++++++++++----------- helix-term/src/commands.rs | 31 +++++++++-------- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 0a8689a8b..ca761e0d7 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -40,7 +40,6 @@ 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(); @@ -52,30 +51,27 @@ fn from(input: &'a str) -> Self { let mut end = 0; for (i, c) in input.char_indices() { - if !inside_variable_expansion { - 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; - } - } - //%{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 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; } } - } else if c == '}' { + //%{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 c == '}' { nested_variable_expansion_count -= 1; if nested_variable_expansion_count == 0 { inside_variable_expansion = false; } - } else if c == '{' { - nested_variable_expansion_count += 1; } state = if !inside_variable_expansion { @@ -266,6 +262,38 @@ fn test_normal() { // 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)] diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 0ee03705f..661c10560 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -217,22 +217,25 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { - let args: Vec> = 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, - }; + let mut args: Vec> = args.iter().map(Cow::from).collect(); + let joined_args = vec![Cow::from(args.join(" "))]; + if let Ok(expanded_args) = cx.editor.expand_variables(&joined_args) { + args = expanded_args + .first() + .unwrap() + .split(' ') + .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, + }; - match cx.editor.expand_variables(&args) { - Ok(args) => { - if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) - { - cx.editor.set_error(format!("{}", e)); - } + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { + cx.editor.set_error(format!("{}", e)); } - Err(err) => cx.editor.set_error(err.to_string()), } } } From 0a655d9978ebeafb2221ddd3509db66f7a6cdd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Wed, 31 Jul 2024 14:34:31 +0200 Subject: [PATCH 07/14] correct handling of commands without args inside of MappableCommands --- helix-term/src/commands.rs | 41 ++++++++++++--------- helix-term/src/commands/typed.rs | 2 +- helix-view/src/editor/variable_expansion.rs | 10 +++-- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 661c10560..564e46e70 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -217,26 +217,31 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { Self::Typable { name, args, doc: _ } => { - let mut args: Vec> = args.iter().map(Cow::from).collect(); - let joined_args = vec![Cow::from(args.join(" "))]; - if let Ok(expanded_args) = cx.editor.expand_variables(&joined_args) { - args = expanded_args - .first() - .unwrap() - .split(' ') - .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 args: Vec> = 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()) { + let mut cx = compositor::Context { + editor: cx.editor, + jobs: cx.jobs, + scroll: None, + }; + + if let Err(e) = + (command.fun)(&mut cx, &expanded_args[..], PromptEvent::Validate) + { + cx.editor.set_error(format!("{}", e)); + } } } Self::Static { fun, .. } => (fun)(cx), diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 50d63fdd7..2e56382e1 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3244,7 +3244,7 @@ pub(super) fn command_mode(cx: &mut Context) { let shellwords = Shellwords::from(input); let words = shellwords.words().to_vec(); let args = if event == PromptEvent::Validate { - match cx.editor.expand_variables(&words) { + match cx.editor.expand_variables_in_vec(&words) { Ok(args) => args, Err(e) => { cx.editor.set_error(format!("{}", e)); diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index f79d24c0e..675c9a5a0 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -2,20 +2,20 @@ use std::borrow::Cow; impl Editor { - pub fn expand_variables<'a>( + pub fn expand_variables_in_vec<'a>( &self, args: &'a Vec>, ) -> anyhow::Result>> { let mut output = Vec::with_capacity(args.len()); for arg in args { - if let Ok(s) = self.expand_arg(arg) { + if let Ok(s) = self.expand_variable_in_string(arg) { output.push(s); } } Ok(output) } - fn expand_arg<'a>(&self, input: &'a str) -> anyhow::Result> { + pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result> { let (view, doc) = current_ref!(self); let shell = &self.config().shell; @@ -94,7 +94,9 @@ fn expand_arg<'a>(&self, input: &'a str) -> anyhow::Result> { } if let Some(o) = output.as_mut() { - let body = self.expand_arg(&input[index + 4..end])?; + 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 { From b5cae57b1e9a8e14f18f2df9fb5d3162aedb740d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Thu, 1 Aug 2024 00:07:31 +0200 Subject: [PATCH 08/14] adding some other expansions --- book/src/commands.md | 21 ++++++++++++--------- helix-view/src/editor/variable_expansion.rs | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/book/src/commands.md b/book/src/commands.md index e4647535e..6cdcfce2a 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -7,15 +7,18 @@ # Commands ## 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}` | The name and extension of the currently focused file. | -| `%{filename}` | The absolute path of the currently focused file. | -| `%{dirname}` | The absolute path of the parent directory of the currently focused file. | -| `%{cwd}` | The absolute path of the current working directory of Helix. | -| `%{linenumber}` | The line number where the primary cursor is positioned. | -| `%{selection}` | The text selected by the primary cursor. | -| `%sh{cmd}` | Executes `cmd` with the default shell and returns the command output, if any. | +| Variable | Description | +| --- | --- | +| `%{basename}` | The name and extension of the currently focused file. | +| `%{filename}` | The absolute path of the currently focused file. | +| `%{ext}` | The extension of the current file | +| `%{lang}` | The language of the current file | +| `%{dirname}` | The absolute path of the parent directory of the currently focused file. | +| `%{cwd}` | The absolute path of the current working directory of Helix. | +| `%{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 diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index 675c9a5a0..fbed254fc 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -67,6 +67,22 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result (doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)) + + 1) + .to_string(), + "lang" => doc.language_name().unwrap_or("text").to_string(), + "ext" => match doc.relative_path() { + Some(path) => path + .to_string_lossy() + .split(".") + .last() + .unwrap_or("") + .to_string(), + _ => "".to_string(), + }, "selection" => doc .selection(view.id) .primary() From e774706440c3098e1c61b96d1728911830c72d2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Fri, 2 Aug 2024 23:00:47 +0200 Subject: [PATCH 09/14] using rust builtin extension function for %{ext} --- helix-view/src/editor/variable_expansion.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index fbed254fc..ec16dc672 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -74,15 +74,12 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result doc.language_name().unwrap_or("text").to_string(), - "ext" => match doc.relative_path() { - Some(path) => path - .to_string_lossy() - .split(".") - .last() - .unwrap_or("") - .to_string(), - _ => "".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() From 2f98016038fa03ef24a0f7ff44bbbb6a4b960fcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Thu, 12 Sep 2024 08:35:05 +0200 Subject: [PATCH 10/14] adding relative filename expansion --- book/src/commands.md | 26 +++++++++++---------- helix-view/src/editor/variable_expansion.rs | 21 +++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/book/src/commands.md b/book/src/commands.md index 6cdcfce2a..8a6ba8bdb 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -7,18 +7,20 @@ # Commands ## 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}` | The name and extension of the currently focused file. | -| `%{filename}` | The absolute path of the currently focused file. | -| `%{ext}` | The extension of the current file | -| `%{lang}` | The language of the current file | -| `%{dirname}` | The absolute path of the parent directory of the currently focused file. | -| `%{cwd}` | The absolute path of the current working directory of Helix. | -| `%{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. | +| Variable | Description | +| --- | --- | +| `%{basename}` | The name and extension of the currently focused file. | +| `%{filename}` | The absolute path of the currently focused file. | +| `%{filename:rel}` | The relative path of the file according to the current working directory (will give absolute path if the file is not a child of the current working directory) | +| `%{filename:git_rel}` | The relative path of the file according to the git repo, or current working directory if not inside a git repo. (will give absolute path if the file is not a child of the git directory or the cwd) | +| `%{ext}` | The extension of the current file | +| `%{lang}` | The language of the current file | +| `%{dirname}` | The absolute path of the parent directory of the currently focused file. | +| `%{cwd}` | The absolute path of the current working directory of Helix. | +| `%{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 diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index ec16dc672..8accfb064 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -51,6 +51,27 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result { + // This will get git 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" => doc .path() .and_then(|p| p.parent()) From 385ea7c07e9f742e68a148835ccf3cb8c43112d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Thu, 12 Sep 2024 09:18:56 +0200 Subject: [PATCH 11/14] adding git_repo variable --- book/src/commands.md | 11 ++++++----- helix-view/src/editor/variable_expansion.rs | 5 +++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/book/src/commands.md b/book/src/commands.md index 8a6ba8bdb..bd7f822cf 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -10,13 +10,14 @@ ## Using variables in typed commands and mapped shortcuts | Variable | Description | | --- | --- | | `%{basename}` | The name and extension of the currently focused file. | -| `%{filename}` | The absolute path of the currently focused file. | -| `%{filename:rel}` | The relative path of the file according to the current working directory (will give absolute path if the file is not a child of the current working directory) | -| `%{filename:git_rel}` | The relative path of the file according to the git repo, or current working directory if not inside a git repo. (will give absolute path if the file is not a child of the git directory or the cwd) | -| `%{ext}` | The extension of the current file | -| `%{lang}` | The language of the current file | | `%{dirname}` | The absolute path of the parent directory of the currently focused file. | | `%{cwd}` | The absolute path of the current working directory of Helix. | +| `%{git_repo}` | The absolute path of the git repository helix is opened in. Fallback to `cwd` if not inside a git repository| +| `%{filename}` | 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:git_rel}` | The relative path of the file according to `git_repo` (will give absolute path if the file is not a child of the git 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. | diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index 8accfb064..85985a177 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -78,6 +78,11 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result helix_loader::find_workspace() + .0 + .to_str() + .unwrap_or("") + .to_owned(), "cwd" => helix_stdx::env::current_working_dir() .to_str() .unwrap() From 755c51fb6de68e80a77f9414b9e375b17554ce4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Mon, 16 Sep 2024 13:21:56 +0200 Subject: [PATCH 12/14] fixing cursor colunm --- helix-view/src/editor/variable_expansion.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index 85985a177..e50bb0ffb 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -1,3 +1,5 @@ +use helix_core::coords_at_pos; + use crate::Editor; use std::borrow::Cow; @@ -93,12 +95,14 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result (doc - .selection(view.id) - .primary() - .cursor(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() From c0666d1219a82cd4e850a30ca1a7f18294c58e9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Mon, 11 Nov 2024 14:16:16 +0100 Subject: [PATCH 13/14] adding shorthands --- book/src/commands.md | 31 +++++++++++---------- helix-view/src/editor/variable_expansion.rs | 6 ++-- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/book/src/commands.md b/book/src/commands.md index bd7f822cf..a57769757 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -7,21 +7,21 @@ # Commands ## 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}` | The name and extension of the currently focused file. | -| `%{dirname}` | The absolute path of the parent directory of the currently focused file. | -| `%{cwd}` | The absolute path of the current working directory of Helix. | -| `%{git_repo}` | The absolute path of the git repository helix is opened in. Fallback to `cwd` if not inside a git repository| -| `%{filename}` | 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:git_rel}` | The relative path of the file according to `git_repo` (will give absolute path if the file is not a child of the git 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. | +| 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. | +| `%{git_repo}` | The absolute path of the git repository helix is opened in. Fallback to `cwd` if not inside a git 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:git_rel}` | The relative path of the file according to `git_repo` (will give absolute path if the file is not a child of the git 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 @@ -29,3 +29,4 @@ ### Example # Print blame info for the line where the main cursor is. C-b = ":echo %sh{git blame -L %{linenumber} %{filename}}" ``` + diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index e50bb0ffb..ace1f1a14 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -41,14 +41,14 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result doc + "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" => doc + "filename" | "f" => doc .path() .and_then(|it| it.to_str()) .unwrap_or(crate::document::SCRATCH_BUFFER_NAME) @@ -74,7 +74,7 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result doc + "dirname" | "d" => doc .path() .and_then(|p| p.parent()) .and_then(std::path::Path::to_str) From d3927d8da3eb1964647380bfb5c09efb16560180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Sat, 16 Nov 2024 13:29:34 +0100 Subject: [PATCH 14/14] renaming git_repo to repo --- book/src/commands.md | 4 ++-- helix-view/src/editor/variable_expansion.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/book/src/commands.md b/book/src/commands.md index a57769757..913090713 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -12,10 +12,10 @@ ## Using variables in typed commands and mapped shortcuts | `%{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. | -| `%{git_repo}` | The absolute path of the git repository helix is opened in. Fallback to `cwd` if not inside a git repository| +| `%{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:git_rel}` | The relative path of the file according to `git_repo` (will give absolute path if the file is not a child of the git directory or the cwd) | +| `%{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. | diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index ace1f1a14..9d1b5f53c 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -53,8 +53,8 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result { - // This will get git repo root or cwd if not inside a git repo. + "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| { @@ -80,7 +80,7 @@ pub fn expand_variable_in_string<'a>(&self, input: &'a str) -> anyhow::Result helix_loader::find_workspace() + "repo" => helix_loader::find_workspace() .0 .to_str() .unwrap_or("")