*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 , , , and : - Navigate and accept |popupmenu-completion|. Useful for |mini.completion|. - Navigate and expand |mini.snippets|. - Execute and 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 and 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 ``). - [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', '', { 'pmenu_next' }) map_multistep('i', '', { 'pmenu_prev' }) map_multistep('i', '', { 'pmenu_accept', 'minipairs_cr' }) map_multistep('i', '', { 'minipairs_bs' }) < Use / 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_steps) local shifttab_steps = { 'minisnippets_prev', 'pmenu_prev' } map_multistep('i', '', shifttab_steps) < An extra smart and : >lua local map_multistep = require('mini.keymap').map_multistep -- NOTE: this will never insert tab, press for that local tab_steps = { 'minisnippets_next', 'minisnippets_expand', 'pmenu_next', 'jump_after_tsnode', 'jump_after_close', } map_multistep('i', '', tab_steps) local shifttab_steps = { 'minisnippets_prev', 'pmenu_prev', 'jump_before_tsnode', 'jump_before_open', } map_multistep('i', '', 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' }, '', { 'vimsnippet_next', 'pmenu_next' }) map_multistep({ 'i', 's' }, '', { '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 key: >lua -- Support most common modes. This can also contain 't', but would -- only mean to press `` inside terminal. local mode = { 'i', 'c', 'x', 's' } require('mini.keymap').map_combo(mode, 'jk', '') -- To not have to worry about the order of keys, also map "kj" require('mini.keymap').map_combo(mode, 'kj', '') -- Escape into Normal mode from Terminal mode require('mini.keymap').map_combo('t', 'jk', '') require('mini.keymap').map_combo('t', 'kj', '') < ## 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 = '[s1z=gi' require('mini.keymap').map_combo('i', 'kk', action) < ## Hide search highlighting ~ Use double to execute |:nohlsearch|. Although this can also be done with `nmap nohl`, the combo approach also exists and can be used to free mapping in Normal mode for something else. >lua local action = function() vim.cmd('nohlsearch') end require('mini.keymap').map_combo({ 'n','i','x','c' }, '', 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 , , , and . 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 ) │ │ │ pmenu_prev │ Pmenu visible │ Select prev (as ) │ │ │ pmenu_accept │ Item selected │ Accept (as ) │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |mini.snippets| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ minisnippets_next │ Session active │ Jump to next tabstop │ │ │ minisnippets_prev │ Session active │ Jump to prev tabstop │ │ │ minisnippets_expand │ Can expand │ Expand snippet at cursor │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |mini.pairs| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ minipairs_cr │ Module set up │ respecting pairs │ │ │ minipairs_bs │ Module set up │ respecting pairs │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ Jump around in Insert mode ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ jump_after_tsnode │ TS parser │ Jump after node end │ │ │ jump_before_tsnode │ TS parser │ Jump before node start │ │ │ jump_after_close │ Insert mode │ Jump after )]}"'` │ │ │ jump_before_open │ Insert mode │ Jump before ([{"'` │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ Work with whitespace ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ increase_indent │ Is on indent │ Increase indent │ │ │ decrease_indent │ Is on indent │ Decrease indent │ │ │ hungry_bs │ Space to left │ Delete all space to left │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ |vim.snippet| ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ vimsnippet_next │ Session active │ Jump to next tabstop │ │ │ vimsnippet_prev │ Session active │ Jump to prev tabstop │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'hrsh7th/nvim-cmp' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ cmp_next │ Menu visible │ Select next item │ │ │ cmp_prev │ Menu visible │ Select prev item │ │ │ cmp_accept │ Item selected │ Accept selected item │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'Saghen/blink.cmp' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ blink_next │ Menu visible │ Select next item │ │ │ blink_prev │ Menu visible │ Select prev item │ │ │ blink_accept │ Item selected │ Accept selected item │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'L3MON4D3/LuaSnip' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ luasnip_next │ Session active │ Jump to next tabstop │ │ │ luasnip_prev │ Session active │ Jump to prev tabstop │ │ │ luasnip_expand │ Can expand │ Expand snippet at cursor │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ 'windwp/nvim-autopairs' ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┤ │ nvimautopairs_cr │ Module present │ respecting pairs │ │ │ nvimautopairs_bs │ Module present │ respecting pairs │ │ └─────────────────────┴────────────────┴──────────────────────────┴─────────┘ < 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 , ) 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): - - return `true` if the action should be executed. - - action to be executed if 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 `""` (treat as key) or `"lua vim.notify('Hello')"`. Should not escape keycodes (i.e. return "" and not "\t"). To undo already done escape, use |keytrans()|. - Function - will be executed as if `"lua f()"`, 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 mapping require('mini.keymap').map_multistep('i', '', steps) -- Pressing inserts fallback `\t`; logs C1+C2+C3 _G.cond1, _G.cond2, _G.cond3 = false, false, false -- Pressing inserts `hello`; logs C1+A1 _G.cond1, _G.cond2, _G.cond3 = true, false, false -- Pressing 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: - `(string)` - one of `"before"` (default) or `"after"`. - `(number|function)` - forwarded to |search()| (as number or as function's output after calling it before every search). - `(number)` - forwarded to |search()|. - `(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_step_insert }) local tab_step = keymap.gen_step.search_pattern([=[[)\]}]\+]=], 'eW') keymap.map_multistep({ 'n', 'x' }, '', { tab_step }) local stab_step = keymap.gen_step.search_pattern([=[[(\[{]\+]=], 'bW') keymap.map_multistep({ 'i', 'n', 'x' }, '', { 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 `` 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 (delete already present `jk`) and 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: - `(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: