local function augroup(name) return vim.api.nvim_create_augroup('nvim_' .. name, { clear = true }) end 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() -- 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 yanked text autocmd('TextYankPost', { group = augroup('highlight_yank'), callback = function() vim.hl.on_yank() end, desc = 'Highlight yanked text', }) 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 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 root = get_project_root() if root then vim.opt_local.path = { root .. '/**' } end end, desc = 'Anchor path to project root for gf/:find', }) -- Restore last cursor position when reopening a file autocmd('BufReadPost', { group = augroup('restore_cursor'), callback = function() 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 = 'Restore last cursor position on open', }) -- 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() 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 = 'Auto-update plugins on startup if updates are available', })