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.o and vim.opt look similar but behave differently for list options. vim.o.listchars = "tab:>> " sets a string. vim.opt.listchars = { tab = ">> " } accepts a table. Use vim.opt for list/map options, vim.o for 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 of vim.api.nvim_set_keymap(). Use vim.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:

  1. Intercepts require() calls
  2. Checks if a cached .luac bytecode file exists and is fresh
  3. If yes, loads bytecode directly (faster than parsing)
  4. 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.system run outside the main loop. You must wrap any API calls in vim.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:

FileSizeContains
api.lua114KBAll vim.api.nvim_* function signatures
options.lua347KBEvery option with types and descriptions
vimfn.lua424KBAll 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 _meta files automatically via the neodev or lazydev plugin to give you full autocomplete on vim.*.