Create your own
Lesson illustration

Minimal Lua Configuration: Options, Keymaps, and Autocommands

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.

Starting

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.

Lua-guide

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:

NeedLua APIMeaning
Change editor behaviorvim.optSet a Neovim option
Bind a keystrokevim.keymap.set()Register a mapping for one or more modes
React to an editor eventvim.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 by w, because mapleader was defined first.
  • "<cmd>write<cr>" executes the Ex command :write and presses Enter programmatically.
  • desc gives 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:

ConstructTakes effect when init.lua is sourced or loadedObservable result
vim.opt.splitbelow = trueImmediatelyFuture horizontal splits open below
vim.keymap.set(...)ImmediatelySpace followed by w works in Normal mode
nvim_create_autocmd(...)Immediately, but only as a registrationThe callback waits for a matching Lua-buffer write
vim.notify(...) inside the callbackOnly after BufWritePost for *.luaA 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.

Lua code registers a `TermOpen` autocommand for terminal buffers, running `startinsert | set winfixheight`; the referenced `custom_buffer` group would need to be created elsewhere in the configuration.

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.
  • $MYVIMRC is the specific initialization file Neovim selected and loaded. It should end in init.lua.
  • :scriptnames is evidence rather than assumption. Search its output for your init.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:

QuestionCommand
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:

  1. Run :new. The new horizontal split should appear below the original window.

  2. Run :close to return to one window.

  3. Open your configuration with:

    :edit $MYVIMRC
    
  4. 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:

  1. Edit init.lua.
  2. Save it with :write.
  3. Source it with :source %.
  4. Use the narrowest verification command: :verbose set ..., :verbose nmap ..., or :verbose autocmd ....
  5. Trigger the behavior deliberately.
  6. Check :messages immediately if the result is surprising.
  7. 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