Skip to main content
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

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
-- 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" })

Search enhancements

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
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

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
-- 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:
lua/plugins/lsp/init.lua
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

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
-- Exit terminal mode with double Escape
vim.keymap.set("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode" })

Custom delete behavior

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
-- 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

lua/config/keymaps.lua
-- 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

  1. Use descriptive descriptions - The desc field shows in which-key and helps document your keymaps
  2. Avoid conflicts - Check existing keymaps with :map <key> before mapping
  3. Use leader key - Prefix custom keymaps with <leader> to avoid conflicts
  4. Buffer-local when appropriate - Use { buffer = bufnr } for filetype or LSP-specific keymaps
  5. 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

Build docs developers (and LLMs) love