anuvyklack / hydra.nvim

Create custom submodes and menus
1.04k stars 23 forks source link
lua neovim neovim-plugin

Hydra.nvim

This is the Neovim implementation of the famous Emacs Hydra package.

Description for Poets

Once you summon the Hydra through the prefixed binding (the body + any one head), all heads can be called in succession with only a short extension.

The Hydra is vanquished once Hercules, any binding that isn't the Hydra's head, arrives. Note that Hercules, besides vanquishing the Hydra, will still serve his original purpose, calling his proper command. This makes the Hydra very seamless.

Description for Pragmatics

Imagine you want to change the size of your current window. Vim allows you to do it with <C-w>+, <C-w>-, <C-w><, <C-w>> bindings. So, you have to press <C-w>+<C-w>+<C-w>+<C-w><<C-w><<C-w><... as many times as you need (I know about count prefixes, but I was never fun of them). Hydra allows you to press <C-w> just once and then get access to any <C-w>... bindings without pressing the prefix again: <C-w>+++++--<<<<. Or buffer side scrolling: instead of zlzlzlzlzlzl... press zlllllllllhhhl to freely scroll buffer left and right. Any key other than bind to a hydra will stop hydra state and do what they should.

Hydra also allows assigning a custom hint to such group of keybindings to allows you an easy glance at what you can do.

If you want to quickly understand the concept, you can watch the original Emacs Hydra video demo

Sample Hydras

Side scroll

Simple hydra to scroll screen to the side with auto generated hint.

local Hydra = require('hydra')

Hydra({
   name = 'Side scroll',
   mode = 'n',
   body = 'z',
   heads = {
      { 'h', '5zh' },
      { 'l', '5zl', { desc = '←/→' } },
      { 'H', 'zH' },
      { 'L', 'zL', { desc = 'half screen ←/→' } },
   }
})

Git submode

A full fledged git "submode".

Link to the code

Telescope menu

You can also create a fancy menu to easy recall seldom used mappings.

Link to the code

Frequently used options

Link to the code

More hydras

You can find more hydras in the wiki. Feel free to add your own or edit the existing ones!

Installation

To install with packer use:

use 'anuvyklack/hydra.nvim' 

How to create hydra

To create hydra you need to call Hydra's constructor with input parameters table of the next form:

local Hydra = require('hydra')
Hydra({
    name = "Hydra's name",
    hint = [[...]] -- multiline string
    config = {...}
    mode = 'n',
    body = '<leader>o',
    heads = {...},
})

Each of the fields of this table is described in details below.

name

string (optional)

The name of the hydra. Not necessary, used only in auto-generated hint.

mode

string | string[]\ default: "n"

Mode or modes in which this hydra will exist. Same format as vim.keymap.set() accepts.

body

string (optional)

To summon the hydra you need to press in sequence keys corresponds to body + any head. For example, if body is z and heads are: a, b, c, you can invoke hydra with any of the za, zb, zc keybindings.

Hydra without body can only be summoned through Hydra:activate() method.

config

table

With this table, you can set the behavior of the whole hydra, which later can be customized for each head particularly. Below is a list of all options.


exit

boolean\ default: false\ parent table: config

The exit option (heads can override it) defines what will happen after executing head's command:

foreign_keys

"warn" | "run" | nil\ default: nil\ parent table: config

The foreign_keys option belongs to the body and decides what to do when a key is pressed that doesn't belong to any head:

color

"red" | "amaranth" | "teal" | "pink"\ default: "red"\ parent table: config

The color option is a shortcut for both exit and foreign_keys options and aggregates them in the following way:

| color    | toggle                             |
|----------+------------------------------------|
| red      |                                    |
| blue     | exit = true                        |
| amaranth | foreign_keys = 'warn'              |
| teal     | foreign_keys = 'warn', exit = true |
| pink     | foreign_keys = 'run'               |

It's also a trick to make you instantly aware of the current hydra keys that you're about to press: the keys will be highlighted with the appropriate color.

Note: The exit and foreign_keys options are higher priority than color option and can't be overridden by it. I.e, if manually set values of exit and foreign_keys options contradict the color option value, then exactly thees values will be taken into account and for color option the matching value will be automatically set

More about colors concept

Each hydra head has a basic associated color, red or blue, that determines whether or not the hydra will continue after the head is called:

They may have a reddish or a bluish face that isn't exactly red or blue, but that's what they are underneath.

Overall, the hydra body can have one of five variants of the basic colors: amaranth, teal, pink, red, blue. They (according to basic color) determines the default behavior of all the heads; and determines what happens when a key that is not associated to a head is pressed. The following table summarizes the effects of the different colors.

Body Color Basic color Executing NON-HEAD Executing HEAD
amaranth red Disallow and Continue Continue
teal blue Disallow and Continue Quit
pink red Allow and Continue Continue
red red Allow and Quit Continue
blue blue Allow and Quit Quit
Amaranth color

The amaranth color wasn't chosen by accident because it is the variation of the red color, but it has the sense underneath. According to Wikipedia:

The word amaranth comes from the Greek word amaranton, meaning "unwilting" (from the verb marainesthai, meaning "wilt"). The word was applied to amaranth because it did not soon fade and so symbolized immortality.

Hydras with amaranth body are impossible to quit with any binding except a blue head.

Blue and teal colors

A blue hydra has little sense in Vim since it works exactly like standard Vim multi-key keybinding with addition you can add a custom hint to it.

A teal hydra working the same way, except it blocks all other keys which are not hydra heads, what can be useful.

Pink color

Pink hydra is of a different nature. It is a key-layer inside, so all keys except overwritten are work as usual. Even [count] prefixes.

buffer

true | number\ parent table: config

Define hydra only for particular buffer. If true — the current buffer will be used.

invoke_on_body

boolean\ default: false\ parent table: config

By default, to invoke the hydra you need to press in sequence keys corresponds to body + any non-private head (about private heads see later). This option allows you to summon hydra by pressing only the body keys.

desc

string\ default: [Hydra] + name \ parent table: config

If you set invoke_on_body option, then you can pass here the description for the hydra body key sequence.

on_enter and on_exit

parent table: config

Functions that will be called on enter and on exit hydra.

meta-accessors

Inside the on_enter functions the vim.o, vim.go, vim.bo and vim.wo meta-accessors are redefined to work the way you think they should. If you want some option value to be temporary changed while Hydra is active, you need just set it with one of this meta-accessor in the on_enter function. And that's it. No need to set it back in on_exit function. All other will be done automatically in the backstage.

config = {
    on_enter = function()
       print('Hydra enter')
       vim.bo.modifiable = false  -- temporary set `nomodifiable` while Hydra is active
    end,
    on_exit = function()
       print('Hydra exit')
       -- No need to set modifiable back here
    end
}

on_key

parent table: config

Function that will be called after every hydra head.

timeout

boolean | number parent table: config

The timeout option set a timer after which the hydra will be automatically disabled. Calling any head will refresh the timer. (see :help timeout, :help timeoutlen)

hint

table | false parent table: config

If false — doesn't show hint. Or a table with settings for manually- or auto-generated hint.

Hydra's heads

Each hydra's head has the form:

{ head, rhs, opts }

which is pretty-much compares to the signature of the vim.keymap.set() function.

head

string

The lhs (left-hand-side) of the mapping, i.e the keys you press to call an action.

rhs

string | function | nil

Right-hand-side of the mapping. Can be nil which means just do nothing, but if you also want to pass opts table, you need to pass nil explicitly.

opts

table

A table with head options to tune its behavior.

private

boolean

When the hydra hides (not active), the private head does not bounce outside.

I.e., the private head is unreachable outside of the hydra state.

exit

boolean

Stop the hydra state after executing a command corresponds to such head.

Note: All exit heads are also private.

Note: If no exit head is specified, the <Esc> key will be set by default.

Note: Remind that rhs can be nil, so the pure escape head looks like this:

{ '<Esc>', nil, { exit = true } }
exit_before

boolean

Like the previous option, stops hydra state but BEFORE executing a command corresponds to head.

on_key

boolean

If false config.on_key function won't be executed after this head.

desc

string | false

The description that will be shown in the auto-generated part of the hint. If false won't be show in the hint window

expr, silent

boolean

Built-in map arguments. See:

nowait

boolean

Only relevant for pink hydra. For all others will be skipped. The pink hydra is a layer inside, and Layer binds its keymaps buffer local, which makes flag nowait available. See :help :map-<nowait>.

This allows, for example bind exit key:

config = {
    color = 'pink',
}
...
heads = {
    { 'q', nil, { nowait = true } }
    ...
}

which will exit layer, without waiting &timeoutlen milliseconds for possible continuation.

mode

string | string[]

Overwrite mode field for this particular head. Only relevant for pink hydra, for all others will be ignored.

hint

multiline string

You can create any hint you wish.

horizontal vertical

To highlight a key, just wrap it in underscores. Note that the key must belong to one of the heads. The key will be highlighted with the color that is appropriate to the behavior of the key, i.e. if the key will make the hydra exit, the color will be blue.

To insert an empty character, use ^. It won't be rendered. The only use of it is to have your code aligned as nicely as the result.

You can also create a Lua functions which returns a string and place it in config.hint.funcs table under some key which will be used as a function name. Than you can require this function from the hint wrap its name (key in the table) with %{...}. The result of the function will be inserted in that place in the hint when it will be shown. And later this function will be called every time when hydra head will be pressed and the hint will be updated with the function result string. Some functions are already built-in, you can find them in this file. You may submit or request some others which you think may be useful. The using of this feature is shown in options hydra.

If you pass no hint, then one line hint will be generated automatically. The keys and their descriptions will be placed in the order heads were passed in the heads table. Heads with desc = false in opts table will be skipped.

Every head that won't be found in the manually created hint, will be automatically added at the bottom of the hint window according to rules of auto generated hint.

Public methods

Highlight

Hydra defines next highlight groups with their defaults:

HydraRed        #FF5733
HydraBlue       #5EBCF6
HydraAmaranth   #ff1757
HydraTeal       #00a1a1
HydraPink       #ff55de

Keymap utility functions

Utility functions to use in keymaps. Can be required from the next table:

require('hydra.keymap-util')

Statusline

In the statusline module require('hydra.statusline') there are functions that can help you to integrate Hydra in your statusline:

Drawbacks

[count] is not supported in a red, amaranth and teal hydras (see :help count). But supported in pink hydra since it is a layer.

How it works under the hood

You can read about the internal mechanics in the CONTRIBUTING