Lua API
NeoVim’s vim global is the entry point to everything. It’s a Lua table that lazily loads submodules, wraps the C API, and provides high-level abstractions for configuration, diagnostics, LSP, tree-sitter, and more.
The vim.* Namespace
The vim global is assembled in runtime/lua/vim/_core/editor.lua. Most submodules are lazy-loaded on first access to keep startup fast:
-- These load immediately (always needed)
vim.inspect -- Pretty-print Lua values
vim.version -- Version comparison utilities
vim.fs -- Filesystem utilities
vim.glob -- Glob pattern matching
vim.iter -- Iterator combinators (like Rust's Iterator trait)
vim.re -- LPeg regex-like patterns
vim.lpeg -- PEG parsing library
-- These lazy-load on first access
vim.treesitter -- Tree-sitter parsing and highlighting
vim.filetype -- Filetype detection
vim.lsp -- Language Server Protocol client
vim.diagnostic -- Diagnostic framework (errors, warnings, hints)
vim.keymap -- Keymap management
vim.ui -- UI abstractions (select, input)
vim.health -- Health check framework
vim.snippet -- Snippet expansion
vim.loader -- Bytecode caching module loader
vim.pack -- Built-in package manager (0.12+)
vim.net -- HTTP requests (0.12+)
vim.pos -- Position utilities (0.12+)
vim.range -- Range utilities (0.12+)
Options
Four ways to access options, each with different scope:
-- Global options
vim.o.number = true -- Generic: respects scope rules
vim.go.background = "dark" -- Explicitly global
-- Buffer-local options
vim.bo.filetype = "lua" -- Current buffer
vim.bo[bufnr].filetype = "lua" -- Specific buffer
-- Window-local options
vim.wo.wrap = false -- Current window
vim.wo[winnr].wrap = false -- Specific window
-- vim.opt: richer API for list/map/set options
vim.opt.wildignore:append("*.o")
vim.opt.listchars = { tab = ">> ", trail = "-" }
print(vim.opt.wildignore:get()) -- Returns a Lua table
Gotcha:
vim.oandvim.optlook similar but behave differently for list options.vim.o.listchars = "tab:>> "sets a string.vim.opt.listchars = { tab = ">> " }accepts a table. Usevim.optfor list/map options,vim.ofor simple values.
Variables
Access Vim variables through scoped tables:
vim.g.mapleader = " " -- g: global variables
vim.b.my_var = true -- b: buffer-local
vim.w.my_var = true -- w: window-local
vim.t.my_var = true -- t: tab-local
vim.v.count -- v: Vim special variables (read-only mostly)
vim.env.PATH -- Environment variables
The C API Bridge
Every vim.api.nvim_* function maps directly to a C function in src/nvim/api/. The call goes through the same dispatch mechanism as an RPC call, but without serialization overhead:
-- Buffer operations
vim.api.nvim_buf_get_lines(0, 0, -1, false) -- Get all lines
vim.api.nvim_buf_set_lines(0, 0, 0, false, {"first line"})
-- Window operations
vim.api.nvim_win_get_cursor(0) -- {row, col} (1-indexed row)
vim.api.nvim_win_set_cursor(0, {1, 0})
-- Create autocommands
vim.api.nvim_create_autocmd("BufWritePre", {
pattern = "*.lua",
callback = function() print("saving lua file") end,
})
-- Create user commands
vim.api.nvim_create_user_command("Greet", function(opts)
print("Hello, " .. opts.args)
end, { nargs = 1 })
Tip: Prefer the high-level wrappers when they exist. Use
vim.keymap.set()instead ofvim.api.nvim_set_keymap(). Usevim.api.nvim_create_autocmd()directly though - there’s no wrapper for it.
Lazy Loading with vim._defer_require
Performance-critical modules use deferred loading. When you access vim.lsp.buf, it doesn’t load lsp/buf.lua until that first access:
-- From runtime/lua/vim/lsp.lua
vim._defer_require('vim.lsp', {
buf = ...,
client = ...,
completion = ...,
diagnostic = ...,
-- etc.
})
This pattern is why NeoVim starts fast despite having a massive Lua API surface. Modules load only when touched.
vim.loader - Bytecode Caching
vim.loader replaces Lua’s default require with a version that caches bytecode. It’s enabled by default since 0.9:
-- Explicitly enable (usually not needed - it's the default)
vim.loader.enable()
Under the hood, it:
- Intercepts
require()calls - Checks if a cached
.luacbytecode file exists and is fresh - If yes, loads bytecode directly (faster than parsing)
- If no, compiles the source and caches the bytecode
Tip: This means your first NeoVim startup after a plugin update is slower (recompiling bytecode), but subsequent starts are faster.
vim.iter - Iterator Combinators
Added in 0.9, vim.iter brings functional iteration patterns:
-- Filter and map over a table
local names = vim.iter({ "Alice", "Bob", "Charlie", "Dave" })
:filter(function(name) return #name > 3 end)
:map(string.upper)
:totable()
-- { "ALICE", "CHARLIE", "DAVE" }
-- Works with key-value pairs
local opts = { a = 1, b = 2, c = 3 }
local filtered = vim.iter(pairs(opts))
:filter(function(_, v) return v > 1 end)
:fold({}, function(acc, k, v)
acc[k] = v
return acc
end)
-- { b = 2, c = 3 }
-- Chain with vim.fs for file operations
local lua_files = vim.iter(vim.fs.dir(".", { depth = 3 }))
:filter(function(name) return name:match("%.lua$") end)
:totable()
vim.system - Async Process Execution
Run external commands without blocking the editor:
-- Synchronous (blocks)
local result = vim.system({ "git", "status", "--short" }):wait()
print(result.stdout)
-- Asynchronous (non-blocking)
vim.system({ "make", "build" }, {}, function(result)
vim.schedule(function()
if result.code == 0 then
print("Build succeeded")
else
print("Build failed: " .. result.stderr)
end
end)
end)
Gotcha: Callbacks from
vim.systemrun outside the main loop. You must wrap any API calls invim.schedule()or you’ll get “E5560: Vimscript function must not be called in a fast event” errors.
vim.fs - Filesystem Utilities
-- Find files walking up from current buffer
vim.fs.find(".git", { upward = true, path = vim.fn.expand("%:p:h") })
-- Normalize paths
vim.fs.normalize("~/code/../code/file.lua") -- "/home/user/code/file.lua"
-- Directory iteration
for name, type in vim.fs.dir(".") do
print(name, type) -- "init.lua", "file"
end
-- 0.11+ additions
vim.fs.rm("build/", { recursive = true })
vim.fs.abspath("relative/path")
vim.fs.relpath("/absolute/path", "/base")
Generated Type Definitions
The runtime/lua/vim/_meta/ directory contains generated type annotations:
| File | Size | Contains |
|---|---|---|
api.lua | 114KB | All vim.api.nvim_* function signatures |
options.lua | 347KB | Every option with types and descriptions |
vimfn.lua | 424KB | All vim.fn.* Vimscript function signatures |
These files are generated from C source by src/gen/ scripts. They’re not loaded at runtime - they exist for Lua language servers (lua-language-server) to provide autocomplete and type checking in your config.
Tip: If you use lua-language-server for your NeoVim config, it reads these
_metafiles automatically via theneodevorlazydevplugin to give you full autocomplete onvim.*.