Welcome back. In the previous lesson, you installed Neovim on native Windows and established a diagnostic baseline: stdpath('config') identifies your configuration directory, $VIMRUNTIME identifies Neovim’s bundled runtime, :scriptnames shows what loaded, and :checkhealth reports environment issues in context.
This lesson turns that baseline into a small, inspectable configuration. You will create one init.lua containing an editor option, a Normal-mode keymap, and an autocommand. More importantly, you will verify the distinction between configuring something and triggering it—an essential habit before adopting Kickstart or LazyVim.
When init.lua runs
Neovim’s user configuration is ordinary code executed during startup. It is not a hidden preferences database: your settings are source-controlled text, and Neovim can tell you which file supplied a setting.
Read the official Neovim help page to confirm the startup position of user configuration and the default Windows configuration location.
In the Initialization section, read the startup order up to the user-config loading step. Then continue with the paragraphs beginning “A file containing initialization commands...” through the configuration location. Focus on two facts: Neovim uses either init.lua or init.vim, not both, and on standard Windows installations init.lua lives beneath the directory reported by stdpath('config').
At startup, your init.lua will set options, define mappings, and register autocommands. An autocommand’s callback does not necessarily run during startup; it waits until its configured editor event occurs.
The following concise reference is worth reading now. It covers the modern Lua APIs you will use directly, rather than older Vimscript-style configuration.
Read the relevant sections of Neovim’s official Lua guide. This is the reference to return to when a future LazyVim override needs to become a deliberate Lua setting.
Read Using Lua files on startup, especially the startup-file rule. In Options, focus on the vim.opt guidance. In Mappings and Creating mappings, read the mapping contract and its examples. Finally, in Autocommands and Creating autocommands, read the opening explanation beginning the event model, then the descriptions of pattern, command, callback, and desc.
Three APIs form the practical core of this lesson:
| Need | Lua API | Meaning |
|---|---|---|
| Change editor behavior | vim.opt | Set a Neovim option |
| Bind a keystroke | vim.keymap.set() | Register a mapping for one or more modes |
| React to an editor event | vim.api.nvim_create_autocmd() | Register work to run later when an event occurs |
Create the smallest useful configuration
First, launch Neovim normally:
nvim
Inside Neovim, confirm the location that applies to your current session:
:echo stdpath('config')
On the conventional Windows installation established last lesson, this should be under AppData\Local\nvim. Create that directory if needed and open init.lua with these two Ex commands:
:call mkdir(stdpath('config'), 'p')
:execute 'edit ' .. fnameescape(stdpath('config') .. '/init.lua')
The first command creates the directory and any missing parent directories. The second opens the file at the path Neovim itself resolved. You do not need to memorize these bootstrap commands; once init.lua exists, you will normally open it with :edit $MYVIMRC.
Enter Insert mode with i, paste the following file, then save with Esc, :w, Enter.
-- init.lua: a deliberately small, inspectable configuration
vim.g.mapleader = " "
vim.opt.splitbelow = true
vim.keymap.set("n", "<leader>w", "<cmd>write<cr>", {
desc = "Write current buffer",
})
local practice_group = vim.api.nvim_create_augroup("PracticeConfig", {
clear = true,
})
vim.api.nvim_create_autocmd("BufWritePost", {
group = practice_group,
pattern = "*.lua",
callback = function(event)
vim.notify("Wrote " .. event.match)
end,
desc = "Confirm Lua file writes",
})
Because this file did not exist when Neovim started, saving it does not automatically load it into the current session. Source the file explicitly:
:source %
Here % means “the current file.” Sourcing is useful during configuration work: it executes the saved init.lua immediately, without quitting and reopening Neovim.
Read the configuration as three contracts
The first non-comment line sets the leader key:
vim.g.mapleader = " "
mapleader is a global variable, not an editor option. It determines what <leader> means in mappings defined afterward. Here it is set to Space, a convention you will also encounter in LazyVim.
The option line is direct:
vim.opt.splitbelow = true
This sets Neovim’s splitbelow option. A horizontal split created after this setting takes effect opens below the current window. vim.opt is the idiomatic Lua interface for options, especially in configuration code.
The keymap has four meaningful parts:
vim.keymap.set("n", "<leader>w", "<cmd>write<cr>", {
desc = "Write current buffer",
})
"n"means the mapping exists in Normal mode only."<leader>w"expands to Space followed byw, becausemapleaderwas defined first."<cmd>write<cr>"executes the Ex command:writeand presses Enter programmatically.descgives the mapping a human-readable purpose when you inspect it later or when a keymap-discovery plugin displays it.
This is intentionally a small mapping rather than an attempt to recreate Visual Studio shortcuts. Neovim’s Normal mode already has a large, composable editing vocabulary; mappings should supplement it, not obscure it.
Finally, the autocommand uses an event and a pattern:
vim.api.nvim_create_autocmd("BufWritePost", {
group = practice_group,
pattern = "*.lua",
callback = function(event)
vim.notify("Wrote " .. event.match)
end,
desc = "Confirm Lua file writes",
})
BufWritePost occurs after a buffer has been written. The "*.lua" pattern limits the callback to Lua files. When it fires, the callback receives an event table; event.match identifies the filename that matched the pattern.
The group is not decoration. Re-running :source % should be safe while you iterate. clear = true removes previously registered autocommands in PracticeConfig before creating the current one. Without it, every source operation could add another callback, producing duplicate notifications on each Lua save.
Registration is not execution
It helps to distinguish the time at which each part takes effect:
| Construct | Takes effect when init.lua is sourced or loaded | Observable result |
|---|---|---|
vim.opt.splitbelow = true | Immediately | Future horizontal splits open below |
vim.keymap.set(...) | Immediately | Space followed by w works in Normal mode |
nvim_create_autocmd(...) | Immediately, but only as a registration | The callback waits for a matching Lua-buffer write |
vim.notify(...) inside the callback | Only after BufWritePost for *.lua | A notification or message reports the saved Lua file |
That final distinction avoids a common configuration mistake: assuming an autocommand “does nothing” because its callback did not run as soon as you sourced the file. After :source %, your callback is merely waiting for its event.
The terminal-focused example below has the same structure but uses a different event and a command-string action.

Your configuration instead uses a Lua callback, which is generally easier to extend safely once the behavior needs conditionals, buffer information, or OS-aware logic.
Verify what loaded, where it came from, and when it runs
Quit and start Neovim again:
:qa
nvim
A restart is the authoritative test that Neovim can discover and load the configuration as part of normal startup. Run the following checks inside Neovim.
:echo stdpath('config')
:echo $MYVIMRC
:scriptnames
Interpret them as follows:
stdpath('config')is the directory Neovim uses for personal configuration.$MYVIMRCis the specific initialization file Neovim selected and loaded. It should end ininit.lua.:scriptnamesis evidence rather than assumption. Search its output for yourinit.lua; it should appear among the files sourced during startup.
Now inspect the option, mapping, and autocommand individually:
:verbose set splitbelow?
:verbose nmap <Space>w
:verbose autocmd PracticeConfig
The :verbose prefix is especially valuable. In addition to showing the current definition, it should report where it was last set or defined—typically your init.lua and a line number.
A useful diagnostic translation is:
| Question | Command |
|---|---|
| Is the option currently enabled, and who set it? | :verbose set splitbelow? |
| Does this exact Normal-mode keymap exist, and where was it defined? | :verbose nmap <Space>w |
| Has the autocommand been registered, and which group owns it? | :verbose autocmd PracticeConfig |
| What happened when something behaved unexpectedly? | :messages |
Now validate the actual behavior, not just its definition:
-
Run
:new. The new horizontal split should appear below the original window. -
Run
:closeto return to one window. -
Open your configuration with:
:edit $MYVIMRC -
Ensure you are in Normal mode with
Esc, then press Space, followed by w.
The mapping writes init.lua. Since it is a Lua file, that write triggers BufWritePost, and you should see the Wrote ... notification. If the notification flashes by quickly or does not appear as expected, inspect:
:messages
This one action validates the entire chain: the startup configuration loaded, the leader map is active in Normal mode, the mapping executed :write, the matching post-write event occurred, and the autocommand callback ran.
A compact workflow for configuration changes
For this early stage, use a disciplined edit-test loop:
- Edit
init.lua. - Save it with
:write. - Source it with
:source %. - Use the narrowest verification command:
:verbose set ...,:verbose nmap ..., or:verbose autocmd .... - Trigger the behavior deliberately.
- Check
:messagesimmediately if the result is surprising. - Restart Neovim before considering the change complete.
Be cautious with :source %: it is convenient, but it is not transactional. If a syntax error occurs midway through the file, earlier lines may already have run. The autocommand group in this lesson is one small safeguard against accidental duplication during repeated reloads.
For now, keep this file minimal. You are building the ability to trace behavior to a precise source location. In the next module, Kickstart and LazyVim will introduce structured configuration and plugins; these same inspection commands will let you tell whether a behavior originates in Neovim, a distribution default, a plugin, or your own override.
You now have a real Lua-based Neovim configuration with one option, one deliberate keymap, and one event-driven behavior. The key operational distinction is:
- options and mappings change the running editor when the configuration is loaded;
- autocommands register a future reaction and execute only when their event and pattern match.
You also verified both where the configuration lives (stdpath('config'), $MYVIMRC, :scriptnames) and where each setting originated (:verbose inspection commands). Next, you will run Kickstart and LazyVim as isolated configurations and trace a practical feature from a documented default to a personal LazyVim override.
Can't find a good explanation? Sign up and we'll make it for you
Sign up