Skip to content

Commit

Permalink
adding variable expansion (impl + docs + test)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdaron committed Dec 22, 2024
1 parent 18e3975 commit 77482cd
Show file tree
Hide file tree
Showing 7 changed files with 447 additions and 23 deletions.
30 changes: 30 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,35 @@ 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 @@ -88,3 +88,4 @@
| `: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 |
| `:read`, `:r` | Load a file into buffer |
| `:echo` | Print the processed input to the editor status |
18 changes: 14 additions & 4 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub(crate) mod dap;
pub(crate) mod expansions;
pub(crate) mod lsp;
pub(crate) mod typed;

Expand Down Expand Up @@ -251,10 +252,17 @@ impl MappableCommand {
scroll: None,
};

if let Err(err) =
(command.fun)(&mut cx, Args::from(args), PromptEvent::Validate)
{
cx.editor.set_error(format!("{err}"));
match expand_args(cx.editor, args.into(), true) {
Ok(args) => {
if let Err(err) = (command.fun)(
&mut cx,
Args::from(args.as_ref()),
PromptEvent::Validate,
) {
cx.editor.set_error(format!("{err}"));
}
}
Err(e) => cx.editor.set_error(format!("{e}")),
}
}
}
Expand Down Expand Up @@ -716,6 +724,8 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem

use helix_core::movement::{move_horizontally, move_vertically};

use self::expansions::expand_args;

fn move_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
}
Expand Down
194 changes: 194 additions & 0 deletions helix-term/src/commands/expansions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use std::borrow::Cow;

use anyhow::Result;
use helix_core::coords_at_pos;
use helix_view::Editor;

pub fn expand_args<'a>(
editor: &Editor,
input: Cow<'a, str>,
expand_sh: bool,
) -> Result<Cow<'a, str>> {
let (view, doc) = current_ref!(editor);
let shell = &editor.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::helix_view::document::SCRATCH_BUFFER_NAME)
.to_owned(),
"filename" | "f" => doc
.path()
.and_then(|it| it.to_str())
.unwrap_or(crate::helix_view::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::helix_view::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::helix_view::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::helix_view::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' && expand_sh {
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 = expand_args(
editor,
Cow::Borrowed(&input[index + 4..end]),
expand_sh,
)?;

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(input),
}
}
42 changes: 24 additions & 18 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::ops::Deref;

use crate::job::Job;

use super::expansions::expand_args;
use super::*;

use helix_core::fuzzy::fuzzy_match;
Expand Down Expand Up @@ -3122,28 +3123,33 @@ pub(super) fn command_mode(cx: &mut Context) {
}
}, // completion
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
let shellwords = Shellwords::from(input);
let command = shellwords.command();
match expand_args(cx.editor, input.into(), true) {
Ok(args) => {
let shellwords = Shellwords::from(args.as_ref());
let command = shellwords.command();

if command.is_empty() {
return;
}
if command.is_empty() {
return;
}

// If input is `:NUMBER`, interpret as line number and go there.
if command.parse::<usize>().is_ok() {
if let Err(err) = typed::goto_line_number(cx, Args::from(command), event) {
cx.editor.set_error(format!("{err}"));
}
return;
}
// If input is `:NUMBER`, interpret as line number and go there.
if command.parse::<usize>().is_ok() {
if let Err(err) = typed::goto_line_number(cx, Args::from(command), event) {
cx.editor.set_error(format!("{err}"));
}
return;
}

// Handle typable commands
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}"));
// Handle typable commands
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}"));
}
} else if event == PromptEvent::Validate {
cx.editor.set_error(format!("no such command: '{command}'"));
}
}
} else if event == PromptEvent::Validate {
cx.editor.set_error(format!("no such command: '{command}'"));
Err(e) => cx.editor.set_error(format!("{e}")),
}
},
);
Expand Down
2 changes: 1 addition & 1 deletion helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use super::*;

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

#[tokio::test(flavor = "multi_thread")]
async fn test_selection_duplication() -> anyhow::Result<()> {
// Forward
Expand Down
Loading

0 comments on commit 77482cd

Please sign in to comment.