donthorp / legendary.nvim

🗺️ A legend for your keymaps, commands, and autocmds, with which-key.nvim integration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Legendary

Define your keymaps, commands, and autocommands as simple Lua tables, building a legend at the same time.

demo 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/autocmds as simple Lua tables, then bind them with legendary.nvim
  • Integration with which-key.nvim, use your existing which-key.nvim tables with legendary.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.

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/autocmds:

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/autocmds 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 autocmds, 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 autocmds manually with :doautocmd is a much less common use-case, so autocmds 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 },
}
augroups and autocmds

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 printed to the command area, or displayed in a float, see configuration).

Sponsors

Huge thanks to my sponsors for helping to support this project:

About

🗺️ A legend for your keymaps, commands, and autocmds, with which-key.nvim integration

License:MIT License


Languages

Language:Lua 97.8%Language:Makefile 2.2%