Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom typable commands #4423

Open
the-mikedavis opened this issue Oct 22, 2022 · 10 comments · May be fixed by #12320
Open

Custom typable commands #4423

the-mikedavis opened this issue Oct 22, 2022 · 10 comments · May be fixed by #12320
Labels
A-helix-term Area: Helix term improvements C-enhancement Category: Improvements S-needs-discussion Status: Needs discussion or design.

Comments

@the-mikedavis
Copy link
Member

Regular commands can be rebound but typable commands (anything entered in command mode, :) can't be modified in any way.

You might use custom typable commands to implement file operations in conjunction with #3134 for example. You might define :mv as :sh mv $file $1 or :rm as :sh rm $1. (The syntax to use for variables, or whether to use variables at all should be discussed.)

Custom typable commands could also be used to create custom abbreviations for commands.

@rkshthrmsh

This comment was marked as off-topic.

@the-mikedavis

This comment was marked as off-topic.

@kendfss
Copy link

kendfss commented Apr 21, 2023

Does the editor already support custom commands and just not typables?

I was thinking it would be nice to have something like:

[custom.commands]
bring_line_up = {
  cmds = [ 'push_register', 'extend_line', 'delete_selection', 'move_line_up', 'paste_before', 'pop_register' ] # semicolon separates operations' names from parameters' arguments. 
  desc = "swap line with above"
  mode = editor || prompt || both
}

@kendfss
Copy link

kendfss commented Apr 21, 2023

or set the mode via - vs _. I think that would be a more cumbersome implementation to maintain though.

@the-mikedavis
Copy link
Member Author

You can already map keys to lists of commands (https://docs.helix-editor.com/master/remapping.html) to create custom non-typable commands. You can also rebind the normal_mode, insert_mode and select_mode commands to - (minus when configuring this in config.toml) and/or _. This issue is only about creating typable commands: ones entered in command mode with : that optionally take arguments.

@kendfss
Copy link

kendfss commented Apr 23, 2023

Could separate func name from params with ;
Though, I was/am assuming what you wanted could be reproduced by sequence(s) of the builtin ones. Sorry if not.

@the-mikedavis
Copy link
Member Author

Combining sequences of typable commands is covered by this. For example #6857 might be written as:

# Note: this is not implemented.
[commands]
":wcb" = [":write", ":buffer-close"]

Handling the arguments of the commands is also important for the design of this feature though. If you wanted :wcb to take the filename to write, you would need a syntax for where to place the argument (in :write's arguments rather than :buffer-close). Maybe this could be expressed as [":write %arg{1}", ":buffer-close"] (using syntax like in #3393).

There are nuances to this though (optional arguments, aliases, command names / docs) so maybe this issue is better handled by plugins once the plugin system exists instead. We don't want to create new programming languages in the config TOML.

@the-mikedavis the-mikedavis added the S-needs-discussion Status: Needs discussion or design. label May 3, 2023
@kendfss
Copy link

kendfss commented May 9, 2023

That looks so sick! Imagine if we could define all key remaps like that! We could just escape for "\\space"
What about using an environment-variable-like syntax for those sorts of things. Just $FILE/\\$PWD to escape to pass literals to terminal (or vice versa/option for that)?
I think a great thing about helix is not needing plugins, lol. That said, I do think, it would be good to have a way to share snippets.

edit:
I've not yet read on the state of helix plugins, so forgive this ramble:
Like if we get imports working, we can separate public things from private things. Just publish everything in a designated file to some server which keeps a database of usernames and commands. we could have something like ":fetch user.cmd" then you just use my_remap = "user.cmd" in your config. we could just have packages :fetch package.cmd or :fetch package. Could also be cool to have :share command|file_aka_package
It could also be set up in a more distributed way using repository hosts, in which case you might have :fetch host.user.cmd and use git to clone (or just scrape the relevant file; if designated) and extract the desired command. I guess :fetch host.user would be a convenient option too for working through ssh. Git could probably handle authentication and whatnot.

That's all to say, plugin systems can get really hairy. They're complex, and easy to break on user-side. Just save ourselves time and keep it snippety, lol.

@rcorre
Copy link
Contributor

rcorre commented May 10, 2023

What about using an environment-variable-like syntax for those sorts of things. Just $FILE/\$PWD to escape to pass literals to terminal (or vice versa/option for that)?

#3134

@RoloEdits
Copy link
Contributor

RoloEdits commented Dec 22, 2024

Started to prototype this, and am finding that this too is hampered by #5555. The custom commands need to be part of the editor config, so cannot directly store a TypableCommand or a CommandSignature. Its not the end of the world, just have to get them after, but indirection has to be done to get what is needed, and there is no way to offer config validation on if the commands are valid commands like how it checks the other parts of the config. Can only know if you were to try to run them.

The implimentation needs to build off of #12288 and #11164 which themselves build off of #11149, so still a ways out, but I think I have a good framework going forward.

Config

Basic

The config will get a [commands] table for which the custom commands will go. At the most basic level of usage, an alias for a single command, it would look like this:

[commands]
"waq" = ":write --all --quit"

This would provide no completions, only showing up in the list of commands. The prompt would be bare, only the name and the mapping, no description or what it accepts:

waq:

maps:
    :write --all --quit

Advanced

The most advanced usage is what I hope to be possible to implement, but as I haven't actually done it, jury is still out on what's actually feasible:

[commands]
"wcd!" = {
commands = [ ":write --force %{arg}", ":cd %sh{ %{arg} | path dirname }" ],
desc= "writes buffer forcefully, then changes to its directory"
completions = "write"
accepts = "<path>"
}

This would show the commands from the list of commands like before, but now offers the ability to run multiple commands in a chain, provide positional arguments, a description, indicate what it accepts, as well as which completer to run when using the command, sourced from the name of an existing typable command.

The prompt now looks like this:

wcd! <path>: writes buffer forcefully, then changes to its directory

maps:
    :write --force %{arg} -> :cd %sh{ %{arg} | path dirname }

%{arg} represents a positional argument that is provided when using the command.

:wcd! parent/sub/sub/file.txt
      ^^^^^^^^^^^^^^^^^^^^^^^

These could be given numbers as well: %{arg:1}. %{arg} would be equal to %{arg:0}. This is able to take advantage of the Args iterator that #11149 introduces, just taking the number provided and pass it to nth.

Nesting

Custom commands can also be used in other custom commands, though with some limitations to be mindful of:

[commands]
"wcd!" = {
# If the above was instead `wcd`, with no `--force`
# and you tried to use it in an `wcd!` impl,
# you would run into an issue with the other commands
# single positional `%{arg}.
#
# Notice even if we tried to pass the flag and the path as
# one argument, using quotes to wrap it, we run into an
# issue with the path dirname evaluation.
# 
# When it all gets expanded write should look fine:
# write --force parent/sub/sub/file.txt
# 
# The issue is with the `:cd`
# :cd  %sh {--force parent/sub/sub/file.txt | path dirname }
# which would error in nushell
commands = ":wcd '--force %{arg}'",
desc= "writes buffer forcefully, then changes to its directory"
completions = "write"
accepts = "<path>"
}

The prompt would look like so, again if the first impl was a wcd, no force.

wcd! <path>: writes buffer forcefully, then changes to its directory
maps:
    :wcd! -> wcd `--force %{arg}`

In short, it works, but the design of the commands must be able to compose.

Implimentation Details

Its is based around a CustomTypableCommand struct:

pub struct CustomTypableCommand {
    pub name: String,
    pub desc: String,
    // TODO: Cannot store a `TypableCommand` directly as this has to live in
    // the editor config.
    pub commands: Vec<String>,
    pub accepts: String,
    // Can `get` from the typable command map and then `clone` the signature
    pub compeltions: String,
}

The config itself would hold a wrapper of this:

pub struct CustomTypableCommands {
    commands: Vec<CustomTypableCommand>,
}

The usage of this would look roughly like:

// Checking against user provided commands first gives priority
// to user defined aliases over the built-in, allowing for overriding.
 if let Some(custom: &CustomTypeableCommand) = cx.editor.config.load().commands.get(shellwords.command()) {
     for command: &str in custom.commands.iter() {
         let shellwords = Shellwords::from(command);

         if let Some(command: &TypeableCommand) = typed::TYPABLE_COMMAND_MAP.get(shellwords.command()) {
             let args = match variables::expand(cx.editor, shellwords.args().raw(), event == PromptEvent::Validate) {
                Ok(args) => args,
                Err(err) => {
                     cx.editor.set_error(format!("{err}"));
                     // short circuit if error
                     return;
                },
             }

             if let Err(err) = (command.fun)(cx, Args::from(&args), command.flags, event) {
                cx.editor.set_error(format!("{err}"));
                     // short circuit if error
                     return;
             }
         } else {
             cx.editor.set_error(format!("command `:{}` is not a valid command", shellwords.command()));
             // short circuit if error
             return;
         }
 } else if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(shellwords.command()) {
    // Current impl
 }

There is more here that would need to be layered in, but this is the gist.

The main points I haven't delved into yet is the parsing from the toml into the struct and add it to the config, which to me is looking like the hardest part (😆). As well as how to provide the command in the list of commands and be able to show the prompt for it properly. Might have to introduce a Prompt trait and refactor to use that to get a string out, but this can be left for later as i'm pretty confident of getting that to work.

Id love some feedback on the toml design, as well as some example usage, some ultra creative ones, to make sure I can design around as much as possible. Like I said at the start, this is all a ways out, but this still has issues to work out anyways, so hopefully if we get these ironed out, and the dependent pull requests merge in without issue, and I can work on this in the next release cycle as a major feature of the release.


Potentially Common Use-Cases

Floating lazygit pane

# theoretical `--eat` flag that eats the popup
":lg" = ":sh --eat wezterm cli spawn --floating-pane lazygit"

Floating terminal pane

# theoretical `--eat` flag that eats the popup
":t[erm]" = ":sh --eat wezterm cli spawn --floating-pane"

Floating yazi pane

# theoretical `--eat` flag that eats the popup
":y[azi]" = ":sh --eat wezterm cli spawn --floating-pane yazi"

Removing current buffer file

# theoretical `--eat` flag that eats the popup
":rm" = ":sh --eat rm %{path}"

Questions

  1. Will this only support running typable commands? Or like the keybinds, it can run any command/macro?
  2. Will there need to be support for passing outputs between each command?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-helix-term Area: Helix term improvements C-enhancement Category: Improvements S-needs-discussion Status: Needs discussion or design.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants