*mini.keymap* Special key mappings

MIT License Copyright (c) 2025 Evgeni Chasnovski

------------------------------------------------------------------------------
                                                                    *MiniKeymap*
Features:

- Map keys to perform configurable multi-step actions: if condition for step
  one is true - execute step one action, else check step two, and so on until
  falling back to executing original keys. This is usually referred to as
  "smart" keys (like "smart tab"). See |MiniKeymap.map_multistep()|.

  There are many built-in steps targeted for Insert mode mappings of special
  keys like <Tab>, <S-Tab>, <CR>, and <BS>:
  - Navigate and accept |popupmenu-completion|. Useful for |mini.completion|.
  - Navigate and expand |mini.snippets|.
  - Execute <CR> and <BS> respecting |mini.pairs|.
  - Jump before/after current tree-sitter node.
  - Jump before opening and after closing characters (brackets and quotes).
  - Increase/decrease indent when cursor is inside of it.
  - Delete all whitespace to the left ("hungry backspace").
  - Navigate |vim.snippet|.
  - Navigate and accept in [hrsh7th/nvim-cmp](https://github.com/hrsh7th/nvim-cmp) completion.
  - Navigate and accept in [Saghen/blink.cmp](https://github.com/Saghen/blink.cmp) completion.
  - Navigate and expand [L3MON4D3/LuaSnip](https://github.com/L3MON4D3/LuaSnip) snippets.
  - Execute <CR> and <BS> respecting [windwp/nvim-autopairs](https://github.com/windwp/nvim-autopairs).

- Map keys as "combo": each key acts immediately plus execute extra action if
  all are typed within configurable delay between each other.
  See |MiniKeymap.map_combo()|. Some of the common use cases include:
    - Map insertable keys (like "jk", "kj") in Insert and Command-line mode
      to exit into Normal mode.
    - Fight against bad habits of pressing the same navigation key by showing
      a notification if there are too many of them pressed in a row.

Sources with more details:
- |MiniKeymap-examples|

# Setup ~

This module doesn't need setup, but it can be done to improve usability.
Setup with `require('mini.keymap').setup({})` (replace `{}` with your `config`
table). It will create global Lua table `MiniKeymap` which you can use for
scripting or manually (with `:lua MiniKeymap.*`).

See |MiniKeymap.config| for `config` structure and default values.

This module doesn't have runtime options, so using `vim.b.minikeymap_config`
will have no effect here.

# Comparisons ~

- [max397574/better-escape.nvim](https://github.com/max397574/better-escape.nvim):
    - Mostly similar to |MiniKeymap.map_combo()| with a different approach
      to creating mappings.
    - Mostly targeted for Insert mode mappings as pressed keys get removed
      automatically after typed. This module allows more general cases while
      requiring explicit removal of keys (usually via explicit `<BS><BS>`).

- [abecodes/tabout.nvim](https://github.com/abecodes/tabout.nvim):
    - Similar general idea as in `'jump_{after,before}_tsnode'` steps
      of |MiniKeymap.map_multistep()|.
    - Works only with enabled tree-sitter parser. This module provides
      fallback via 'jump_after_close' and 'jump_before_open' that work
      without tree-sitter parser.
    - 'tabout.nvim' has finer control of how the tree-sitter node movement
      is done, while this module has "jump outside of current node" behavior.

# Disabling ~

To disable acting in mappings, set `vim.g.minikeymap_disable` (globally) or
`vim.b.minikeymap_disable` (for a buffer) to `true`. Considering high number
of different scenarios and customization intentions, writing exact rules
for disabling module's functionality is left to user.
See |mini.nvim-disabling-recipes| for common recipes.

------------------------------------------------------------------------------
                                                           *MiniKeymap-examples*
# Multi-step ~

See |MiniKeymap.map_multistep()| for a general description of how multi-step
mappings work and what built-in steps are available.

Setup that works well with |mini.completion| and |mini.pairs|: >lua

  local map_multistep = require('mini.keymap').map_multistep
  map_multistep('i', '<Tab>',   { 'pmenu_next' })
  map_multistep('i', '<S-Tab>', { 'pmenu_prev' })
  map_multistep('i', '<CR>',    { 'pmenu_accept', 'minipairs_cr' })
  map_multistep('i', '<BS>',    { 'minipairs_bs' })
<
Use <Tab> / <S-Tab> to also navigate and expand |mini.snippets|: >lua

  local map_multistep = require('mini.keymap').map_multistep

  local tab_steps = { 'minisnippets_next','minisnippets_expand','pmenu_next' }
  map_multistep('i', '<Tab>', tab_steps)

  local shifttab_steps = { 'minisnippets_prev', 'pmenu_prev' }
  map_multistep('i', '<S-Tab>', shifttab_steps)
<
An extra smart <Tab> and <S-Tab>: >lua

  local map_multistep = require('mini.keymap').map_multistep

  -- NOTE: this will never insert tab, press <C-v><Tab> for that
  local tab_steps = {
    'minisnippets_next', 'minisnippets_expand', 'pmenu_next',
    'jump_after_tsnode', 'jump_after_close',
  }
  map_multistep('i', '<Tab>', tab_steps)

  local shifttab_steps = {
    'minisnippets_prev',  'pmenu_prev',
    'jump_before_tsnode', 'jump_before_open',
  }
  map_multistep('i', '<S-Tab>', shifttab_steps)
<
Navigation in active |vim.snippet| session also requires mapping in |Select-mode|: >lua

  local map_multistep = require('mini.keymap').map_multistep
  map_multistep({ 'i', 's' }, '<Tab>',   { 'vimsnippet_next', 'pmenu_next' })
  map_multistep({ 'i', 's' }, '<S-Tab>', { 'vimsnippet_prev', 'pmenu_prev' })
<
# Combos ~

See |MiniKeymap.map_combo()| for a general description of what is a combo and
more caveats about its usage.

All combos require their left hand side keys to be typed relatively quickly.
To adjust the delay between keys, add `{ delay = 500 }` (use custom value) as
fourth argument.

## "Better escape" to Normal mode ~

Leave into |Normal-mode| without having to reach for <Esc> key: >lua

  -- Support most common modes. This can also contain 't', but would
  -- only mean to press `<Esc>` inside terminal.
  local mode = { 'i', 'c', 'x', 's' }
  require('mini.keymap').map_combo(mode, 'jk', '<BS><BS><Esc>')

  -- To not have to worry about the order of keys, also map "kj"
  require('mini.keymap').map_combo(mode, 'kj', '<BS><BS><Esc>')

  -- Escape into Normal mode from Terminal mode
  require('mini.keymap').map_combo('t', 'jk', '<BS><BS><C-\\><C-n>')
  require('mini.keymap').map_combo('t', 'kj', '<BS><BS><C-\\><C-n>')
<
## Show bad navigation habits ~

Show notification if there is too much movement by repeating same key: >lua

  local notify_many_keys = function(key)
    local lhs = string.rep(key, 5)
    local action = function() vim.notify('Too many ' .. key) end
    require('mini.keymap').map_combo({ 'n', 'x' }, lhs, action)
  end
  notify_many_keys('h')
  notify_many_keys('j')
  notify_many_keys('k')
  notify_many_keys('l')
<
## Fix previous spelling mistake ~

Fix previous spelling mistake (see |[s| and |z=|) without manually leaving
Insert mode: >lua

  local action = '<BS><BS><Esc>[s1z=gi<Right>'
  require('mini.keymap').map_combo('i', 'kk', action)
<
## Hide search highlighting ~

Use double <Esc><Esc> to execute |:nohlsearch|. Although this can also be done
with `nmap <Esc> <Cmd>nohl<CR>`, the combo approach also exists and can be used
to free <Esc> mapping in Normal mode for something else. >lua

  local action = function() vim.cmd('nohlsearch') end
  require('mini.keymap').map_combo({ 'n','i','x','c' }, '<Esc><Esc>', action)
<
## Buffer navigation ~

Replace some movements with easier to type alternatives: >lua

  local map_combo = require('mini.keymap').map_combo
  map_combo({ 'n', 'x' }, 'll', 'g$')
  map_combo({ 'n', 'x' }, 'hh', 'g^')
  map_combo({ 'n', 'x' }, 'jj', '}')
  map_combo({ 'n', 'x' }, 'kk', '{')
<
------------------------------------------------------------------------------
                                                            *MiniKeymap.setup()*
                          `MiniKeymap.setup`({config})
Module setup

Parameters ~
{config} `(table|nil)` Module config table. See |MiniKeymap.config|.

Usage ~
>lua
  require('mini.keymap').setup({}) -- replace {} with your config table
                                   -- needs `keymap` field present
<
------------------------------------------------------------------------------
                                                             *MiniKeymap.config*
                              `MiniKeymap.config`
Defaults ~
>lua
  MiniKeymap.config = {}
<
------------------------------------------------------------------------------
                                                    *MiniKeymap.map_multistep()*
           `MiniKeymap.map_multistep`({mode}, {lhs}, {steps}, {opts})
Map multi-step action

Mapping of a multi-step action is an expression mapping (|:map-expression|).
Executing a multi-step action is essentially:
- Check condition for step one. If `true` - execute step one action and stop.
- Check condition for step two, and so on.
- If there is no more steps, fall back to returning mapped key.

For better user experience there are many built-in steps mostly designed
to create Insert mode "smart" mappings of <Tab>, <S-Tab>, <CR>, and <BS>.
Available built-in steps ("For key" is a suggestion, any can be used):
>
 ┌─────────────────────┬────────────────┬──────────────────────────┬─────────┐
 │      Step name      │   Condition    │          Action          │ For key │
 ├─────────────────────┴────────────────┴──────────────────────────┴─────────┤
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |ins-completion-menu| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ pmenu_next          │ Pmenu visible  │ Select next (as <C-n>)   │ <Tab>   │
 │ pmenu_prev          │ Pmenu visible  │ Select prev (as <C-p>)   │ <S-Tab> │
 │ pmenu_accept        │ Item selected  │ Accept (as <C-y>)        │ <CR>    │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |mini.snippets| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ minisnippets_next   │ Session active │ Jump to next tabstop     │ <Tab>   │
 │ minisnippets_prev   │ Session active │ Jump to prev tabstop     │ <S-Tab> │
 │ minisnippets_expand │ Can expand     │ Expand snippet at cursor │ <Tab>   │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |mini.pairs| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ minipairs_cr        │ Module set up  │ <CR> respecting pairs    │ <CR>    │
 │ minipairs_bs        │ Module set up  │ <BS> respecting pairs    │ <BS>    │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ Jump around in Insert mode ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ jump_after_tsnode   │ TS parser      │ Jump after node end      │ <Tab>   │
 │ jump_before_tsnode  │ TS parser      │ Jump before node start   │ <S-Tab> │
 │ jump_after_close    │ Insert mode    │ Jump after  )]}"'`       │ <Tab>   │
 │ jump_before_open    │ Insert mode    │ Jump before ([{"'`       │ <S-Tab> │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ Work with whitespace ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ increase_indent     │ Is on indent   │ Increase indent          │ <Tab>   │
 │ decrease_indent     │ Is on indent   │ Decrease indent          │ <S-Tab> │
 │ hungry_bs           │ Space to left  │ Delete all space to left │ <BS>    │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |vim.snippet| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ vimsnippet_next     │ Session active │ Jump to next tabstop     │ <Tab>   │
 │ vimsnippet_prev     │ Session active │ Jump to prev tabstop     │ <S-Tab> │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'hrsh7th/nvim-cmp' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ cmp_next            │ Menu visible   │ Select next item         │ <Tab>   │
 │ cmp_prev            │ Menu visible   │ Select prev item         │ <S-Tab> │
 │ cmp_accept          │ Item selected  │ Accept selected item     │ <CR>    │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'Saghen/blink.cmp' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ blink_next          │ Menu visible   │ Select next item         │ <Tab>   │
 │ blink_prev          │ Menu visible   │ Select prev item         │ <S-Tab> │
 │ blink_accept        │ Item selected  │ Accept selected item     │ <CR>    │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'L3MON4D3/LuaSnip' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ luasnip_next        │ Session active │ Jump to next tabstop     │ <Tab>   │
 │ luasnip_prev        │ Session active │ Jump to prev tabstop     │ <S-Tab> │
 │ luasnip_expand      │ Can expand     │ Expand snippet at cursor │ <Tab>   │
 ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'windwp/nvim-autopairs' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤
 │ nvimautopairs_cr    │ Module present │ <CR> respecting pairs    │ <CR>    │
 │ nvimautopairs_bs    │ Module present │ <BS> respecting pairs    │ <BS>    │
 └─────────────────────┴────────────────┴──────────────────────────┴─────────┘
<
Notes:
- Executing action has limitations of |:map-expression| (like not allowed text
  or buffer changes, etc.). To execute complex lua code, use |vim.schedule()|
  inside action, return the code as string in |:map-cmd| format, or return
  a function to be later executed. See usage examples.

- Some mapped keys (like <Tab>, <CR>) might require disabling smart presets
  in plugins (like 'nvim-cmp', 'blink-cmp', 'nvim-autopairs').

Parameters ~
{mode} `(string|table)` Same as for |vim.keymap.set()|.
{lhs} `(string)` Same as for |vim.keymap.set()|.
{steps} `(table)` Array of steps. Each step can be a string with the name
  of built-in step or a table with two callable methods (will be called
  without arguments):
  - <condition> - return `true` if the action should be executed.
  - <action> - action to be executed if <condition> returns `true`.
    For more flexibility, it can also return a value which can be:
      - String - will be returned as expression output. Can be something like
        `"<Tab>"` (treat as <Tab> key) or `"<Cmd>lua vim.notify('Hello')<CR>"`.
        Should not escape keycodes (i.e. return "<Tab>" and not "\t").
        To undo already done escape, use |keytrans()|.
      - Function - will be executed as if `"<Cmd>lua f()<CR>"`, but does not
        need to create a global function for that.
      - `false` - do not stop going through steps.
{opts} `(table|nil)` Same as for |vim.keymap.set()|.

Usage ~
See |MiniKeymap-examples| for practical examples.

Some illustrative examples: >lua

  _G.log = {}
  local steps = {}
  steps[1] = {
    condition = function() table.insert(_G.log, 'C1'); return _G.cond1 end,
    -- Compute and return keys. Will be emulated as pressed.
    action = function() table.insert(_G.log, 'A1'); return 'hello' end,
  }

  steps[2] = {
    condition = function() table.insert(_G.log, 'C2'); return _G.cond2 end,
    -- Perform action immediately, return `false` to keep asking other steps
    action = function() table.insert(_G.log, 'A2'); return false end,
  }

  steps[3] = {
    condition = function() table.insert(_G.log, 'C3'); return _G.cond3 end,
    -- Perform action later (to overcom expression mapping limitations)
    action = function()
      table.insert(_G.log, 'A3_1')
      return function() table.insert(_G.log, 'A3_2') end
    end,
  }

  -- Make Insert mode <Tab> mapping
  require('mini.keymap').map_multistep('i', '<Tab>', steps)

  -- Pressing <Tab> inserts fallback `\t`; logs C1+C2+C3
  _G.cond1, _G.cond2, _G.cond3 = false, false, false

  -- Pressing <Tab> inserts `hello`; logs C1+A1
  _G.cond1, _G.cond2, _G.cond3 = true, false, false

  -- Pressing <Tab> inserts nothing; logs C1+C2+A2+C3+A3_1+A3_2
  _G.cond1, _G.cond2, _G.cond3 = false, true, true
<
------------------------------------------------------------------------------
                                                           *MiniKeymap.gen_step*
                             `MiniKeymap.gen_step`
Generate step for multi-step mappings

This is a table with function elements. Call to actually get a step.

------------------------------------------------------------------------------
                                          *MiniKeymap.gen_step.search_pattern()*
        `MiniKeymap.gen_step.search_pattern`({pattern}, {flags}, {opts})
Search pattern step

Use |search()| to jump to pattern match. Possibly adjust final position to
be just to the right of the match (useful in Insert mode).

Parameters ~
{pattern} `(string)` Same as for |search()|.
{flags} `(string|nil)` Same as for |search()|.
{opts} `(table|nil)` Options. Possible fields:
  - <side> `(string)` - one of `"before"` (default) or `"after"`.
  - <stopline> `(number|function)` - forwarded to |search()| (as number or
    as function's output after calling it before every search).
  - <timeout> `(number)` - forwarded to |search()|.
  - <skip> `(string|function)` - forwarded to |search()|.

Return ~
`(table)` Step which searches pattern.

Usage ~
Built-in |MiniKeymap.map_multistep()| steps "jump_after_close" and
"jump_before_open" use this, but only in Insert mode.

Steps that jump before/after all consecutive brackets in several modes: >lua

  local keymap = require('mini.keymap')
  local tab_step_insert = keymap.gen_step.search_pattern(
    -- Need to use 'c' flag and 'after' side for robust "chaining"
    [=[[)\]}]\+]=], 'ceW', { side = 'after' }
  )
  keymap.map_multistep('i', '<Tab>', { tab_step_insert })
  local tab_step = keymap.gen_step.search_pattern([=[[)\]}]\+]=], 'eW')
  keymap.map_multistep({ 'n', 'x' }, '<Tab>', { tab_step })

  local stab_step = keymap.gen_step.search_pattern([=[[(\[{]\+]=], 'bW')
  keymap.map_multistep({ 'i', 'n', 'x' }, '<S-Tab>', { stab_step })
<
------------------------------------------------------------------------------
                                                        *MiniKeymap.map_combo()*
            `MiniKeymap.map_combo`({mode}, {lhs}, {action}, {opts})
Map combo post action

Create a combo: sequence of keys where each acts immediately plus execute
an extra action if all are typed within configurable delay between each other.

Example for Insert mode "better escape" `jk` combo with `<BS><BS><Esc>` action:
- Press `j`. It is visible immediately without any side effects.
- Quickly (no more than default 200 ms after) press `k`. This triggers the
  action which is equivalent to typing <BS><BS> (delete already present `jk`)
  and <Esc> to exit into Normal mode.

Notes:
- IMPORTANT! Combo is not a regular mapping but a separate key tracking
  with |vim.on_key()|. This is important as combos will not be visible and
  can not be managed as regular mappings. Instead each combo is associated
  with a dedicated |namespace| (named for human readability). However, it is not
  really expected to manage them on the fly after they are created.

- String action is executed with |nvim_input()|, i.e. emulated keys will
  respect custom mappings.

- Different combos are tracked and act independent of each other. For example,
  if there are combos for `jjk` and `jk` keys, fast typing `jjk` will execute both.

- Neovim>=0.11 is recommended due to |vim.on_key()| improvement to allow
  watching for keys as they are typed and not as if coming from mappings.
  For example, this matters when creating a `jk` combo for Visual mode while
  also having `xnoremap j gj` style of remaps. On Neovim<0.11 the fix is to
  use `gjgk` as combo's left hand side.

- Each combo adds very small but non-zero overhead on each keystroke.
  Usually about 1-3 microseconds (i.e. 0.001-0.003 ms), which should be
  fast enough for most setups. For a "normal, real world" coding session
  with a total of ~20000 keystrokes it results in extra ~40ms of overhead
  for a single created combo. Create many combos with caution.

Parameters ~
{mode} `(string|table)` String or array of string mode id (like "n", "i", etc.).
  Array of several modes is more performant than several single mode combos.
{lhs} `(string|table)` String with tracked key sequence or an array of
  tracked keys (one element - one key).
{action} `(string|function)` Action to perform after key sequence is detected.
  If string, treated as keys and emulated with |nvim_input()|.
  If function, executed in |vim.schedule()|. Can return string keys which will
  be emulated.
{opts} `(table|nil)` Options. Possible fields:
  - <delay> `(number)` - delay in milliseconds within which keys should be
    pressed to detect a key sequence. Default: 200.

Usage ~
See |MiniKeymap-examples| for practical examples.

Some illustrative examples: >lua

  local map_combo = require('mini.keymap').map_combo

  -- In Insert mode pressing `x` followed by `x` within 1 second logs 'A'
  -- and emulates extra pressing of `yy`
  _G.log = {}
  local action = function() table.insert(_G.log, 'A'); return 'yy' end
  map_combo('i', 'xx', action, { delay = 1000 })
<

 vim:tw=78:ts=8:noet:ft=help:norl: