How to Configure Custom Autocommands in Neovim Lua: A Complete Guide
Configure custom autocommands in Neovim Lua by using vim.api.nvim_create_augroup to create namespaces and vim.api.nvim_create_autocmd to bind Lua callbacks to editor events.
Modern Neovim configurations leverage Lua instead of legacy VimScript for better performance and readability. The jdhao/nvim-config repository demonstrates a clean, maintainable pattern for organizing custom autocommands that automate workflows, enhance UI behavior, and optimize file handling.
Core API for Defining Autocommands
Neovim exposes autocommand functionality through the vim.api module. Every custom automation relies on two functions: nvim_create_augroup establishes a container for related commands, and nvim_create_autocmd registers the actual event listener.
In lua/custom-autocmd.lua, the author imports the API locally for brevity:
local api = vim.api
-- Create a namespace for related autocommands
local my_group = api.nvim_create_augroup("MyCustomGroup", { clear = true })
-- Register an event handler
api.nvim_create_autocmd({ "BufWritePre" }, {
pattern = "*",
group = my_group,
callback = function(ev)
-- Logic executes before saving any file
end,
})
Setting clear = true when creating the group ensures that reloading your configuration does not duplicate autocommands.
Setting Up Your Autocommand Module
The jdhao/nvim-config repository centralizes all custom autocommands in lua/custom-autocmd.lua, keeping the entry point uncluttered. This module is loaded from init.lua at line 24:
-- In init.lua
require("custom-autocmd")
This modular approach allows you to add, remove, or debug event handlers without touching plugin specifications or key mappings. Helper utilities reside in lua/utils.lua, providing shared functions for filesystem checks and executable detection that multiple autocommands can reference.
Practical Autocommands for Editing Workflows
Highlight Yanked Text with TextYankPost
Visual feedback improves editing confidence. Lines 20–27 of lua/custom-autocmd.lua implement temporary highlighting using the TextYankPost event, which fires after text is yanked:
local yank_grp = api.nvim_create_augroup("highlight_yank", { clear = true })
api.nvim_create_autocmd({ "TextYankPost" }, {
pattern = "*",
group = yank_grp,
callback = function()
vim.hl.on_yank { higroup = "YankColor", timeout = 300 }
end,
})
The repository also preserves cursor position across yanks by storing coordinates on CursorMoved and restoring them after TextYankPost (lines 29–46).
Automate Directory Creation on Save
Never encounter "directory does not exist" errors again. The BufWritePre event triggers before writing a buffer to disk, allowing the configuration to create missing parent directories automatically (lines 48–55):
api.nvim_create_autocmd({ "BufWritePre" }, {
pattern = "*",
group = api.nvim_create_augroup("auto_create_dir", { clear = true }),
callback = function(ev)
local dir = vim.fn.fnamemodify(ev.file, ":p:h")
if vim.fn.isdirectory(dir) == 0 then
vim.fn.mkdir(dir, "p")
end
end,
})
Here, ev.file contains the target path, and vim.fn.fnamemodify extracts the directory component before vim.fn.mkdir creates it recursively.
Reload Files Changed on Disk
When external tools modify files (e.g., git checkout), Neovim can automatically reload buffers. Lines 58–82 implement a smart check that runs checktime only when not actively typing in the command-line window:
api.nvim_create_augroup("auto_read", { clear = true })
api.nvim_create_autocmd({ "FocusGained", "CursorHold" }, {
pattern = "*",
group = "auto_read",
callback = function()
if vim.fn.getcmdwintype() == "" then
vim.cmd("checktime")
end
end,
})
The getcmdwintype() check prevents disruptive prompts while entering Ex commands.
Window and UI Management Autocommands
Resize Splits Automatically
Maintain split proportions when the terminal window changes size using VimResized (lines 85–90):
api.nvim_create_autocmd({ "VimResized" }, {
pattern = "*",
group = api.nvim_create_augroup("win_resize", { clear = true }),
command = "wincmd =",
})
This executes the Ex command wincmd = to equalize split dimensions.
Dynamic Line Number Behavior
Toggle between relative and absolute line numbers based on buffer focus and edit mode. Lines 143–163 define a group that enables relative numbers on BufEnter and FocusGained, but disables them on InsertEnter:
local num_grp = api.nvim_create_augroup("numbertoggle", { clear = true })
api.nvim_create_autocmd({ "BufEnter", "FocusGained", "InsertLeave" }, {
pattern = "*",
group = num_grp,
callback = function()
vim.opt.relativenumber = true
end,
})
api.nvim_create_autocmd({ "BufLeave", "FocusLost", "InsertEnter" }, {
pattern = "*",
group = num_grp,
callback = function()
vim.opt.relativenumber = false
end,
})
Terminal and Command-Line Customizations
Configure Terminal Buffers
Terminal buffers require distinct settings. The TermOpen event (lines 130–141) disables line numbers and starts insert mode automatically:
api.nvim_create_autocmd({ "TermOpen" }, {
pattern = "*",
group = api.nvim_create_augroup("term_settings", { clear = true }),
callback = function()
vim.opt.number = false
vim.opt.relativenumber = false
vim.cmd("startinsert")
end,
})
Disable Smartcase in Command Mode
Prevent case-insensitive search from interfering while typing commands. Lines 112–128 toggle the smartcase option when entering and leaving the command line:
api.nvim_create_autocmd({ "CmdLineEnter" }, {
pattern = "*",
group = api.nvim_create_augroup("cmdline_config", { clear = true }),
callback = function()
vim.opt.smartcase = false
end,
})
api.nvim_create_autocmd({ "CmdLineLeave" }, {
pattern = "*",
group = "cmdline_config",
callback = function()
vim.opt.smartcase = true
end,
})
Performance Optimizations
Handle Large Files Efficiently
Editing files larger than 0.5 MiB can slow Neovim significantly. Lines 332–356 detect large files on BufReadPre and disable expensive features:
api.nvim_create_autocmd({ "BufReadPre" }, {
pattern = "*",
group = api.nvim_create_augroup("large_file", { clear = true }),
callback = function(ev)
local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(ev.buf))
if ok and stats and stats.size > 524288 then
vim.opt.eventignore:append("FileType")
vim.opt.number = false
vim.opt.swapfile = false
-- Additional optimizations...
end
end,
})
This uses vim.loop.fs_stat to check file size before applying performance tweaks.
Auto-Format on Save with Error Handling
Lines 358–392 demonstrate running external formatters in check mode after BufWritePost, parsing output and notifying only on failure. This pattern uses a filetype map to determine which executable to invoke, keeping the configuration modular.
Summary
- Use augroups: Always create a dedicated
augroupwithclear = trueto organize related autocommands and prevent duplication on config reload. - Prefer Lua callbacks: Write
callback = function() ... endinstead of rawcommandstrings for better performance and access tovim.*APIs. - Centralize configuration: Follow the jdhao/nvim-config pattern by placing all autocommands in
lua/custom-autocmd.luaand requiring it frominit.lua(line 24). - Leverage event data: Use the
evparameter in callbacks to access buffer numbers, file paths, and other context without global state. - Optimize conditionally: Check
vim.fnstates (likegetcmdwintype()) or file stats before executing heavy operations.
Frequently Asked Questions
How do I prevent duplicate autocommands when reloading my config?
Pass { clear = true } as the second argument to vim.api.nvim_create_augroup. This clears existing autocommands in that group before adding new ones, ensuring clean state after :source or config reloads.
What is the difference between callback and command in nvim_create_autocmd?
callback accepts a Lua function that executes in a fresh context with access to the ev table containing event details, while command accepts a raw Vim Ex command string. Use callback for complex logic and command for simple one-liners like "wincmd =".
How can I make autocommands trigger only for specific filetypes?
Set the pattern option to a filetype pattern such as *.lua or use the group parameter with conditional logic inside the callback checking vim.bo.filetype. For multiple specific filetypes, use an array: pattern = { "*.py", "*.lua" }.
Where should I place utility functions used by multiple autocommands?
Create a separate module like lua/utils.lua to host shared helpers such as may_create_dir or inside_git_repo, then require it at the top of lua/custom-autocmd.lua. This keeps your autocommand definitions readable and your logic reusable across the configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →