Files
nvim/lua/keymap.lua
T
2026-08-15 20:44:19 +01:00

527 lines
17 KiB
Lua

-- Basic Keymaps
-- See `:help vim.keymap.set()`
local helpers = require("helpers")
-- Swap : and ; around
vim.keymap.set({ "n", "v" }, ":", ";")
vim.keymap.set({ "n", "v" }, ";", ":")
-- Clear highlights on search when pressing <Esc> in normal mode
-- See `:help hlsearch`
vim.keymap.set("n", "<Esc>", "<cmd>nohlsearch<CR>")
-- Diagnostic keymaps
vim.keymap.set("n", "<leader>q", vim.diagnostic.setloclist, { desc = "Open diagnostic [Q]uickfix list" })
vim.keymap.set({ "n", "v" }, "c", '"zc', { desc = "Change without copying to clipboard" })
vim.keymap.set({ "n", "v" }, "x", '"zx', { desc = "Cut without copying to clipboard" })
vim.keymap.set("v", "p", '"zdP', { desc = "Paste over selection without yanking replaced text" })
-- Exit terminal mode in the builtin terminal with a shortcut that is a bit easier
-- for people to discover. Otherwise, you normally need to press <C-\><C-n>, which
-- is not what someone will guess without a bit more experience.
--
-- NOTE: This won't work in all terminal emulators/tmux/etc. Try your own mapping
-- or just use <C-\><C-n> to exit terminal mode
vim.keymap.set("t", "<Esc><Esc>", "<C-\\><C-n>", { desc = "Exit terminal mode" })
vim.keymap.set("n", "<Leader>z", function()
vim.treesitter.inspect_tree()
end, { desc = "Treesitter" })
vim.keymap.set("n", "<C-S-D>", function()
local node = vim.treesitter.get_node({})
local range = { vim.treesitter.get_node_range(node) }
vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 })
vim.fn.setpos("'x", { 0, range[1] + 1, range[2] + 1, 0 })
vim.cmd.normal("v`x")
end, { desc = "Select surrounding treesitter node" })
vim.keymap.set("v", "<C-S-D>", function()
local start = vim.api.nvim_win_get_cursor(0)
local start_row, start_column
if start[2] == 0 then
start_row = start[1] - 1
start_column = 0
else
start_row = start[1]
start_column = start[2] - 1
end
vim.api.nvim_win_set_cursor(0, { start_row, start_column })
local node = vim.treesitter.get_node({})
local range = { vim.treesitter.get_node_range(node) }
vim.api.nvim_win_set_cursor(0, { range[3] + 1, range[4] - 1 })
vim.fn.setpos("'x", { 0, range[1] + 1, range[2] + 1, 0 })
vim.cmd.normal("i")
vim.cmd.normal("v`x")
end, { desc = "Select surrounding treesitter node" })
local swappable_nodes = {
"argument",
"array_element_initializer",
"simple_parameter",
"string",
"integer",
"member_call_expression",
"method_declaration",
}
local function closest_swappable_node(node)
while
node
and not vim.list_contains(swappable_nodes, node:type())
and (not node:next_named_sibling() or not node:prev_named_sibling())
do
node = node:parent()
end
return node
end
vim.keymap.set({ "n" }, "<C-S-h>", function()
local node = closest_swappable_node(vim.treesitter.get_node())
if not node then
return
end
local prev_node = node:prev_named_sibling()
while prev_node and not vim.list_contains(swappable_nodes, prev_node:type()) do
prev_node = prev_node:prev_named_sibling()
end
if prev_node == nil then
return
end
require("nvim-treesitter.ts_utils").swap_nodes(node, prev_node, 0, true)
end, { desc = "Swap with node to the left" })
vim.keymap.set({ "n" }, "<C-S-l>", function()
local node = closest_swappable_node(vim.treesitter.get_node())
if not node then
return
end
local next_node = node:next_named_sibling()
while next_node and not vim.list_contains(swappable_nodes, next_node:type()) do
next_node = next_node:next_named_sibling()
end
if next_node == nil then
return
end
require("nvim-treesitter.ts_utils").swap_nodes(node, next_node, 0, true)
end, { desc = "Swap with node to the right" })
vim.keymap.set("n", "<C-S-j>", "ddp", { desc = "Move line down" })
vim.keymap.set("n", "<C-S-k>", "ddkP", { desc = "Move line up" })
vim.keymap.set("v", "<C-S-j>", "do<Esc>p`[v`]", { desc = "Move selection down" })
vim.keymap.set("v", "<C-S-k>", "dkO<Esc>p`[v`]", { desc = "Move selection up" })
-- Keybinds to make split navigation easier.
-- Use CTRL+<hjkl> to switch between windows
--
-- See `:help wincmd` for a list of all window commands
local function win_or_treesj(dir_cmd, desc)
return function()
local cur = vim.api.nvim_get_current_win()
vim.cmd("wincmd " .. dir_cmd)
if vim.api.nvim_get_current_win() == cur then
local ok, treesj = pcall(require, "treesj")
if ok and type(treesj.toggle) == "function" then
treesj.toggle()
end
end
end, { desc = desc }
end
vim.keymap.set("n", "<C-h>", "<C-w><C-h>", { desc = "Move focus to the left window" })
vim.keymap.set("n", "<C-l>", "<C-w><C-l>", { desc = "Move focus to the right window" })
vim.keymap.set("n", "<C-j>", (win_or_treesj)("j", "Move focus to the lower window or treesj.toggle()"))
vim.keymap.set("n", "<C-k>", (win_or_treesj)("k", "Move focus to the upper window or treesj.toggle()"))
vim.keymap.set("t", "<C-h>", "<C-\\><C-n><C-w>h", { desc = "Move focus to the left window" })
vim.keymap.set("t", "<C-l>", "<C-\\><C-n><C-w>l", { desc = "Move focus to the right window" })
vim.keymap.set("t", "<C-j>", "<C-\\><C-n><C-w>j", { desc = "Move focus to the lower window" })
vim.keymap.set("t", "<C-k>", "<C-\\><C-n><C-w>k", { desc = "Move focus to the upper window" })
vim.keymap.set({ "i" }, "<C-J>", function()
local ls = require("luasnip")
if ls.choice_active() then
ls.change_choice(1)
end
end, { desc = "Toggle snippet choice", silent = true })
vim.keymap.set({ "i" }, "<C-k>", function()
local ls = require("luasnip")
if ls.choice_active() then
ls.change_choice(-1)
end
end, { desc = "Toggle snippet choice", silent = true })
vim.keymap.set({ "i", "s" }, "<C-L>", function()
local ls = require("luasnip")
if ls.expandable() then
ls.expand()
elseif ls.in_snippet() then
ls.jump(1)
end
end, { desc = "Expand snippet", silent = true })
vim.keymap.set({ "i", "s" }, "<C-H>", function()
local ls = require("luasnip")
if ls.in_snippet() then
ls.jump(-1)
end
end, { desc = "Go back a snippet slot", silent = true })
vim.keymap.set("n", "<Leader>.", "<Cmd>tabnext<CR>", { desc = "Next tab" })
vim.keymap.set("n", "<Leader>,", "<Cmd>tabprevious<CR>", { desc = "Previous tab" })
-- Overriding CTRL mappings because some of them are stupid
vim.keymap.set({ "n", "v", "c", "i" }, "<C-S-B>", "<CMD>Git blame<CR>", { desc = "Git blame" })
vim.keymap.set(
{ "n", "v", "c", "i" },
"<C-Q>",
"<CMD>Telescope lsp_document_symbols<CR>",
{ desc = "Show symbols in current document" }
)
vim.keymap.set(
{ "n", "v", "c", "i" },
"<C-S-Q>",
"<CMD>Telescope lsp_workspace_symbols<CR>",
{ desc = "Show symbols in workspace" }
)
vim.keymap.set({ "n", "v", "c", "i" }, "<C-E>", "<CMD>Telescope oldfiles<CR>", { desc = "Show recently opened files" })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-P>", function()
require("telescope.builtin").find_files({
show_untracked = true,
no_ignore = false,
hidden = true,
})
end, { desc = "Find all files" })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-S-P>", function()
require("telescope.builtin").find_files({
show_untracked = true,
no_ignore = true,
hidden = true,
})
end, { desc = "Find all files" })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-f>", function()
require("telescope.builtin").live_grep({
hidden = true,
})
end, { desc = "Search within the whole project" })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-Y>", "<CMD>Telescope quickfix<CR>", { desc = "Show quickfix list" })
vim.keymap.set(
{ "n", "v", "c", "i" },
"<C-S-Y>",
"<CMD>Telescope quickfixhistory<CR>",
{ desc = "Show quickfix history" }
)
vim.keymap.set({ "n", "v", "c", "i", "t" }, "<C-A>", function()
local ai_tools = { "codex", "agy", "opencode" }
local ai_win_opts = {
width = math.floor(vim.o.columns / 3),
vertical = true,
split = "right",
}
local term_bufnr = -1
for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[bufnr].buftype == "terminal" then
local name = vim.api.nvim_buf_get_name(bufnr)
for _, tool in ipairs(ai_tools) do
if name:match(":" .. tool .. "$") or name:match("/" .. tool .. "$") then
term_bufnr = bufnr
end
end
end
end
if term_bufnr ~= -1 then
local term_winid = vim.fn.bufwinid(term_bufnr)
if term_winid ~= -1 then
vim.api.nvim_win_hide(term_winid)
return
end
vim.api.nvim_open_win(term_bufnr, true, ai_win_opts)
vim.cmd.startinsert()
return
end
for _, tool in ipairs(ai_tools) do
if vim.fn.executable(tool) == 1 then
helpers.open_term({
cmd = tool,
win_opts = ai_win_opts,
buf_opts = {
bufhidden = "hide",
modifiable = false,
},
})
return
end
end
vim.api.nvim_echo(
{ { "No AI tool found. Please install one of the following: codex, agy, opencode", "WarningMsg" } },
false,
{}
)
end, { desc = "Toggle AI tool" })
-- vim.keymap.set({ 'n', 'v', 'c', 'i' }, '<C-A>', '<CMD>vsplit term://codex<CR>i', { desc = 'Open AI tool' })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-S>", function()
require("snacks").scratch()
end, { desc = "Open scratchpad" })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-S-S>", function()
require("snacks").scratch.select()
end, { desc = "Open scratchpad buffers" })
vim.keymap.set({ "n", "v", "c", "i", "t" }, "<C-T>", function()
require("snacks").terminal.toggle()
end, { desc = "Toggle terminal" })
vim.keymap.set({ "n", "v", "c", "i" }, "<C-N>", "<Esc>", { desc = "Enter normal mode" })
vim.keymap.set({ "t" }, "<C-N>", "<C-\\><C-N>", { desc = "Enter normal mode" })
vim.keymap.set({ "t" }, "<C-U>", "<C-\\><C-N><C-U>", { desc = "Scroll up in terminal" })
vim.keymap.set({ "t" }, "<C-D>", "<C-\\><C-N><C-D>", { desc = "Scroll down in terminal" })
-- Editing helpers
vim.keymap.set("i", "<C-O>", "<Esc>o", { desc = "Add line below" })
vim.keymap.set("i", "<C-S-O>", "<Esc>O", { desc = "Add line above" })
local esc_keys = { "jj", "jk", "kk" }
for _, key in ipairs(esc_keys) do
vim.keymap.set("i", key, "<Esc>", { desc = "Exit insert mode" })
end
vim.keymap.set("i", "<C-D>", "<Esc>ddi", { desc = "Delete line" })
vim.keymap.set("i", "<Leader>;", "<Esc>mzA;<Esc>`za", { desc = "Append a semicolon" })
vim.keymap.set("n", "<Leader>;", "mzA;<Esc>`za", { desc = "Append a semicolon" })
vim.keymap.set("n", "<Leader>{", "mzF[`a<CR><Esc>``%i<CR><Esc>`z", { desc = "Indent an array" })
vim.keymap.set(
"n",
"<Leader>}",
"mzF[`a<CR><Esc>``%i<CR><Esc>`zvi[:s/,\\s*/,\\r/g<CR>vi[=<Esc>`z:nohlsearch<CR>",
{ desc = "Indent an array some other way?" }
)
-- Git mappings
vim.keymap.set("n", "<Leader>gb", "<CMD>Git blame<CR>", { desc = "Git blame" })
vim.keymap.set("n", "<Leader>gd", "<CMD>Git diff<CR>", { desc = "Git diff" })
vim.keymap.set("n", "<Leader>gdc", "<CMD>Git diff --cached<CR>", { desc = "Git diff" })
vim.keymap.set("n", "<Leader>G", function()
helpers.open_term({ cmd = "lazygit" })
end, { desc = "Git" })
-- Add keymaps for diff mode
vim.api.nvim_create_autocmd("BufEnter", {
callback = function()
if vim.wo.diff then
-- Keymaps for navigating hunks
vim.api.nvim_buf_set_keymap(
0,
"n",
"<Tab>",
"]c",
{ noremap = true, silent = true, desc = "Next hunk in diff mode" }
)
vim.api.nvim_buf_set_keymap(
0,
"n",
"<S-Tab>",
"[c",
{ noremap = true, silent = true, desc = "Previous hunk in diff mode" }
)
-- Keymaps for resolving conflicts
vim.api.nvim_buf_set_keymap(
0,
"n",
"<leader>dl",
":diffget LOCAL<CR>",
{ noremap = true, silent = true, desc = "Get LOCAL changes" }
)
vim.api.nvim_buf_set_keymap(
0,
"n",
"<leader>dr",
":diffget REMOTE<CR>",
{ noremap = true, silent = true, desc = "Get REMOTE changes" }
)
end
end,
})
-- AI mappings
-- vim.keymap.set('v', '<Leader>ae', '<CMD>CodeCompanion<CR>', { desc = 'Edit selection with AI' })
-- vim.keymap.set('n', '<Leader>ac', '<CMD>CodeCompanionCmd<CR>', { desc = 'Run Neovim commands with AI' })
vim.api.nvim_create_autocmd("FileType", {
pattern = "codecompanion",
callback = function()
vim.keymap.set("i", "<Enter>", function()
require("codecompanion").last_chat():submit()
end, { buffer = true, desc = "Use enter to send the message" })
vim.keymap.set("i", "<S-Enter>", "<Enter>", { buffer = true, desc = "Use shift enter to start a new line" })
vim.keymap.set(
"n",
"<Enter>",
"G",
{ buffer = true, desc = "Use enter in normal mode to go to the end of the chat" }
)
local models = { vim.env.DEFAULT_AI_MODEL, vim.env.REASONING_MODEL, vim.env.FAST_MODEL }
-- Loop through models and create keymaps for each
for i, model in ipairs(models) do
vim.keymap.set("n", "<C-" .. i .. ">", 'mzggj0W"_C' .. model .. "<Esc>`z", { desc = "Switch to " .. model })
vim.keymap.set(
"i",
"<C-" .. i .. ">",
'<Esc>mzggj0W"_C' .. model .. "<Esc>`za",
{ desc = "Switch to " .. model }
)
end
end,
})
-- Search mappings
vim.keymap.set("n", "<Leader>sGb", "<CMD>Telescope git_branches<CR>", { desc = "Search git branches" })
vim.keymap.set("n", "<Leader>sGc", "<CMD>Telescope git_commits<CR>", { desc = "Search git history" })
vim.keymap.set(
"n",
"<Leader>sGh",
"<CMD>Telescope git_bcommits<CR>",
{ desc = "Search git history within current buffer/selection" }
)
vim.keymap.set("n", "<Leader>sGs", "<CMD>Telescope git_status<CR>", { desc = "Search git status" })
vim.keymap.set("n", "<Leader>sGS", "<CMD>Telescope git_stash<CR>", { desc = "Search git stash" })
-- Misc
vim.keymap.set("n", "<Leader>]", "<CMD>cnext<CR>", { desc = "Next item in quickfix list" })
vim.keymap.set("n", "<Leader>[", "<CMD>cprevious<CR>", { desc = "Previous item in quickfix list" })
vim.keymap.set("n", "gd", "<CMD>Telescope lsp_definitions<CR>", { desc = "Go to definition" })
vim.keymap.set("n", "<Leader>r", function()
for _, c in ipairs(vim.lsp.get_clients({ bufnr = 0 })) do
if c.name == "phpantom_lsp" then
c:stop(true)
end
end
vim.cmd("edit")
end, { desc = "Reload buffer" })
local function open_test()
require("neotest").summary.open()
require("neotest").output_panel.open()
end
-- Testing
local test_maps = {
{
keys = { "<Leader>tc" },
action = function()
require("neotest").summary.close()
require("neotest").output_panel.close()
end,
desc = "Close [T]est panels",
},
{
keys = { "<Leader>tn" },
action = function()
require("neotest").run.run()
open_test()
end,
desc = "Run nearest [T]est",
},
{
keys = { "<Leader>ta" },
action = function()
require("neotest").run.run({ suite = true })
open_test()
end,
desc = "Run all [T]ests in the project",
},
{
keys = { "<Leader>tp" },
action = function()
require("neotest").run.run_last()
open_test()
end,
desc = "Run previous [T]est again",
},
{
keys = { "<Leader>td" },
action = function()
local dap = require("dap")
if dap.session() == nil then
dap.continue()
end
require("dapui").open()
local neotest = require("neotest")
local bufnr = vim.api.nvim_get_current_buf()
local row = vim.api.nvim_win_get_cursor(0)[1] - 1
local adapters = neotest.state.adapter_ids()
local found = false
vim.g.neotest_debug = true
for _, adapter_id in ipairs(adapters) do
local tree = neotest.state.positions(adapter_id, { buffer = bufnr })
if tree then
local nearest = require("neotest.lib.positions").nearest(tree, row)
if nearest and nearest:data().type ~= "file" then
neotest.run.run()
found = true
break
end
end
end
if not found then
neotest.run.run_last()
end
vim.g.neotest_debug = nil
end,
desc = "Run last [T]est with debugger",
},
}
for _, map_info in ipairs(test_maps) do
for _, key in ipairs(map_info.keys) do
vim.keymap.set("n", key, map_info.action, { desc = map_info.desc })
end
end
vim.keymap.set("n", "<Leader>tf", function()
require("neotest").run.run(vim.fn.expand("%"))
open_test()
end, { desc = "Run all tests in the current file" })
vim.keymap.set("n", "<Leader>tc", function()
require("neotest").summary.close()
require("neotest").output_panel.close()
end, { desc = "Close test panels" })
vim.keymap.set("i", "<Tab>", function()
local ls = require("luasnip")
if ls.jumpable(1) then
vim.api.nvim_echo({ { "Jumping in snippet" } }, false, {})
ls.jump()
else
vim.api.nvim_echo({ { "Inserting tab" } }, false, {})
vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<Tab>", true, true, true), "n", true)
end
end, { desc = "Luasnip accept copilot or jump forward" })
-- Leaving this commented out, I will try the format command instead
-- "A command to properly indent json code
-- command! FormatJSON %!python -m json.tool
-- Edit the snippet file for the current buffer filetype or the snippets
-- directory if the file does not exist
vim.keymap.set("n", "<Leader>es", function()
local ft = vim.bo.filetype
if ft == "vue" or ft == "typescript" then
ft = "javascript"
end
local snippets_dir = vim.fn.stdpath("config") .. "/lua/snippets"
local snippets_file = snippets_dir .. "/" .. ft .. ".lua"
vim.cmd("tabedit " .. snippets_file)
end, { desc = "Edit snippets file" })
vim.keymap.set("n", "<Leader>cz", function()
require("utils/cheatsheets").open("zsh")
end, { desc = "Open zsh cheat sheet" })
vim.keymap.set("n", "<Leader>cb", function()
require("utils/cheatsheets").open("bash")
end, { desc = "Open bash cheat sheet" })
vim.keymap.set("n", "<Leader>cr", function()
require("utils/cheatsheets").open("regex")
end, { desc = "Open RegExp cheat sheet" })
require("helpers").edit_cf("k", "/lua/keymap.lua")