Legendary
Define your keymaps, commands, and autocommands as simple Lua tables, building a legend at the same time.
Theme used in recording is lighthaus.nvim. The finder UI is handled by telescope.nvim via dressing.nvim. See Prerequisites for details.
Features
- Define your keymaps, commands, and
augroup
/autocmd
s as simple Lua tables, then bind them withlegendary.nvim
- Integration with which-key.nvim, use your existing
which-key.nvim
tables withlegendary.nvim
- Uses
vim.ui.select()
so it can be hooked up to a fuzzy finder using something like dressing.nvim for a VS Code command palette like interface - Execute normal, insert, and visual mode keymaps, commands, and autocommands, when you select them
- Show your most recently executed keymap, command, or autocmd at the top when triggered via
legendary.nvim
(can be disabled via config) - Buffer-local keymaps, commands, and autocmds only appear in the finder for the current buffer
- Help execute commands that take arguments by prefilling the command line instead of executing immediately
- Search built-in keymaps and commands along with your user-defined keymaps and commands (may be disabled in config). Notice some missing? Comment on this discussion or submit a PR!
- A
legendary.helpers
module to help create lazily-evaluated keymaps and commands. Have an idea for a new helper? Comment on this discussion or submit a PR!
Prerequisites
- Neovim 0.7.0+; specifically, this plugin depends on the following APIs:
vim.keymap.set
vim.api.nvim_create_augroup
vim.api.nvim_create_autocmd
- (Optional) A
vim.ui.select()
handler; this provides the UI for the finder.- I recommend telescope.nvim paired with dressing.nvim.
Installation
With packer.nvim
:
use({'mrjones2014/legendary.nvim'})
With vim-plug
:
Plug "mrjones2014/legendary.nvim"
Usage
To trigger the finder for your configured keymaps, commands, and augroup
/autocmd
s:
Lua:
-- search keymaps, commands, and autocmds
require('legendary').find()
-- search keymaps
require('legendary').find('keymaps')
-- search commands
require('legendary').find('commands')
-- search autocmds
require('legendary').find('autocmds')
Vim commands:
" search keymaps, commands, and autocmds
:Legendary
" search keymaps
:Legendary keymaps
" search commands
:Legendary commands
" search autocmds
:Legendary autocmds
In Lua, you can also specify filters in the second argument. It can be either a function, or a list of functions,
with the signature function(item: LegendaryItem): boolean
. There are some pre-made filters in the legendary.filters
module.
-- filter keymaps by current mode
require('legendary').find(nil, require('legendary.filters').current_mode())
-- filter keymaps by normal mode
require('legendary').find(nil, require('legendary.filters').mode('n'))
-- show only keymaps and filter by normal mode
require('legendary').find('keymaps', require('legendary.filters').mode('n'))
-- filter keymaps by normal mode and that start with <leader>
require('legendary').find(nil, {
require('legendary.filters').mode('n'),
function(item)
if not string.find(item.kind, 'keymap') then
return true
end
return vim.startswith(item[1], '<leader>')
end
})
Configuration
Default configuration is shown below. For a detailed explanation of the structure for
keymap, command, and augroup
/autocmd
tables, see Table Structures.
require('legendary').setup({
-- Include builtins by default, set to false to disable
include_builtin = true,
-- Include the commands that legendary.nvim creates itself
-- in the legend by default, set to false to disable
include_legendary_cmds = true,
-- Customize the prompt that appears on your vim.ui.select() handler
-- Can be a string or a function that takes the `kind` and returns
-- a string. See "Item Kinds" below for details. By default,
-- prompt is 'Legendary' when searching all items,
-- 'Legendary Keymaps' when searching keymaps,
-- 'Legendary Commands' when searching commands,
-- and 'Legendary Autocmds' when searching autocmds.
select_prompt = nil,
-- Optionally pass a custom formatter function. This function
-- receives the item as a parameter and must return a table of
-- non-nil string values for display. It must return the same
-- number of values for each item to work correctly.
-- The values will be used as column values when formatted.
-- See function `get_default_format_values(item)` in
-- `lua/legendary/formatter.lua` to see default implementation.
formatter = nil,
-- When you trigger an item via legendary.nvim,
-- show it at the top next time you use legendary.nvim
most_recent_item_at_top = true,
-- Initial keymaps to bind
keymaps = {
-- your keymap tables here
},
-- Initial commands to bind
commands = {
-- your command tables here
},
-- Initial augroups and autocmds to bind
autocmds = {
-- your autocmd tables here
},
which_key = {
-- you can put which-key.nvim tables here,
-- or alternatively have them auto-register,
-- see section on which-key integration
mappings = {},
opts = {},
-- controls whether legendary.nvim actually binds they keymaps,
-- or if you want to let which-key.nvim handle the bindings.
-- if not passed, true by default
do_binding = {},
},
-- Automatically add which-key tables to legendary
-- see "which-key.nvim Integration" below for more details
auto_register_which_key = true,
-- settings for the :LegendaryScratch command
scratchpad = {
-- configure how to show results of evaluated Lua code,
-- either 'print' or 'float'
-- Pressing q or <ESC> will close the float
display_results = 'float',
},
})
which-key.nvim
Integration
Already a which-key.nvim
user? Use your existing which-key.nvim
tables with legendary.nvim
!
There's a couple ways you can choose to do it:
-- automatically register which-key.nvim tables with legendary.nvim
-- when you register them with which-key.nvim.
-- `setup()` must be called before `require('which-key).register()`
require('legendary').setup()
-- now this will register them with both which-key.nvim and legendary.nvim
require('which-key').register(your_which_key_tables, your_which_key_opts)
-- or, pass them through setup() directly
require('legendary').setup({
which_key = {
mappings = your_which_key_tables,
opts = your_which_key_opts,
-- false if which-key.nvim handles binding them,
-- set to true if you want legendary.nvim to handle binding
-- the mappings; if not passed, true by default
do_binding = false,
},
})
-- or, if you'd prefer to manually register with legendary.nvim
require('legendary').setup({ auto_register_which_key = false })
require('which-key').register(your_which_key_tables, your_which_key_opts)
require('legendary').bind_whichkey(
your_which_key_tables,
your_which_key_opts,
-- false if which-key.nvim handles binding them,
-- set to true if you want legendary.nvim to handle binding
-- the mappings; if not passed, true by default
false,
)
Table Structures
The tables for keymaps, commands, and augroup
/autocmd
s are all similar.
Descriptions can be specified either in the top-level description
property
on each table, or inside the opts
table as opts.desc = 'Description goes here'
.
For autocmd
s, you must include a description
property for it to appear in the finder.
This is a design decision because keymaps and commands are frequently executed manually,
so they should appear in the finder by default, while executing autocmd
s manually with
:doautocmd
is a much less common use-case, so autocmd
s are hidden from the finder
unless a description is provided.
Keymaps
For keymaps you are mapping yourself (as opposed to mappings set by other plugins),
the first two elements are the key and the handler, respectively. The handler
can be a command string like :wa<CR>
or a Lua function. Example:
local keymaps = {
{ '<leader>s', ':wa<CR>', description = 'Write all buffers', opts = {} },
{ '<leader>fm', vim.lsp.buf.formatting_sync, description = 'Format buffer with LSP' },
}
If you need to pass parameters to the Lua function or call a function dynamically from a plugin, you can use the following helper functions:
local helpers = require('legendary.helpers')
local keymaps = {
{ '<leader>p', helpers.lazy(vim.lsp.buf.formatting_sync, nil, 1500), description = 'Format with 1.5s timeout' },
{ '<leader>f', helpers.lazy_required_fn('telescope.builtin', 'oldfiles', { only_cwd = true }) }
}
The keymap's mode defaults to normal (n
), but you can set a different mode, or list of modes, via
the mode
property:
local keymaps = {
{ '<leader>c', ':CommentToggle<CR>', description = 'Toggle comment', mode = { 'n', 'v' } }
}
Alternatively, you can map separate implementations for each mode by passing the second element as a table, where the table keys are the modes:
local keymaps = {
{ '<leader>c', { n = ':CommentToggle<CR>', v = ':VisualCommentToggle<CR>' }, description = 'Toggle comment' }
}
If you need to pass separate opts per-mode, you can do that too:
local keymaps = {
{
'<leader>c',
{
n = { ':CommentToggle<CR>' opts = { noremap = true } },
v = { ':VisualCommentToggle<CR>' opts = { silent = false } }
},
description = 'Toggle comment'
-- if outer opts exist, the inner opts tables will be merged,
-- with the inner opts taking precedence
opts = { expr = false }
}
}
If you want the per-mode mappings to be treated as separate keymaps, you can specify a separate description per-mode:
local keymaps = {
{
'<leader>c',
{
n = {
':Something<CR>',
description = 'Something in normal mode',
opts = { noremap = true }
},
v = {
':SomethingElse<CR>'
opts = {
-- you can also specify description through opts.desc
-- if you prefer
desc = 'Something else in visual mode',
silent = false,
}
}
},
description = 'Toggle comment'
-- if outer opts exist, the inner opts tables will be merged,
-- with the inner opts taking precedence
opts = { expr = false }
}
}
You can also pass options to the keymap via the opts
property, see :h vim.keymap.set
to
see available options.
local keymaps = {
{
'<leader>fm',
vim.lsp.buf.formatting_sync,
description = 'Format buffer with LSP',
opts = { silent = true, noremap = true }
},
}
If you want a keymap to apply to both normal and insert mode, use a Lua function. The function will be given a table containing the visual selection range (the marks will also be set). This allows you to create mappings like:
local keymaps = {
{
'<leader>c',
function(visual_selection)
if visual_selection then
-- comment a visual block
vim.cmd(":'<,'>CommentToggle")
else
-- comment a single line from normal mode
vim.cmd(':CommentToggle')
end
end,
description = 'Toggle comment',
mode = { 'n', 'v' },
}
}
Finally, if you want to register keymaps with legendary.nvim
in order to see them in the finder, but not bind
them (like for keymaps set by other plugins), you can just omit the handler element:
local keymaps = {
{ '<C-d>', description = 'Scroll docs up' },
{ '<C-f>', description = 'Scroll docs down' },
}
Commands
Command tables follow the exact same structure as keymaps, but specify a command name instead of a key code.
local commands = {
{ ':DoSomething', ':echo "something"', description = 'Do something!' },
{ ':DoSomethingWithLua', require('some-module').some_method, description = 'Do something with Lua!' },
-- a command from a plugin, don't specify a handler
{ ':CommentToggle', description = 'Toggle comment' },
}
You can also pass options to the command via the opts
property, see :h nvim_create_user_command
to
see available options. In addition to those options, legendary.nvim
adds handling for an additional
buffer
option (a buffer handle, or 0
for current buffer), which will cause the command to be bound
as a buffer-local command.
If you need a command to take an argument, specify unfinished = true
to pre-fill the command line instead
of executing the command on selected. You can put an argument name/hint in []
or {}
that will be stripped
when filling the command line.
local commands = {
{ ':MyCommand {some_argument}<CR>', description = 'Command with argument', unfinished = true },
-- or
{ ':MyCommand [some_argument]<CR>', description = 'Command with argument', unfinished = true },
}
augroup
s and autocmd
s
augroup
tables are very simple. They have a name
property, and a clear
property which defaults to true
.
This will clear the augroup
when creating it, equivalent to au!
. autocmd
tables nested within augroup
tables will automatically be defined in the augroup
.
local augroups = {
{
name = 'MyAugroupName',
clear = true,
-- you autocmd tables here
}
}
autocmd
tables have an event or list of events, and a handler as the first two elements, respectively.
You can also specify options to be passed to the autocmd
via the opts
property. The opts
property
defaults to { pattern = '*', group = nil }
.
local autocmds = {
{
'FileType',
':setlocal conceallevel=0',
opts = {
pattern = { 'json', 'jsonc' },
},
},
{
{ 'BufRead', 'BufNewFile' },
':set filetype=jsonc',
opts = {
pattern = { '*.jsonc', 'tsconfig*.json' },
},
},
{
'BufWritePre',
vim.lsp.buf.formatting_sync,
-- include a description to execute it
-- like a command on-demand from the finder
description = 'Format on write with LSP',
},
}
An example putting both together:
local augroups = {
{
name = 'LspOnAttachAutocmds',
clear = true,
{
'BufWritePre',
require('lsp.utils').format_document,
},
{
'CursorHold',
vim.diagnostic.open_float,
},
},
{
{ 'BufRead', 'BufNewFile' },
':set filetype=jsonc',
opts = {
-- you can also manually add an autocmd
-- to an existing augroup
group = 'filetypedetect',
pattern = { '*.jsonc', 'tsconfig*.json' },
},
}
}
Lua API
You can also manually bind new items after you've already called require('legendary').setup()
.
This can be useful for things like binding language-specific keyaps in the LSP on_attach
function.
The following API functions are available:
-- bind a single keymap
require('legendary').bind_keymap(keymap)
-- bind a list of keymaps
require('legendary').bind_keymaps({
-- your keymaps here
})
-- bind a single command
require('legendary').bind_command(command)
-- bind a list of commands
require('legendary').bind_commands({
-- your commands here
})
-- bind single or multiple augroups and/or autocmds
-- these all use the same function
require('legendary').bind_autocmds(augroup)
require('legendary').bind_autocmds(autocmd)
require('legendary').bind_autocmds({
-- your augroups and autocmds here
})
-- search keymaps, commands, and autocmds
require('legendary').find()
-- search keymaps
require('legendary').find('keymaps')
-- search commands
require('legendary').find('commands')
-- search autocmds
require('legendary').find('autocmds')
-- filter keymaps by current mode
require('legendary').find(nil, require('legendary.filters').current_mode())
-- find only keymaps, and filter by current mode
require('legendary').find('keymaps', require('legendary.filters').current_mode())
-- filter keymaps by normal mode
require('legendary').find(nil, require('legendary.filters').mode('n'))
-- filter keymaps by normal mode and that start with <leader>
require('legendary').find(nil, {
require('legendary.filters').mode('n'),
function(item)
if not string.find(item.kind, 'keymap') then
return true
end
return vim.startswith(item[1], '<leader>')
end
})
Item Kinds
legendary.nvim
will set the kind
option on vim.ui.select()
to legendary.keymaps
,
legendary.commands
, legendary.autocmds
, or legendary.items
, depending on whether you
are searching keymaps, commands, autocmds, or all.
The individual items will have kind = 'legendary.keymap'
, kind = 'legendary.command'
,
or kind = 'legendary.autocmd'
, depending on whether it is a keymap, command, or autocmd.
Builtins will have kind = 'legendary.keymap.bulitin'
, kind = 'legendary.command.builtin'
,
or kind = 'legendary.autocmd'
, depending on whether it is a built-in keymap, command, or autocmd.
Lua Helpers for Creating Mappings, Commands, and Autocmds
When creating keymaps to Lua functions, the Lua expressions are evaluated at the time the mappings
table is first read by nvim. This means you typically need to pass a function reference instead
of calling the function. For example, you probably want to map vim.lsp.buf.formatting_sync
, not
vim.lsp.buf.formatting_sync()
.
If you need to pass arguments to a function when it's called, you can use the lazy
helper:
-- lazy() takes the first argument (a function)
-- and calls it with the rest of the arguments
require('legendary.helpers').lazy(vim.lsp.buf.formatting_sync, nil, 1500)
-- this will *return a new function* defined as:
function()
vim.lsp.buf.formatting_sync(nil, 1500)
end
If you need to call a function from Legendary, but the plugin won't be loaded at the time
you define your keymaps (for example, if you're using Packer to lazy-load plugins), you can use the
lazy_required_fn
helper:
-- lazy_required_fn() takes a module path as the first argument,
-- a function name from that module as the second argument,
-- and returns a new function that calls the function by name
-- with the rest of the arguments
require('legendary.helpers').lazy_required_fn('telescope.builtin', 'oldfiles', { only_cwd = true })
-- this will *return a new function* defined as:
function()
require('telescope.bulitin')['oldfiles']({ only_cwd = true })
end
If you want to create a keymap that creates a split pane, then does something in the new pane, there are helpers for that too:
-- split_then() and vsplit_then() both take a Lua function as the
-- only parameter, and return a new function that creates a
-- horizontal or vertical split, then calls the specified Lua function
require('legendary.helpers').split_then(vim.lsp.buf.definition)
-- this will *return a new function* defined as:
function()
vim.cmd('sp')
vim.lsp.buf.definition()
end
-- and likewise, this:
require('legendary.helpers').vsplit_then(vim.lsp.buf.definition)
-- will *return a new function* defined as:
function()
vim.cmd('vsp')
vim.lsp.buf.definition()
end
These helpers can also be composed together. For example, to create a function that creates a vertical split, then uses Telescope to find and open a file in the new split, you could write:
local helpers = require('legendary.helpers')
helpers.vsplit_then(helpers.lazy_required_fn('telescope', 'find_file', { only_cwd = true }))
Utilities
legendary.nvim
also provides some utilities for developing Lua keymaps, commands, etc.
The following commands are available once legendary.nvim
is loaded:
:LegendaryScratch
- create a scratchpad buffer to test Lua snippets in:LegendaryEvalLine
- evaluate the current line as a Lua expression:LegendaryEvalLines
- evaluate the line range selected in visual mode as a Lua snippet:LegendaryEvalBuf
- evaluate the entire current buffer as a Lua snippet
Any return
value from evaluated Lua is displayed by your configured method (either print
ed
to the command area, or displayed in a float, see configuration).
Sponsors
Huge thanks to my sponsors for helping to support this project: