Skip to main content

Init.lua Structure

The init.lua file is the entry point for your Neovim configuration. A well-organized structure makes your config maintainable, readable, and portable.

Learning Focus

Build the config structure first (empty modules), then fill each file. Never put everything in one file.

~/.config/nvim/
├── init.lua ← entry point, loads everything
└── lua/
├── config/
│ ├── options.lua ← vim.opt settings
│ ├── keymaps.lua ← vim.keymap.set
│ └── autocmds.lua ← autocommands
└── plugins/
├── init.lua ← lazy.nvim setup + plugin list
├── lsp.lua ← LSP configuration
├── telescope.lua ← Telescope config
├── completion.lua ← nvim-cmp config
├── treesitter.lua ← treesitter config
├── ui.lua ← theme, statusline, etc.
└── git.lua ← git plugins

The Entry Point: init.lua

~/.config/nvim/init.lua
-- Load core config modules
require("config.options")
require("config.keymaps")
require("config.autocmds")

-- Bootstrap and load plugins
require("plugins")

That's it — the entry point just loads modules. All the substance is in the modules.

How Lua Modules Work in Neovim

require("config.options") loads ~/.config/nvim/lua/config/options.lua.

The path mapping is:

require("config.options")    → lua/config/options.lua
require("plugins.lsp") → lua/plugins/lsp.lua
require("utils.helpers") → lua/utils/helpers.lua

Lua uses the package.path and Neovim adds lua/ to the search path automatically.

Creating the Directory Structure

mkdir -p ~/.config/nvim/lua/config
mkdir -p ~/.config/nvim/lua/plugins
touch ~/.config/nvim/init.lua
touch ~/.config/nvim/lua/config/{options,keymaps,autocmds}.lua
touch ~/.config/nvim/lua/plugins/init.lua

The Options Module

~/.config/nvim/lua/config/options.lua
local opt = vim.opt

-- Line numbers
opt.number = true -- show line numbers
opt.relativenumber = true -- show relative line numbers

-- Indentation
opt.tabstop = 2 -- tab width
opt.shiftwidth = 2 -- indent width
opt.softtabstop = 2
opt.expandtab = true -- spaces instead of tabs
opt.autoindent = true
opt.smartindent = true

-- Search
opt.ignorecase = true
opt.smartcase = true -- case-sensitive if uppercase
opt.incsearch = true
opt.hlsearch = true

-- Appearance
opt.termguicolors = true -- true color support
opt.signcolumn = "yes" -- always show sign column
opt.cursorline = true -- highlight current line
opt.wrap = false -- no line wrapping
opt.scrolloff = 8 -- keep 8 lines above/below cursor
opt.sidescrolloff = 8

-- Files
opt.encoding = "utf-8"
opt.fileencoding = "utf-8"
opt.undofile = true -- persistent undo
opt.swapfile = false
opt.backup = false
opt.updatetime = 300 -- faster CursorHold events

-- Performance
opt.timeoutlen = 300 -- key sequence timeout (ms)
opt.ttimeoutlen = 0

-- Completion
opt.completeopt = { "menu", "menuone", "noselect" }
opt.pumheight = 10 -- max popup menu items

-- Split behavior
opt.splitright = true -- vertical splits go right
opt.splitbelow = true -- horizontal splits go below

-- Clipboard
opt.clipboard = "unnamedplus" -- sync with system clipboard

The Keymaps Module

~/.config/nvim/lua/config/keymaps.lua
local map = vim.keymap.set

-- Set leader key (do this before plugins load)
vim.g.mapleader = " " -- space as leader
vim.g.maplocalleader = "\\" -- backslash as local leader

-- Better window navigation
map("n", "<C-h>", "<C-w>h", { desc = "Move to left window" })
map("n", "<C-l>", "<C-w>l", { desc = "Move to right window" })
map("n", "<C-j>", "<C-w>j", { desc = "Move to bottom window" })
map("n", "<C-k>", "<C-w>k", { desc = "Move to top window" })

-- Resize windows
map("n", "<C-Up>", ":resize -2<CR>", { desc = "Resize window up" })
map("n", "<C-Down>", ":resize +2<CR>", { desc = "Resize window down" })
map("n", "<C-Left>", ":vertical resize -2<CR>", { desc = "Resize window left" })
map("n", "<C-Right>", ":vertical resize +2<CR>", { desc = "Resize window right" })

-- Buffer navigation
map("n", "<S-l>", ":bnext<CR>", { desc = "Next buffer" })
map("n", "<S-h>", ":bprev<CR>", { desc = "Prev buffer" })
map("n", "<leader>bd", ":bdelete<CR>", { desc = "Delete buffer" })

-- Stay in indent mode (keep selection after indent)
map("v", "<", "<gv", { desc = "Indent left" })
map("v", ">", ">gv", { desc = "Indent right" })

-- Move lines up/down
map("n", "<A-j>", ":m .+1<CR>==", { desc = "Move line down" })
map("n", "<A-k>", ":m .-2<CR>==", { desc = "Move line up" })
map("v", "<A-j>", ":m '>+1<CR>gv=gv", { desc = "Move selection down" })
map("v", "<A-k>", ":m '<-2<CR>gv=gv", { desc = "Move selection up" })

-- Clear search highlight
map("n", "<Esc>", ":nohlsearch<CR>", { desc = "Clear highlights" })

-- Better paste (don't overwrite clipboard when pasting over selection)
map("v", "p", '"_dP', { desc = "Paste without yank" })

-- Save
map("n", "<leader>w", ":w<CR>", { desc = "Save file" })
map("n", "<leader>q", ":q<CR>", { desc = "Quit" })
map("n", "<leader>Q", ":qa!<CR>", { desc = "Force quit all" })

-- Open config
map("n", "<leader>ec", ":e ~/.config/nvim/init.lua<CR>", { desc = "Edit config" })

The Autocmds Module

~/.config/nvim/lua/config/autocmds.lua
local autocmd = vim.api.nvim_create_autocmd
local augroup = vim.api.nvim_create_augroup

-- Highlight on yank
autocmd("TextYankPost", {
group = augroup("highlight_yank", { clear = true }),
callback = function()
vim.highlight.on_yank({ higroup = "IncSearch", timeout = 200 })
end,
})

-- Remove trailing whitespace on save
autocmd("BufWritePre", {
group = augroup("trim_whitespace", { clear = true }),
pattern = "*",
callback = function()
local pos = vim.api.nvim_win_get_cursor(0)
vim.cmd([[%s/\s\+$//e]])
vim.api.nvim_win_set_cursor(0, pos)
end,
})

-- Return to last edit position
autocmd("BufReadPost", {
group = augroup("last_position", { clear = true }),
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
vim.api.nvim_win_set_cursor(0, mark)
end
end,
})

-- Resize splits on window resize
autocmd("VimResized", {
group = augroup("equalize_splits", { clear = true }),
callback = function()
vim.cmd("tabdo wincmd =")
end,
})

-- Auto-format on save (if formatter configured)
autocmd("BufWritePre", {
group = augroup("auto_format", { clear = true }),
pattern = { "*.lua", "*.js", "*.ts", "*.py" },
callback = function()
vim.lsp.buf.format({ async = false })
end,
})

Verifying Your Config Loads

nvim --headless -c "lua print(vim.o.number)" -c "quit"
# Should print: true

# Check for errors on startup
nvim --startuptime /tmp/nvim-startup.log
cat /tmp/nvim-startup.log

What's Next