Configuration
Everything starts with ~/.config/nvim/init.lua. This guide covers the core configuration patterns: options, keymaps, autocommands, and user commands.
Minimal init.lua
-- Set leader key (must be before any keymaps that use <leader>)
vim.g.mapleader = " "
vim.g.maplocalleader = " "
-- Editor options
vim.o.number = true -- Line numbers
vim.o.relativenumber = true -- Relative line numbers
vim.o.signcolumn = "yes" -- Always show sign column
vim.o.clipboard = "unnamedplus" -- System clipboard
vim.o.undofile = true -- Persistent undo
vim.o.ignorecase = true -- Case-insensitive search...
vim.o.smartcase = true -- ...unless uppercase used
vim.o.splitright = true -- Open vertical splits right
vim.o.splitbelow = true -- Open horizontal splits below
vim.o.updatetime = 250 -- Faster CursorHold events
vim.o.cursorline = true -- Highlight current line
-- Indentation
vim.o.tabstop = 2
vim.o.shiftwidth = 2
vim.o.expandtab = true -- Spaces, not tabs
Gotcha:
vim.g.mapleadermust be set BEFORE any keymap that uses<leader>. If you set it after, those keymaps will use the old leader (backslash by default).
Options
NeoVim has three option accessors. Use the right one:
| Accessor | When to use | Example |
|---|---|---|
vim.o | Simple values (string, number, boolean) | vim.o.number = true |
vim.opt | List/map options that need append/remove | vim.opt.wildignore:append("*.o") |
vim.bo / vim.wo | Buffer/window-specific overrides | vim.bo.filetype = "lua" |
-- vim.opt handles list options nicely
vim.opt.completeopt = { "menu", "menuone", "noselect" }
vim.opt.shortmess:append("c") -- Don't show completion messages
vim.opt.path:append("**") -- Search subdirectories
-- Check current value
print(vim.inspect(vim.opt.completeopt:get()))
-- { "menu", "menuone", "noselect" }
Keymaps
Use vim.keymap.set() for all keymaps:
-- Basic pattern: vim.keymap.set(mode, lhs, rhs, opts)
-- Normal mode
vim.keymap.set("n", "<leader>w", "<cmd>write<cr>", { desc = "Save file" })
-- Multiple modes
vim.keymap.set({ "n", "v" }, "<leader>y", '"+y', { desc = "Copy to clipboard" })
-- Lua function as handler
vim.keymap.set("n", "<leader>q", function()
local wins = vim.api.nvim_list_wins()
if #wins > 1 then
vim.cmd.close()
else
vim.cmd.quit()
end
end, { desc = "Smart close" })
-- Buffer-local keymap
vim.keymap.set("n", "K", vim.lsp.buf.hover, { buffer = true, desc = "Hover docs" })
-- Silent and no remap (defaults, but explicit for clarity)
vim.keymap.set("n", "j", "gj", { silent = true, noremap = true })
Common options:
| Option | Default | Purpose |
|---|---|---|
desc | nil | Description (shown in :map, which-key, etc.) |
buffer | nil | Buffer number or true for current |
silent | false | Don’t echo the command |
noremap | true | Don’t remap recursively |
expr | false | RHS is an expression |
remap | false | Allow recursive mapping |
Tip: Always set
descon keymaps. It shows up in:map, telescope keymaps picker, and which-key. Your future self will thank you.
Autocommands
-- Basic autocommand
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*",
callback = function()
-- Remove trailing whitespace
local cursor = vim.api.nvim_win_get_cursor(0)
vim.cmd([[%s/\s\+$//e]])
vim.api.nvim_win_set_cursor(0, cursor)
end,
})
-- Autocommand groups (prevent duplicates on config reload)
local augroup = vim.api.nvim_create_augroup("MyConfig", { clear = true })
vim.api.nvim_create_autocmd("TextYankPost", {
group = augroup,
callback = function()
vim.hl.on_yank({ timeout = 200 }) -- Highlight yanked text briefly
end,
})
vim.api.nvim_create_autocmd("FileType", {
group = augroup,
pattern = { "go", "rust" },
callback = function()
vim.bo.tabstop = 4
vim.bo.shiftwidth = 4
vim.bo.expandtab = false
end,
})
-- Pattern matching
vim.api.nvim_create_autocmd("BufRead", {
group = augroup,
pattern = "*.md",
callback = function()
vim.wo.wrap = true
vim.wo.linebreak = true
end,
})
Gotcha: Always use autocommand groups with
{ clear = true }. Without this, re-sourcing your config creates duplicate autocommands that fire multiple times.
User Commands
-- Simple command
vim.api.nvim_create_user_command("Config", function()
vim.cmd.edit("~/.config/nvim/init.lua")
end, { desc = "Edit NeoVim config" })
-- Command with arguments
vim.api.nvim_create_user_command("Grep", function(opts)
vim.cmd("silent grep! " .. opts.args)
vim.cmd.copen()
end, { nargs = "+", desc = "Grep and open quickfix" })
-- Command with completion
vim.api.nvim_create_user_command("Theme", function(opts)
vim.cmd.colorscheme(opts.args)
end, {
nargs = 1,
complete = "color", -- Built-in colorscheme completion
desc = "Switch colorscheme",
})
File Organization
For larger configs, split into modules:
~/.config/nvim/
init.lua -- Entry point, sources everything
lua/
config/
options.lua -- vim.o.* settings
keymaps.lua -- vim.keymap.set() calls
autocmds.lua -- autocommands
plugins/
lsp.lua -- LSP configuration
treesitter.lua -- Tree-sitter setup
telescope.lua -- Fuzzy finder config
-- init.lua
vim.g.mapleader = " "
require("config.options")
require("config.keymaps")
require("config.autocmds")
-- lua/config/options.lua
vim.o.number = true
vim.o.relativenumber = true
-- ... rest of options
Tip: Files in
lua/are loaded viarequire(). Files inplugin/auto-run on startup. Keep your config inlua/andrequire()it frominit.luafor explicit control over load order.
Diagnostic Display
Configure how diagnostics (errors, warnings) appear:
vim.diagnostic.config({
virtual_text = {
prefix = ">>",
spacing = 2,
},
signs = true,
underline = true,
update_in_insert = false, -- Don't update while typing
severity_sort = true, -- Show errors before warnings
float = {
border = "rounded",
source = true, -- Show diagnostic source
},
})