Lua for Neovim: The Neovim Lua API — vim.api, vim.opt, vim.keymap.set & Autocommands | Episode 31
Video: Lua for Neovim: The Neovim Lua API — vim.api, vim.opt, vim.keymap.set & Autocommands | Episode 31 by Taught by Celeste AI - AI Coding Coach
The Neovim Lua API: vim.api, vim.opt, vim.keymap.set, Autocommands
vim.api.*for low-level operations.vim.opt.*for options.vim.keymap.setfor keymaps.vim.api.nvim_create_user_commandandnvim_create_autocmdfor commands and autocmds. The bridge from Lua scripts to Neovim configuration.
For 30 episodes we've used Neovim as a Lua editor. Today we use Lua to configure Neovim. The vim global is the gateway — every customisation goes through it.
vim.opt: Neovim options
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.cursorline = true
vim.opt.X is the modern Lua way to set option X. Equivalent vimscript:
set number
set relativenumber
set cursorline
vim.opt returns proxy objects with operator overloading (using __newindex/__index from episode 29 — yes, the Neovim API is metatable-based). Some options accept lists, sets, or maps:
vim.opt.shortmess:append("I") -- add "I" to shortmess
vim.opt.listchars = { tab = "→ ", space = "·" }
For raw scalar access (no list/set magic), use vim.o:
vim.o.tabstop = 2
vim.opt is for "Lua-friendly" configuration; vim.o is for raw values.
vim.api: low-level functions
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
print("This buffer has " .. #lines .. " lines")
vim.api.nvim_* functions are auto-generated bindings for Neovim's C-level API. Naming convention: nvim_<scope>_<action> — buffer, window, command, etc.
Common buffer functions:
vim.api.nvim_buf_get_lines(buf, start, end, strict_indexing)— read lines.vim.api.nvim_buf_set_lines(buf, start, end, strict, replacement)— write lines.vim.api.nvim_buf_get_name(buf)— file path.vim.api.nvim_get_current_line()— line under cursor.
buf is a buffer handle; 0 means "current buffer."
vim.fn: vimscript function bridge
local filename = vim.fn.expand("%")
print("Current file: " .. filename)
vim.fn.X(...) calls the vimscript function X(...). Useful for vimscript functions without a vim.api equivalent — expand, getline, system, globpath, etc.
Slightly slower than vim.api calls (needs to go through the vimscript layer), but covers everything the old API does.
Creating a user command
vim.api.nvim_create_user_command("Hello", function(opts)
local name = opts.args
if name == "" then
name = "World"
end
vim.notify("Hello, " .. name .. "!")
end, { nargs = "?" })
After running this, type :Hello Alice and you get a notification "Hello, Alice!". Type :Hello (no arg) → "Hello, World!".
nvim_create_user_command(name, callback, opts):
name— the command (must start with uppercase letter).callback— function called when command runs. Receivesoptstable with.args,.fargs,.bang, etc.opts.nargs—0,1,*,?,+. How many arguments expected.
vim.notify(msg, level) shows a notification — replaces print for user-facing messages.
Setting a keymap
vim.keymap.set("n", "<leader>h", function()
vim.notify("Hello from a keymap!")
end, { desc = "Say hello" })
vim.keymap.set(mode, lhs, rhs, opts):
mode—"n"normal,"i"insert,"v"visual,"x"visual+select,"t"terminal. Or a list{"n", "v"}.lhs— the keys to press ("<leader>h","jk","<C-s>").rhs— a function (Lua) or string (vimscript). Functions are recommended.opts.desc— description, shown in:maplistings.opts.silent— suppress command-line echo. Defaultfalse.opts.noremap— don't recursively remap. Defaulttrue(the safe choice).
Modern config uses vim.keymap.set for everything; older code uses vim.api.nvim_set_keymap (more verbose, no Lua function support).
Autocommands
vim.api.nvim_create_autocmd("BufWritePost", {
pattern = "*.lua",
callback = function()
vim.notify("Lua file saved!")
end,
})
nvim_create_autocmd(events, opts):
events—"BufWritePost","BufRead","InsertLeave", etc. Or a list.opts.pattern— file pattern ("*.lua","*.md").opts.callback— function to run when event fires.opts.command— vimscript command (alternative to callback).opts.group— autocmd group (for cleanup).
After running this, save any .lua file and you'll see the notification.
For grouped autocmds (clean cleanup):
local group = vim.api.nvim_create_augroup("MyConfig", { clear = true })
vim.api.nvim_create_autocmd("BufWritePost", {
group = group,
pattern = "*.lua",
callback = function() ... end,
})
augroup lets you re-source your config without duplicating autocmds.
vim.cmd: run vimscript
vim.cmd("colorscheme gruvbox")
vim.cmd([[
set background=dark
set termguicolors
]])
When there's no Lua API for what you need, vim.cmd(string) runs vimscript. The [[ ... ]] long string lets you embed multi-line vimscript naturally.
The leader key
vim.g.mapleader = " " -- space as <leader>
vim.g.maplocalleader = ","
vim.g.X is the global vimscript variable equivalent. mapleader must be set before any keymap that uses <leader>.
Integrating with init.lua
Drop these snippets in ~/.config/nvim/init.lua:
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true
vim.g.mapleader = " "
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save" })
vim.keymap.set("n", "<leader>q", ":q<CR>", { desc = "Quit" })
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*",
callback = function()
-- Strip trailing whitespace
local save = vim.fn.winsaveview()
vim.cmd([[silent! %s/\s\+$//e]])
vim.fn.winrestview(save)
end,
})
A small but real Neovim configuration: tabs as 2 spaces, line numbers, leader for save/quit, strip trailing whitespace on save.
Sourcing a Lua file
To run the snippet you're working on:
:luafile %
Same idea as :!lua % from episode 1, but inside Neovim's Lua state. Variables, functions, autocmds defined in the file persist for the session.
For a single line: :lua print("hi").
Common stumbles
Setting vim.opt.X to a list with =. Some options expect comma-separated strings (foldmethod); others expect lists. vim.opt handles both, but check the docs.
vim.opt vs vim.o confusion. Use vim.opt for new code. vim.o is fine for simple scalars.
Forgetting to set leader before keymaps. vim.g.mapleader must be set before any vim.keymap.set with <leader>. Otherwise the leader is \ (default).
Autocmd duplication on re-source. Re-running your config without an augroup adds another autocmd each time. Always group: augroup ... { clear = true }.
Running blocking system calls in callbacks. Freezes the editor. Use vim.fn.jobstart for async.
What's next
Episode 32: writing a Neovim plugin. Take everything we've learned — modules, classes, the API — and ship a plugin: word counter with commands. Plugin structure, the setup pattern, distributing via lazy.nvim or packer.
Recap
vim is the global gateway. vim.opt.X = value for options. vim.api.nvim_* for low-level operations. vim.fn.X(...) for vimscript functions. vim.keymap.set(mode, lhs, rhs, opts) for keymaps. vim.api.nvim_create_user_command(name, fn, opts) for :Commands. vim.api.nvim_create_autocmd(events, opts) for autocommands. vim.g.mapleader for the leader key. vim.cmd(...) to run raw vimscript. :luafile % to source the current Lua file.
Next episode: writing a Neovim plugin.