Skip to content

Commit

Permalink
Merge remote-tracking branch 'expansions/command-expansions'
Browse files Browse the repository at this point in the history
  • Loading branch information
postsolar committed Jan 19, 2024
2 parents 20b7d8e + efe6aef commit cf69489
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 5 deletions.
1 change: 1 addition & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,4 @@
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
| `:redraw` | Clear and re-render the whole UI |
| `:move` | Move the current buffer and its corresponding file to a different path |
| `:echo` | Print the processed input to the editor status |
21 changes: 21 additions & 0 deletions book/src/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Selecting and manipulating text with textobjects](#selecting-and-manipulating-text-with-textobjects)
- [Navigating using tree-sitter textobjects](#navigating-using-tree-sitter-textobjects)
- [Moving the selection with syntax-aware motions](#moving-the-selection-with-syntax-aware-motions)
- [Using variables in typed commands and mapped shortcuts](#using-variables-in-typed-commands-and-mapped-shortcuts)
<!--toc:end-->

For a full interactive introduction to Helix, refer to the
Expand Down Expand Up @@ -203,6 +204,26 @@ sibling, the selection will move up the syntax tree and select the previous
element. As a result, using `Alt-p` with a selection on `arg1` will move the
selection to the "func" `identifier`.

## 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}}"
```

[lang-support]: ./lang-support.md
[unimpaired-keybinds]: ./keymap.md#unimpaired
[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif
21 changes: 17 additions & 4 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,29 @@ impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
match &self {
Self::Typable { name, args, doc: _ } => {
let args: Vec<Cow<str>> = 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,
};
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<Cow<str>> = 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());
}
};
}
}
Self::Static { fun, .. } => (fun)(cx),
Expand Down
33 changes: 32 additions & 1 deletion helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2471,6 +2471,18 @@ fn move_buffer(
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 @@ -3078,6 +3090,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: move_buffer,
signature: CommandSignature::positional(&[completers::filename]),
},
TypableCommand {
name: "echo",
aliases: &[],
doc: "Print the processed input to the editor status",
fun: echo,
signature: CommandSignature::all(completers::variables)
},
];

pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
Expand Down Expand Up @@ -3141,6 +3160,18 @@ pub(super) fn command_mode(cx: &mut Context) {
}
}, // completion
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
let input: Cow<str> = 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::<Vec<&str>>();
if parts.is_empty() {
return;
Expand All @@ -3156,7 +3187,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) {
Expand Down
7 changes: 7 additions & 0 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,13 @@ pub mod completers {
})
}

pub fn variables(_: &Editor, input: &str) -> Vec<Completion> {
fuzzy_match(input, helix_view::editor::VARIABLES, false)
.into_iter()
.map(|(name, _)| ((0..), name.to_owned().into()))
.collect()
}

#[derive(Copy, Clone, PartialEq, Eq)]
enum FileMatch {
/// Entry should be ignored
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 @@ -3,6 +3,7 @@ use helix_term::application::Application;
use super::*;

mod movement;
mod variable_expansion;
mod write;

#[tokio::test(flavor = "multi_thread")]
Expand Down
133 changes: 133 additions & 0 deletions helix-term/tests/test/commands/variable_expansion.rs
Original file line number Diff line number Diff line change
@@ -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("<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?;
}

Ok(())
}
3 changes: 3 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
mod variable_expansion;
pub use variable_expansion::VARIABLES;

use crate::{
align_view,
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
Expand Down
Loading

0 comments on commit cf69489

Please sign in to comment.