From e8979088e68ce135e960a6380b185a96cdcca94d Mon Sep 17 00:00:00 2001 From: Jeremie Fraeys Date: Mon, 23 Mar 2026 20:32:36 -0400 Subject: [PATCH] refactor: reorganize keymaps and autocommands - Restructure mappings.lua for better organization - Refactor autocmds.lua with improved event handling --- lua/config/autocmds.lua | 197 ++++++++++++++++++++++++++++------------ lua/config/mappings.lua | 174 ++++++++++++++++++++--------------- 2 files changed, 241 insertions(+), 130 deletions(-) diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 64df1a1..c602624 100755 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -2,18 +2,49 @@ local function augroup(name) return vim.api.nvim_create_augroup('nvim_' .. name, { clear = true }) end --- Auto-format on save -vim.api.nvim_create_autocmd('BufWritePre', { +local autocmd = vim.api.nvim_create_autocmd + +-- Remove trailing whitespace (runs before LSP format) +autocmd('BufWritePre', { + group = augroup('trailing_whitespace'), + pattern = '*', + command = [[%s/\s\+$//e]], + desc = 'Remove trailing whitespace on save', +}) + +-- Ensure file ends with a newline (skip binary/yaml) +autocmd('BufWritePre', { + group = augroup('fix_eol'), + pattern = '*', + callback = function() + local ft = vim.bo.filetype + local skip = { yaml = true, yaml_frontmatter = true } + if not skip[ft] and not vim.bo.eol and not vim.bo.binary then + vim.bo.eol = true + end + end, + desc = 'Ensure newline at EOF', +}) + +-- Auto-format on save via LSP +autocmd('BufWritePre', { group = augroup('format_on_save'), pattern = '*', callback = function() - vim.lsp.buf.format({ async = false }) + -- Only format if an LSP with formatting support is attached + local clients = vim.lsp.get_clients({ bufnr = 0 }) + for _, client in ipairs(clients) do + if client:supports_method('textDocument/formatting', 0) then + vim.lsp.buf.format({ async = false, id = client.id }) + return + end + end end, desc = 'Auto-format on save with LSP', }) --- Highlight on yank -vim.api.nvim_create_autocmd('TextYankPost', { +-- Highlight yanked text +autocmd('TextYankPost', { group = augroup('highlight_yank'), callback = function() vim.hl.on_yank() @@ -21,71 +52,123 @@ vim.api.nvim_create_autocmd('TextYankPost', { desc = 'Highlight yanked text', }) --- Remove trailing whitespace -vim.api.nvim_create_autocmd('BufWritePre', { - group = augroup('remove_trailing_whitespace'), - pattern = '*', - command = [[%s/\s\+$//e]], - desc = 'Remove trailing whitespace on save', -}) +local function get_project_root() + local markers = { '.git', '.hg', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'package.json' } + local buf_dir = vim.fn.expand('%:p:h') + return vim.fs.root(buf_dir, markers) or buf_dir +end --- Append newline at EOF (excluding YAML) -vim.api.nvim_create_autocmd('BufWritePre', { - group = augroup('append_newline_eof'), - pattern = '*', +local function make_relative(paths, root) + return vim.tbl_map(function(p) + return vim.fn.fnamemodify(p, ':~:.') -- strip home + make relative to cwd + end, paths) +end + +local function fuzzy_find(text, _) + local root = get_project_root() + + if vim.fn.executable('fd') == 1 then + local result = vim.fn.systemlist( + string.format( + 'fd --type f --hidden --exclude .git -g "*%s*" %s', + vim.fn.escape(text, '"\\'), + vim.fn.shellescape(root) + ) + ) + if vim.v.shell_error == 0 and #result > 0 then + return make_relative(result, root) + end + end + + if vim.fn.executable('rg') == 1 then + local result = vim.fn.systemlist(string.format("rg --files --hidden --glob '!.git' %s", vim.fn.shellescape(root))) + if vim.v.shell_error == 0 then + return make_relative(vim.fn.matchfuzzy(result, text), root) + end + end + + local saved = vim.fn.getcwd() + vim.cmd('lcd ' .. vim.fn.fnameescape(root)) + local files = vim.fn.glob('**/*', false, true) + vim.cmd('lcd ' .. vim.fn.fnameescape(saved)) + return vim.fn.matchfuzzy(files, text) +end + +_G.fuzzy_find = fuzzy_find +vim.opt.findfunc = 'v:lua.fuzzy_find' + +-- Also anchor path to project root so gf works correctly +vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWinEnter' }, { callback = function() - local last_line = vim.fn.getline('$') - local ft = vim.bo.filetype - if last_line ~= '' and not last_line:match('\n$') and ft ~= 'yaml' then - vim.fn.append(vim.fn.line('$'), '') + local root = get_project_root() + if root then + vim.opt_local.path = { root .. '/**' } end end, - desc = 'Append newline at EOF (except YAML)', + desc = 'Anchor path to project root for gf/:find', }) --- Auto-update plugins on startup -vim.api.nvim_create_autocmd('VimEnter', { - group = augroup('autoupdate'), +-- Restore last cursor position when reopening a file +autocmd('BufReadPost', { + group = augroup('restore_cursor'), callback = function() - local ok, lazy_status = pcall(require, 'lazy.status') - if ok and lazy_status.has_updates() then - require('lazy').update({ show = false }) + local mark = vim.api.nvim_buf_get_mark(0, '"') + local line_count = vim.api.nvim_buf_line_count(0) + if mark[1] > 0 and mark[1] <= line_count then + pcall(vim.api.nvim_win_set_cursor, 0, mark) + vim.cmd('normal! zz') -- centre the restored position end end, - desc = 'Auto-update plugins on startup', + desc = 'Restore last cursor position on open', }) --- Sync plugins after lazy check -vim.api.nvim_create_autocmd('User', { - pattern = 'LazyCheck', - group = augroup('lazy_sync'), +-- Keep splits balanced when Neovim is resized +autocmd('VimResized', { + group = augroup('resize_splits'), callback = function() + vim.cmd('tabdo wincmd =') + end, + desc = 'Equalise splits on terminal resize', +}) + +vim.opt.wildmode = { 'longest:full', 'full' } +vim.opt.wildoptions = 'pum' + +autocmd('CmdlineLeave', { + group = augroup('cmdline_completion'), + pattern = ':', + callback = function() + vim.opt.wildmode = {} + end, + desc = 'Disable wildmenu popup outside cmdline', +}) + +autocmd('TermOpen', { + group = augroup('terminal'), + pattern = '*', + callback = function() + vim.opt_local.number = false + vim.opt_local.relativenumber = false + vim.opt_local.signcolumn = 'no' + vim.cmd('startinsert') + vim.keymap.set('t', '', '', { + buffer = true, + desc = 'Exit terminal mode', + }) + end, + desc = 'Terminal: no line numbers, start in insert, bind Esc', +}) + +autocmd('VimEnter', { + group = augroup('lazy_autoupdate'), + callback = function() + -- Defer so the UI is fully ready before the update check runs vim.schedule(function() - require('lazy').sync({ wait = false, show = false }) + local ok, lazy_status = pcall(require, 'lazy.status') + if ok and lazy_status.has_updates() then + require('lazy').update({ show = false }) + end end) end, - desc = 'Sync plugins after LazyCheck', -}) - --- Terminal buffer settings -vim.api.nvim_create_autocmd('TermOpen', { - group = augroup('terminal_settings'), - pattern = '*', - callback = function() - vim.cmd('startinsert') - vim.wo.number = false - vim.wo.relativenumber = false - vim.cmd('normal! G') - end, - desc = 'Terminal buffer auto-insert and no line numbers', -}) - --- Terminal mode escape mapping (applied per buffer) -vim.api.nvim_create_autocmd('TermOpen', { - group = augroup('terminal_escape'), - pattern = '*', - callback = function() - vim.keymap.set('t', '', '', { buffer = true, desc = 'Exit terminal mode' }) - end, - desc = 'Terminal escape mapping', + desc = 'Auto-update plugins on startup if updates are available', }) diff --git a/lua/config/mappings.lua b/lua/config/mappings.lua index f6901cf..78f4c4c 100755 --- a/lua/config/mappings.lua +++ b/lua/config/mappings.lua @@ -1,101 +1,131 @@ local utils = require('config.utils') +local builtin = require('telescope.builtin') --- Basic Keymaps --- Keymaps for better default experience --- See `:help vim.keymap.set()` +------------------------------------------------------------------------------- +-- Leader / mode escapes +------------------------------------------------------------------------------- vim.keymap.set({ 'n', 'v' }, '', '', { silent = true }) --- vim.keymap.set('n', 'pv', vim.cmd.Ex, { desc = "[P]roject [V]iew" }) -vim.keymap.set('n', '', '', { silent = true }) vim.keymap.set('n', '', 'nohlsearch', { silent = true }) +vim.keymap.set('n', '', '', { silent = true }) --- Remap for dealing with word wrap +-- Disable gp (avoid accidental paste-over in visual) +vim.keymap.set('n', 'gp', '', { silent = true }) + +------------------------------------------------------------------------------- +-- Motion / view +------------------------------------------------------------------------------- + +-- Respect wrapped lines for j/k vim.keymap.set('n', 'k', "v:count == 0 ? 'gk' : 'k'", { expr = true, silent = true }) vim.keymap.set('n', 'j', "v:count == 0 ? 'gj' : 'j'", { expr = true, silent = true }) --- Move lines -vim.keymap.set('v', 'J', ":m '>+1gv=gv", { silent = true }) -vim.keymap.set('v', 'K', ":m '<-2gv=gv", { silent = true }) -vim.keymap.set('n', '>', '>gv', { silent = true }) -vim.keymap.set('n', '<', '', 'zz', { desc = 'Half page down (centred)' }) +vim.keymap.set('n', '', 'zz', { desc = 'Half page up (centred)' }) +vim.keymap.set('n', 'n', 'nzzzv', { desc = 'Next match (centred)' }) +vim.keymap.set('n', 'N', 'Nzzzv', { desc = 'Prev match (centred)' }) --- Miscellaneous Navigation Keymaps -vim.keymap.set('n', '', 'zz', { desc = 'Half Page Jumping Up' }) -vim.keymap.set('n', '', 'zz', { desc = 'Half Page Jumping Down' }) -vim.keymap.set('n', 'n', 'nzzzv', { noremap = true, silent = true }) -vim.keymap.set('n', 'N', 'Nzzzv', { noremap = true, silent = true }) +------------------------------------------------------------------------------- +-- Window / split navigation +------------------------------------------------------------------------------- +vim.keymap.set('n', '', 'h', { desc = 'Focus left split' }) +vim.keymap.set('n', '', 'j', { desc = 'Focus split below' }) +vim.keymap.set('n', '', 'k', { desc = 'Focus split above' }) +vim.keymap.set('n', '', 'l', { desc = 'Focus right split' }) -vim.keymap.set('n', 'n', function() - vim.o.hlsearch = not vim.o.hlsearch -end, { desc = 'Toggle Search Highlight' }) +------------------------------------------------------------------------------- +-- Quickfix list (project-wide: LSP refs, grep, diagnostics) +------------------------------------------------------------------------------- +vim.keymap.set('n', ']q', 'cnextzz', { desc = 'Quickfix next' }) +vim.keymap.set('n', '[q', 'cprevzz', { desc = 'Quickfix prev' }) +vim.keymap.set('n', ']Q', 'clastzz', { desc = 'Quickfix last' }) +vim.keymap.set('n', '[Q', 'cfirstzz', { desc = 'Quickfix first' }) -vim.api.nvim_set_keymap('n', 'gp', '', { noremap = true, silent = true }) -vim.keymap.set('n', '', 'cnextzz') -vim.keymap.set('n', '', 'cprevzz') -vim.keymap.set('n', ']q', 'lnextzz', { desc = 'Location List Next' }) -vim.keymap.set('n', '[q', 'lprevzz', { desc = 'Location List Prev' }) +-- Location list (buffer-local: spell, some linters) +vim.keymap.set('n', ']l', 'lnextzz', { desc = 'Loclist next' }) +vim.keymap.set('n', '[l', 'lprevzz', { desc = 'Loclist prev' }) --- Buffer Navigation Keymaps -vim.keymap.set('n', 'bn', 'bnextzz', { desc = 'Quick Nav Buf Next' }) -vim.keymap.set('n', 'bp', 'bprevzz', { desc = 'Quick Nav Buf Prev' }) -vim.keymap.set('n', 'bd', 'bdelete', { desc = 'Quick Nav Buf Delete' }) -vim.keymap.set('n', 'bs', 'split', { desc = 'Open Buf Horizontal Split' }) -vim.keymap.set('n', 'bv', 'vsp', { desc = 'Open Buf Vertical Split' }) +------------------------------------------------------------------------------- +-- Buffer navigation +------------------------------------------------------------------------------- +vim.keymap.set('n', '', 'e #', { desc = 'Alternate buffer (toggle last two)' }) +vim.keymap.set('n', 'bn', 'bnext', { desc = 'Buffer next' }) +vim.keymap.set('n', 'bp', 'bprev', { desc = 'Buffer prev' }) +vim.keymap.set('n', 'bd', 'bdelete', { desc = 'Buffer delete' }) +vim.keymap.set('n', 'bs', 'split', { desc = 'Buffer horizontal split' }) +vim.keymap.set('n', 'bv', 'vsplit', { desc = 'Buffer vertical split' }) --- Editing Keymaps -vim.keymap.set('x', 'p', [["_dP"]], { desc = 'Paste without register' }) -vim.keymap.set({ 'n', 'v' }, 'D', [["_d"]], { desc = 'Delete without register' }) -vim.keymap.set({ 'n', 'v' }, 'y', '"+y', { desc = 'Copy to + register' }) -vim.keymap.set('n', 'Y', '"+Y') --- replace current word in file scope +------------------------------------------------------------------------------- +-- Editing +------------------------------------------------------------------------------- + +-- Move selected lines up/down and reindent +vim.keymap.set('v', 'J', ":m '>+1gv=gv", { silent = true, desc = 'Move selection down' }) +vim.keymap.set('v', 'K', ":m '<-2gv=gv", { silent = true, desc = 'Move selection up' }) + +-- Indent and keep visual selection +vim.keymap.set('v', '>', '>gv', { desc = 'Indent and reselect' }) +vim.keymap.set('v', '<', 'p', '"_dP', { desc = 'Paste without overwriting register' }) +vim.keymap.set({ 'n', 'v' }, 'D', '"_d', { desc = 'Delete without register' }) +vim.keymap.set({ 'n', 'v' }, 'y', '"+y', { desc = 'Yank to system clipboard' }) +vim.keymap.set('n', 'Y', '"+Y', { desc = 'Yank line to system clipboard' }) + +-- Project-wide word rename vim.keymap.set( 'n', 'rw', ':%s/\\<\\>//gI', - { desc = '[R]eplace Current [W]ord in File Scope' } + { desc = 'Rename word under cursor (file scope)' } ) --- Open vertical split pane - --- File Management Keymaps --- Wezterm multiplexer (not tmux) +------------------------------------------------------------------------------- +-- File / system +------------------------------------------------------------------------------- vim.keymap.set( 'n', '', 'silent !wezterm cli spawn --cwd %:p:h', - { desc = 'Open Wezterm Tab in Current Dir' } + { desc = 'Open WezTerm tab in current dir' } ) -vim.keymap.set('n', 'fx', '!chmod +x %', { desc = 'Set Current File to Executable', silent = true }) +vim.keymap.set('n', 'fx', '!chmod +x %', { silent = true, desc = 'Make current file executable' }) + +------------------------------------------------------------------------------- +-- Telescope +------------------------------------------------------------------------------- +vim.keymap.set('n', '?', builtin.oldfiles, { desc = 'Recent files' }) +vim.keymap.set('n', '', builtin.buffers, { desc = 'Open buffers' }) +vim.keymap.set('n', 'sr', builtin.resume, { desc = 'Search: resume last' }) +vim.keymap.set('n', 'sd', builtin.diagnostics, { desc = 'Search: diagnostics' }) +vim.keymap.set('n', 'sG', builtin.live_grep, { desc = 'Search: live grep' }) +vim.keymap.set('n', 'sg', 'LiveGrepGitRoot', { desc = 'Search: live grep (git root)' }) --- Telescope Keymaps --- See `:help telescope.builtin` -local builtin = require('telescope.builtin') -vim.keymap.set('n', '?', builtin.oldfiles, { desc = '[?] Find recently opened files' }) -vim.keymap.set('n', '', builtin.buffers, { desc = '[ ] Find existing buffers' }) vim.keymap.set('n', '/', function() builtin.current_buffer_fuzzy_find(require('telescope.themes').get_dropdown({ winblend = 10, previewer = false })) -end, { desc = '[/] Fuzzily search in current buffer' }) -vim.keymap.set('n', 'gf', builtin.git_files, { desc = 'Search [G]it [F]iles' }) -vim.keymap.set('n', 'gb', builtin.git_branches, { desc = 'Search [G]it [B]ranches' }) -vim.keymap.set('n', 'gc', builtin.git_commits, { desc = 'Search [G]it [C]ommits' }) -vim.keymap.set('n', 'sf', builtin.find_files, { desc = '[S]earch [F]iles' }) +end, { desc = 'Search: fuzzy current buffer' }) + +vim.keymap.set('n', 'sf', builtin.find_files, { desc = 'Search: files' }) +vim.keymap.set('n', 'gf', builtin.git_files, { desc = 'Search: git files' }) +vim.keymap.set('n', 'gb', builtin.git_branches, { desc = 'Search: git branches' }) +vim.keymap.set('n', 'gc', builtin.git_commits, { desc = 'Search: git commits' }) vim.keymap.set({ 'n', 'v' }, 'sh', function() - local query = utils.get_search_query() - builtin.help_tags({ search = query, initial_mode = 'insert', default_text = query }) -end, { desc = '[S]earch [H]elp' }) -vim.keymap.set({ 'n', 'v' }, 'sw', function() - local query = utils.get_search_query() - builtin.grep_string({ search = query, initial_mode = 'insert', default_text = query }) -end, { desc = '[S]earch current [W]ord' }) -vim.keymap.set('n', 'sp', function() - builtin.grep_string({ search = vim.fn.input('Grep Search > ') }) -end, { desc = '[S]earch [P]roject' }) -vim.keymap.set('n', 'sG', builtin.live_grep, { desc = '[S]earch by [G]rep' }) -vim.keymap.set('n', 'sg', ':LiveGrepGitRoot', { desc = '[S]earch by [G]rep on Git Root' }) -vim.keymap.set('n', 'sd', builtin.diagnostics, { desc = '[S]earch [D]iagnostics' }) -vim.keymap.set('n', 'sr', builtin.resume, { desc = '[S]earch [R]esume' }) + local q = utils.get_search_query() + builtin.help_tags({ default_text = q, initial_mode = 'insert' }) +end, { desc = 'Search: help tags' }) --- Telescope config files +vim.keymap.set({ 'n', 'v' }, 'sw', function() + local q = utils.get_search_query() + builtin.grep_string({ default_text = q, initial_mode = 'insert' }) +end, { desc = 'Search: word under cursor' }) + +vim.keymap.set('n', 'sp', function() + builtin.grep_string({ search = vim.fn.input('Grep > ') }) +end, { desc = 'Search: grep prompt' }) + +-- Config / plugin file pickers vim.keymap.set('n', 'fc', function() builtin.find_files({ cwd = vim.fn.stdpath('config'), @@ -111,10 +141,8 @@ vim.keymap.set('n', 'fc', function() 'init.lua', }, }) -end, { desc = '[F]ind [C]onfig Files' }) +end, { desc = 'Find: config files' }) vim.keymap.set('n', 'fp', function() - builtin.find_files({ - cwd = vim.fs.joinpath(vim.fn.stdpath('data'), 'lazy'), - }) -end, { desc = '[F]ind [P]lugin Files' }) + builtin.find_files({ cwd = vim.fs.joinpath(vim.fn.stdpath('data'), 'lazy') }) +end, { desc = 'Find: plugin files' })