-- 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 in normal mode -- See `:help hlsearch` vim.keymap.set('n', '', 'nohlsearch') -- Diagnostic keymaps vim.keymap.set('n', '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 , 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 to exit terminal mode vim.keymap.set('t', '', '', { desc = 'Exit terminal mode' }) vim.keymap.set('n', 'z', function() vim.treesitter.inspect_tree() end, { desc = 'Treesitter' }) vim.keymap.set('n', '', 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', '', 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' }, '', 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' }, '', 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', '', 'ddp', { desc = 'Move line down' }) vim.keymap.set('n', '', 'ddkP', { desc = 'Move line up' }) vim.keymap.set('v', '', 'dop`[v`]', { desc = 'Move selection down' }) vim.keymap.set('v', '', 'dkOp`[v`]', { desc = 'Move selection up' }) -- Keybinds to make split navigation easier. -- Use CTRL+ 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', '', '', { desc = 'Move focus to the left window' }) vim.keymap.set('n', '', '', { desc = 'Move focus to the right window' }) vim.keymap.set('n', '', (win_or_treesj)('j', 'Move focus to the lower window or treesj.toggle()')) vim.keymap.set('n', '', (win_or_treesj)('k', 'Move focus to the upper window or treesj.toggle()')) vim.keymap.set('t', '', 'h', { desc = 'Move focus to the left window' }) vim.keymap.set('t', '', 'l', { desc = 'Move focus to the right window' }) vim.keymap.set('t', '', 'j', { desc = 'Move focus to the lower window' }) vim.keymap.set('t', '', 'k', { desc = 'Move focus to the upper window' }) vim.keymap.set({ 'i' }, '', 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' }, '', 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' }, '', 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' }, '', 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', '.', 'tabnext', { desc = 'Next tab' }) vim.keymap.set('n', ',', 'tabprevious', { desc = 'Previous tab' }) -- Overriding CTRL mappings because some of them are stupid vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Git blame', { desc = 'Git blame' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope lsp_document_symbols', { desc = 'Show symbols in current document' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope lsp_workspace_symbols', { desc = 'Show symbols in workspace' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope oldfiles', { desc = 'Show recently opened files' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 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' }, '', 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' }, '', function() require('telescope.builtin').live_grep { hidden = true, } end, { desc = 'Search within the whole project' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope quickfix', { desc = 'Show quickfix list' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope quickfixhistory', { desc = 'Show quickfix history' }) vim.keymap.set({ 'n', 'v', 'c', 'i', 't' }, '', 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' }, '', 'vsplit term://codexi', { desc = 'Open AI tool' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', function() require('snacks').scratch() end, { desc = 'Open scratchpad' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', function() require('snacks').scratch.select() end, { desc = 'Open scratchpad buffers' }) vim.keymap.set({ 'n', 'v', 'c', 'i', 't' }, '', function() require('snacks').terminal.toggle() end, { desc = 'Toggle terminal' }) -- Editing helpers vim.keymap.set('i', '', 'o', { desc = 'Add line below' }) vim.keymap.set('i', '', 'O', { desc = 'Add line above' }) local esc_keys = { 'jj', 'jk', 'kk' } for _, key in ipairs(esc_keys) do vim.keymap.set('i', key, '', { desc = 'Exit insert mode' }) end vim.keymap.set('i', '', 'ddi', { desc = 'Delete line' }) vim.keymap.set('i', ';', 'mzA;`za', { desc = 'Append a semicolon' }) vim.keymap.set('n', ';', 'mzA;`za', { desc = 'Append a semicolon' }) vim.keymap.set('n', '{', 'mzF[`a``%i`z', { desc = 'Indent an array' }) vim.keymap.set('n', '}', 'mzF[`a``%i`zvi[:s/,\\s*/,\\r/gvi[=`z:nohlsearch', { desc = 'Indent an array some other way?' }) -- Git mappings vim.keymap.set('n', 'gb', 'Git blame', { desc = 'Git blame' }) vim.keymap.set('n', 'gd', 'Git diff', { desc = 'Git diff' }) vim.keymap.set('n', 'gdc', 'Git diff --cached', { desc = 'Git diff' }) vim.keymap.set('n', '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', '', ']c', { noremap = true, silent = true, desc = 'Next hunk in diff mode' }) vim.api.nvim_buf_set_keymap(0, 'n', '', '[c', { noremap = true, silent = true, desc = 'Previous hunk in diff mode' }) -- Keymaps for resolving conflicts vim.api.nvim_buf_set_keymap(0, 'n', 'dl', ':diffget LOCAL', { noremap = true, silent = true, desc = 'Get LOCAL changes' }) vim.api.nvim_buf_set_keymap(0, 'n', 'dr', ':diffget REMOTE', { noremap = true, silent = true, desc = 'Get REMOTE changes' }) end end, }) -- AI mappings -- vim.keymap.set('v', 'ae', 'CodeCompanion', { desc = 'Edit selection with AI' }) -- vim.keymap.set('n', 'ac', 'CodeCompanionCmd', { desc = 'Run Neovim commands with AI' }) vim.api.nvim_create_autocmd('FileType', { pattern = 'codecompanion', callback = function() vim.keymap.set('i', '', function() require('codecompanion').last_chat():submit() end, { buffer = true, desc = 'Use enter to send the message' }) vim.keymap.set('i', '', '', { buffer = true, desc = 'Use shift enter to start a new line' }) vim.keymap.set('n', '', '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', '', 'mzggj0W"_C' .. model .. '`z', { desc = 'Switch to ' .. model }) vim.keymap.set('i', '', 'mzggj0W"_C' .. model .. '`za', { desc = 'Switch to ' .. model }) end end, }) -- Search mappings vim.keymap.set('n', 'sGb', 'Telescope git_branches', { desc = 'Search git branches' }) vim.keymap.set('n', 'sGc', 'Telescope git_commits', { desc = 'Search git history' }) vim.keymap.set('n', 'sGh', 'Telescope git_bcommits', { desc = 'Search git history within current buffer/selection' }) vim.keymap.set('n', 'sGs', 'Telescope git_status', { desc = 'Search git status' }) vim.keymap.set('n', 'sGS', 'Telescope git_stash', { desc = 'Search git stash' }) -- Misc vim.keymap.set('n', ']', 'cnext', { desc = 'Next item in quickfix list' }) vim.keymap.set('n', '[', 'cprevious', { desc = 'Previous item in quickfix list' }) vim.keymap.set('n', 'gd', 'Telescope lsp_definitions', { desc = 'Go to definition' }) vim.keymap.set('n', '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 = { 'tc' }, action = function() require('neotest').summary.close() require('neotest').output_panel.close() end, desc = 'Close [T]est panels', }, { keys = { 'tn' }, action = function() require('neotest').run.run() open_test() end, desc = 'Run nearest [T]est', }, { keys = { 'ta' }, action = function() require('neotest').run.run { suite = true } open_test() end, desc = 'Run all [T]ests in the project', }, { keys = { 'tp' }, action = function() require('neotest').run.run_last() open_test() end, desc = 'Run previous [T]est again', }, { keys = { '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', 'tf', function() require('neotest').run.run(vim.fn.expand '%') open_test() end, { desc = 'Run all tests in the current file' }) vim.keymap.set('n', 'tc', function() require('neotest').summary.close() require('neotest').output_panel.close() end, { desc = 'Close test panels' }) vim.keymap.set('i', '', function() 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('', 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', 'es', function() local ft = vim.bo.filetype if ft == 'vue' 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', 'cz', function() require('utils/cheatsheets').open 'zsh' end, { desc = 'Open zsh cheat sheet' }) vim.keymap.set('n', 'cb', function() require('utils/cheatsheets').open 'bash' end, { desc = 'Open bash cheat sheet' }) vim.keymap.set('n', 'cr', function() require('utils/cheatsheets').open 'regex' end, { desc = 'Open RegExp cheat sheet' }) require('helpers').edit_cf('k', '/lua/keymap.lua')