Keymaps are defined in lua/config/keymaps.lua and can be customized or extended. LSP-specific keymaps are set up in lua/plugins/lsp/init.lua when an LSP server attaches to a buffer.
Adding custom keymaps
Use Neovim’s vim.keymap.set() API to add keymaps:
vim.keymap.set(mode, lhs, rhs, { desc = "Description", silent = true })
Parameters
mode - Mode(s) where the keymap applies: "n" (normal), "i" (insert), "v" (visual), "x" (visual block), "t" (terminal)
lhs - The key combination to map
rhs - The action to perform (can be a string command or function)
opts - Options table with desc (description), silent, noremap, etc.
File management
-- Source current file (useful for config development)
vim.keymap.set("n", "<leader>rc", "<cmd>source %<CR>")
-- Execute Lua code
vim.keymap.set("n", "<leader>x", ":.lua<CR>") -- Current line
vim.keymap.set("v", "<leader>x", ":lua<CR>") -- Visual selection
Buffer navigation
-- Navigate buffers (cycles through buffer list)
vim.keymap.set("n", "<Right>", ":bnext<CR>", { desc = "Next Buffer", silent = true })
vim.keymap.set("n", "<Left>", ":bprevious<CR>", { desc = "Prev Buffer", silent = true })
Line and text manipulation
Moving lines
Indenting
Line navigation
Splitting lines
-- Move lines up or down in visual mode
vim.keymap.set("v", "J", ":m '>+1<CR>gv=gv", { desc = "Move Lines Down" })
vim.keymap.set("v", "K", ":m '<-2<CR>gv=gv", { desc = "Move Lines Up" })
-- Better indenting (stays in visual mode)
vim.keymap.set("v", "<", "<gv", { desc = "Indent Left" })
vim.keymap.set("v", ">", ">gv", { desc = "Indent Right" })
-- Move to start/end of line
vim.keymap.set({ "n", "x", "o" }, "H", "^", { desc = "Start of Line" })
vim.keymap.set({ "n", "x", "o" }, "L", "g_", { desc = "End of Line" })
-- Split line at cursor
vim.keymap.set(
"n",
"X",
":keeppatterns substitute/\\s*\\%#\\s*/\\r/e <bar> normal! ==^<cr>",
{ desc = "Split Line", silent = true }
)
Search enhancements
-- Keep search results centered on screen
vim.keymap.set("n", "n", "nzzzv", { desc = "Next Match (centered)" })
vim.keymap.set("n", "N", "Nzzzv", { desc = "Prev Match (centered)" })
vim.keymap.set("n", "*", "*zzzv", { desc = "Search Word (centered)" })
vim.keymap.set("n", "#", "#zzzv", { desc = "Search Word Back (centered)" })
vim.keymap.set("n", "g*", "g*zz", { desc = "Search Partial (centered)" })
vim.keymap.set("n", "g#", "g#zz", { desc = "Search Partial Back (centered)" })
These keymaps automatically center the screen after jumping to search results, making it easier to see context.
Quickfix list
vim.keymap.set("n", "<M-j>", "<cmd>cnext<CR>") -- Next quickfix item
vim.keymap.set("n", "<M-k>", "<cmd>cprev<CR>") -- Previous quickfix item
vim.keymap.set("n", "<M-q>", "<cmd>copen<CR>") -- Open quickfix list
Diagnostics
-- Open diagnostic quickfix list
vim.keymap.set(
"n",
"<leader>q",
vim.diagnostic.setloclist,
{ desc = "Open diagnostic [Q]uickfix list" }
)
-- Show diagnostic error messages
vim.keymap.set(
"n",
"<leader>de",
vim.diagnostic.open_float,
{ desc = "Show diagnostic [E]rror messages" }
)
-- Yank diagnostic message under cursor
vim.keymap.set("n", "<leader>dy", function()
local diagnostics = vim.diagnostic.get(0, { lnum = vim.fn.line(".") - 1 })
if #diagnostics > 0 then
local messages = {}
for _, diag in ipairs(diagnostics) do
table.insert(messages, diag.message)
end
local full_message = table.concat(messages, "\n")
vim.fn.setreg("+", full_message)
vim.fn.setreg('"', full_message)
vim.notify("Diagnostic message(s) yanked to clipboard", vim.log.levels.INFO)
else
vim.notify("No diagnostic message on this line", vim.log.levels.WARN)
end
end, { desc = "[D]iagnostic [Y]ank message" })
Code actions and LSP
-- Code actions with tiny-code-action
vim.keymap.set({ "n", "x" }, "<leader>ca", function()
require("tiny-code-action").code_action({})
end, { noremap = true, silent = true, desc = "[C]ode [A]ction" })
-- Run codelens actions
vim.keymap.set("n", "<leader>cl", vim.lsp.codelens.run, { desc = "Run [C]ode[L]ens actions" })
-- LSP Call Hierarchy
vim.keymap.set(
"n",
"<leader>ci",
vim.lsp.buf.incoming_calls,
{ desc = "Show [C]all hierarchy [I]ncoming" }
)
vim.keymap.set(
"n",
"<leader>co",
vim.lsp.buf.outgoing_calls,
{ desc = "Show [C]all hierarchy [O]utgoing" }
)
Buffer-local LSP keymaps
These keymaps are set up automatically when an LSP server attaches to a buffer:
local function setup_keymaps(bufnr)
local function map(mode, lhs, rhs, desc)
vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = "LSP: " .. desc, silent = true })
end
-- Hover documentation
map("n", "K", function()
vim.lsp.buf.hover({
border = "rounded",
max_height = 25,
max_width = 120,
})
end, "Hover documentation")
-- Signature help
map("n", "<C-k>", function()
vim.lsp.buf.signature_help({ border = "rounded" })
end, "Signature help")
map("i", "<C-k>", function()
vim.lsp.buf.signature_help({ border = "rounded" })
end, "Signature help")
-- Code actions & Refactoring
map({ "n", "x" }, "ga", vim.lsp.buf.code_action, "Code action")
map("n", "<leader>rn", vim.lsp.buf.rename, "Rename symbol")
-- Diagnostics navigation
map("n", "[d", function()
vim.diagnostic.jump({ count = -1 })
end, "Previous diagnostic")
map("n", "]d", function()
vim.diagnostic.jump({ count = 1 })
end, "Next diagnostic")
map("n", "<leader>cd", vim.diagnostic.open_float, "Show diagnostic")
-- Workspace folders
map("n", "<leader>wa", vim.lsp.buf.add_workspace_folder, "Add workspace folder")
map("n", "<leader>wr", vim.lsp.buf.remove_workspace_folder, "Remove workspace folder")
map("n", "<leader>wl", function()
print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
end, "List workspace folders")
-- Toggle inlay hints
map("n", "<leader>ih", function()
vim.lsp.inlay_hint.enable(
not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }),
{ bufnr = bufnr }
)
vim.notify(
"Inlay Hints "
.. (vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }) and "Enabled" or "Disabled")
)
end, "Toggle inlay hints")
end
LSP navigation keymaps (gd, gr, gi, gt, gD) are handled by Snacks pickers for better UX and are not shown in the basic LSP keymap setup.
Window management
-- Focus floating window
vim.keymap.set("n", "<C-w>f", function()
local wins = vim.api.nvim_list_wins()
for _, win in ipairs(wins) do
local config = vim.api.nvim_win_get_config(win)
if config.relative ~= "" then -- This is a floating window
vim.api.nvim_set_current_win(win)
break
end
end
end, { desc = "Focus floating [W]indow" })
Terminal mode
-- Exit terminal mode with double Escape
vim.keymap.set("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode" })
Custom delete behavior
-- Delete without copying to clipboard
vim.keymap.set({ "n", "x" }, "d", '"_d', { desc = "Delete without copying to clipboard" })
vim.keymap.set(
{ "n", "x" },
"D",
'"_D',
{ desc = "Delete to end of line without copying to clipboard" }
)
This configuration remaps d to delete without yanking. Use visual mode + yank if you need to copy text before deleting.
Utility keymaps
-- Open link under cursor
vim.keymap.set("n", "gX", function()
local target = vim.fn.expand("<cfile>")
if target == nil or target == "" then
vim.notify("No link under cursor", vim.log.levels.WARN)
return
end
vim.ui.open(target)
end, { desc = "Open link under cursor" })
-- Get highlighted line numbers
vim.keymap.set(
"v",
"<leader>ln",
':lua require("core.utils").get_highlighted_line_numbers()<CR>',
{ desc = "Copy Line Numbers", silent = true }
)
-- Copy file path with line number
vim.keymap.set("n", "<leader>cp", function()
require("core.utils").copyFilePathAndLineNumber()
end, { desc = "Copy file path with line number" })
Disabling unwanted keymaps
-- Disable command-line window (q:)
vim.keymap.set({ "n", "v" }, "q:", function()
vim.notify("Command-line window (q:) is disabled. Use :q to quit.", vim.log.levels.INFO)
end, { desc = "Disable command-line window (q:)" })
Creating keymap groups
For better organization, group related keymaps:
-- Diagnostic keymaps
local diagnostic_maps = {
{ "n", "<leader>de", vim.diagnostic.open_float, "Show diagnostic error" },
{ "n", "<leader>q", vim.diagnostic.setloclist, "Diagnostic quickfix" },
{ "n", "[d", function() vim.diagnostic.jump({ count = -1 }) end, "Previous diagnostic" },
{ "n", "]d", function() vim.diagnostic.jump({ count = 1 }) end, "Next diagnostic" },
}
for _, map in ipairs(diagnostic_maps) do
vim.keymap.set(map[1], map[2], map[3], { desc = map[4], silent = true })
end
Best practices
- Use descriptive descriptions - The
desc field shows in which-key and helps document your keymaps
- Avoid conflicts - Check existing keymaps with
:map <key> before mapping
- Use leader key - Prefix custom keymaps with
<leader> to avoid conflicts
- Buffer-local when appropriate - Use
{ buffer = bufnr } for filetype or LSP-specific keymaps
- Silent by default - Add
silent = true to avoid showing command messages
Checking keymaps
:map " All keymaps
:nmap " Normal mode keymaps
:imap " Insert mode keymaps
:vmap " Visual mode keymaps
:map <leader> " All leader keymaps