Keymaps and Leader Key
Keymaps are the customizations that make Neovim your editor. A good keymap scheme is organized, memorable, and documented.
The leader key is a prefix key that namespaces custom shortcuts. Vanilla Neovim defaults to \; LazyVim defaults to <Space>.
Setting the Leader Key
-- MUST be set before plugins load
vim.g.mapleader = " " -- Space as leader
vim.g.maplocalleader = "\\" -- Backslash as local leader
For a normal hand-written configuration, set the leader before lazy.nvim or any other plugin manager loads. The optional LazyVim profile below sets these values during LazyVim's specification setup.
Optional Custom Leader Profile
This project includes an independently toggleable profile from
common-config/nvim/plugins/custom-leader/:
; global leader
, local leader
<C-n> repeat the last f/t/F/T motion in the same direction
<C-p> repeat it in the opposite direction
Activate or inspect it from Bash:
nvim-l-on
nvim-l-status
Restart Neovim after activation. LazyVim mappings such as <leader>ff then
become ;ff; <LocalLeader> mappings use ,.
Deactivate it with:
nvim-l-off
nvim-l-status
Restart Neovim after deactivation. The defaults return to <Space> as the
global leader, \ as the local leader, ; as repeat-find forward, and , as
repeat-find backward.
The profile does not change insert-mode completion: <C-n> and <C-p> keep
their completion behavior in insert mode. It also does not modify the existing
keymaps module containing jj and Alt-based scrolling.
The keymap.set API
vim.keymap.set(mode, lhs, rhs, opts)
-- mode: "n" normal, "i" insert, "v" visual, "x" visual only,
-- "t" terminal, "c" command, "o" operator-pending
-- can be a table: {"n", "v"}
-- lhs: key combination string
-- rhs: action (string command or Lua function)
-- opts: table of options
Common Options
{
desc = "Description", -- shows in :map and which-key
noremap = true, -- don't allow remapping (default true in keymap.set)
silent = true, -- don't echo command
expr = true, -- rhs is an expression
buffer = 0, -- buffer-local mapping (0 = current buffer)
}
Keymap Organization by Leader Prefix
Organize keymaps into logical groups using leader prefixes:
local map = vim.keymap.set
-- ????????? Files ??????????????????????????????????????????????????????????????????????????????
map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })
map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>", { desc = "Find in files (grep)" })
map("n", "<leader>fr", "<cmd>Telescope oldfiles<cr>", { desc = "Recent files" })
map("n", "<leader>fw", "<cmd>Telescope grep_string<cr>", { desc = "Find word under cursor" })
-- ????????? Buffers ????????????????????????????????????????????????????????????????????????
map("n", "<leader>bb", "<cmd>Telescope buffers<cr>", { desc = "Switch buffer" })
map("n", "<leader>bd", "<cmd>bdelete<cr>", { desc = "Delete buffer" })
map("n", "<leader>bn", "<cmd>bnext<cr>", { desc = "Next buffer" })
map("n", "<leader>bp", "<cmd>bprev<cr>", { desc = "Prev buffer" })
-- ????????? Windows ????????????????????????????????????????????????????????????????????????
map("n", "<leader>wv", "<cmd>vsplit<cr>", { desc = "Vertical split" })
map("n", "<leader>ws", "<cmd>split<cr>", { desc = "Horizontal split" })
map("n", "<leader>wc", "<cmd>close<cr>", { desc = "Close window" })
map("n", "<leader>wo", "<cmd>only<cr>", { desc = "Close other windows" })
-- ????????? LSP (filled in by lsp config) ???
-- <leader>ld ??? LSP diagnostics
-- <leader>lr ??? LSP references
-- <leader>lR ??? LSP rename
-- <leader>la ??? LSP code actions
-- <leader>lf ??? LSP format
-- ????????? Git ????????????????????????????????????????????????????????????????????????????????????
map("n", "<leader>gs", "<cmd>Neogit<cr>", { desc = "Git status" })
map("n", "<leader>gc", "<cmd>Neogit commit<cr>", { desc = "Git commit" })
map("n", "<leader>gp", "<cmd>Neogit push<cr>", { desc = "Git push" })
map("n", "<leader>gb", "<cmd>Telescope git_branches<cr>", { desc = "Git branches" })
-- ????????? Diagnostics ????????????????????????????????????????????????????????????
map("n", "<leader>dl", "<cmd>Telescope diagnostics<cr>", { desc = "List diagnostics" })
map("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" })
map("n", "[d", vim.diagnostic.goto_prev, { desc = "Prev diagnostic" })
-- ????????? Terminal ?????????????????????????????????????????????????????????????????????
map("n", "<leader>tt", "<cmd>terminal<cr>", { desc = "New terminal" })
map("t", "<Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode" })
-- ????????? Editor ???????????????????????????????????????????????????????????????????????????
map("n", "<leader>ec", "<cmd>e ~/.config/nvim/init.lua<cr>", { desc = "Edit config" })
map("n", "<leader>er", "<cmd>source ~/.config/nvim/init.lua<cr>", { desc = "Reload config" })
Practical Example: Scroll Shortcuts
Here's a real-world example using Alt key combinations for comfortable scrolling without leaving the home row:
-- Half page scroll
vim.keymap.set('n', '<A-d>', '<C-d>', { desc = 'Scroll half page down' })
vim.keymap.set('n', '<A-u>', '<C-u>', { desc = 'Scroll half page up' })
-- Full page scroll
vim.keymap.set('n', '<A-S-d>', '<C-f>', { desc = 'Scroll full page down' })
vim.keymap.set('n', '<A-S-u>', '<C-b>', { desc = 'Scroll full page up' })
The mappings simply delegate to Neovim's built-in scroll commands, so the original Ctrl+d, Ctrl+u, Ctrl+f, and Ctrl+b motions keep working.
Alt key mappings require your terminal to pass Alt/Meta keys through to Neovim. Most modern terminals (Kitty, Alacritty, WezTerm, Ghostty) do this by default. Note that Neovim treats Alt+Shift+d/Alt+Shift+u as equivalent to Alt+D/Alt+U (the Shift folds into the uppercase letter), so they work on any terminal that passes Alt+D/Alt+U through.
Leader Key Map Reference
| Prefix | Namespace |
|---|---|
| <leader>f | Files (find, grep, recent) |
| <leader>b | Buffers |
| <leader>w | Windows |
| <leader>l | LSP actions |
| <leader>g | Git |
| <leader>d | Diagnostics |
| <leader>t | Terminal |
| <leader>e | Editor (config, reload) |
| <leader>s | Search/substitute |
| <leader>u | UI (toggle options) |
Which-key Plugin
which-key.nvim shows available key bindings as you type the leader:
{
"folke/which-key.nvim",
event = "VeryLazy",
opts = {
plugins = { spelling = true },
},
config = function(_, opts)
local wk = require("which-key")
wk.setup(opts)
-- Register group names
wk.add({
{ "<leader>f", group = "Files" },
{ "<leader>b", group = "Buffers" },
{ "<leader>w", group = "Windows" },
{ "<leader>l", group = "LSP" },
{ "<leader>g", group = "Git" },
{ "<leader>d", group = "Diagnostics" },
{ "<leader>t", group = "Terminal" },
})
end,
}
Buffer-Local Keymaps
For keymaps that only apply in specific buffers (e.g., LSP keymaps):
-- In LSP on_attach callback:
local function on_attach(client, bufnr)
local map = function(lhs, rhs, desc)
vim.keymap.set("n", lhs, rhs, { buffer = bufnr, desc = desc })
end
map("gd", vim.lsp.buf.definition, "Go to definition")
map("gr", vim.lsp.buf.references, "References")
map("K", vim.lsp.buf.hover, "Hover docs")
map("<leader>lR", vim.lsp.buf.rename, "Rename")
map("<leader>la", vim.lsp.buf.code_action, "Code action")
map("<leader>lf", function()
vim.lsp.buf.format({ async = true })
end, "Format")
end
In LazyVim
LazyVim uses <Space> as the default leader key and pre-configures hundreds of mappings using which-key.nvim. With the optional profile enabled, press ; instead.
| Aspect | Vanilla Neovim | LazyVim |
|---|---|---|
| Leader Key | You define in init.lua | Pre-configured to <Space> |
| Keymap Location | lua/config/keymaps.lua | Add custom ones to lua/config/keymaps.lua |
| which-key | Requires manual setup & registration | Bundled and pre-configured for all built-in mappings |
| Finding files | You map <leader>ff to Telescope | Pre-mapped to <leader><space> or <leader>ff |