neovim/neovim
Architecture
Neovim is a single-process editor built around a libuv event loop. The C core owns the buffers, windows, options, and the input state machine inherited from Vim; Lua, Vimscript, and remote RPC clients drive that core through one of two paths: in-process function calls or msgpack-rpc messages. UIs are always remote in the architectural sense — even the built-in TUI runs as a separate process that talks to the editor over a pipe.
Top-level processes
graph LR
subgraph nvim_server["nvim server (headless)"]
EL[Event loop<br/>main_loop]
Core[Editor core<br/>buffers, windows, options]
Lua[LuaJIT<br/>vim.* stdlib]
VimL[Vimscript<br/>eval.c]
API[API dispatch<br/>nvim_*]
end
subgraph nvim_tui["nvim --embed (TUI client)"]
TUI[tui.c<br/>termkey + terminfo]
end
User -->|stdin/keys| TUI
TUI -->|msgpack-rpc<br/>nvim_input| EL
EL -->|redraw events| TUI
TUI -->|escape sequences| Term[Terminal emulator]
Plugins[Lua/VimL plugins] --> Lua
Plugins --> VimL
Lua --> API
VimL --> API
API --> Core
Core --> EL
RemoteUI[GUIs:<br/>Neovide, vimr, ...] -.->|TCP/pipe<br/>msgpack-rpc| EL
RemoteClient[API clients:<br/>pynvim, nvim-go, ...] -.->|msgpack-rpc| ELThe nvim binary boots in one of two modes:
- Embedded server — the editor proper. It loads the runtime, opens buffers, runs autocmds, handles input, and serves API calls. Started by
nvim --embed, bynvim --headless, by anothernviminstance, or by a UI. - TUI client — a thin process whose only job is to draw to the terminal. It is started automatically when you run plain
nvimin a terminal: the same binary forks itself into a server process and a TUI process connected by a pipe (src/nvim/ui_client.c,src/nvim/tui/tui.c).
This separation means the TUI is just one of many possible UIs. The editor doesn't know or care whether the screen is being drawn by the built-in TUI, by a Qt GUI like Neovide, or not drawn at all (--headless).
Event loop
All of the editor's I/O — keystrokes, RPC messages, child process output, timers, file watches — is multiplexed through a single libuv loop. The loop and its queues live in src/nvim/event/loop.c:
typedef struct loop {
uv_loop_t uv;
MultiQueue *events; // deferred (safe) events
MultiQueue *thread_events; // events posted from worker threads
MultiQueue *fast_events; // events that may run during a vgetc()
...
} Loop;The Vim-inherited input loop (vgetc() in src/nvim/getchar.c) only knows about keystrokes. To weave async events into that model, Neovim runs the libuv loop whenever it would otherwise be waiting for a key, and synthesizes a special "event" key (K_EVENT) when something has happened. Most callers see this as transparent: writing vgetc() still gives you the next user character, but in between RPC calls and timer callbacks were processed.
For the canonical narrative on this design, read runtime/doc/dev_arch.txt — that is the file the maintainers point new contributors at.
State machine
Neovim is a pushdown automaton. state_enter() in src/nvim/state.c is the generic loop:
void state_enter(VimState *s) {
for (;;) {
...
int key = safe_vgetc();
if (!s->execute(s, key)) break;
}
}Each mode defines its own VimState with check, execute, and enter callbacks:
- Normal mode —
normal_check/normal_executeinsrc/nvim/normal.c. - Insert mode —
insert_check/insert_executeinsrc/nvim/edit.c. - Command-line mode —
command_line_check/command_line_executeinsrc/nvim/ex_getln.c. - Terminal mode —
terminal_executeinsrc/nvim/terminal.c.
Modes nest. When you press : from normal, the normal loop calls into getcmdline(), which calls state_enter() again with a CommandLineState. When the command line returns, control unwinds back to normal.
Layers from a key press to the screen
sequenceDiagram
participant T as Terminal
participant TUI as TUI client (tui.c)
participant Loop as Server event loop
participant Input as input.c / getchar.c
participant Mode as Normal/Insert/...
participant Buf as Buffer (memline)
participant UI as ui.c
participant API as api/ui.c
T->>TUI: Escape sequence / bytes
TUI->>Loop: nvim_input("hello") via msgpack
Loop->>Input: input_enqueue
Input->>Mode: vgetc returns key
Mode->>Buf: ml_replace / ml_append
Mode->>UI: ui_call_grid_line(...)
UI->>API: dispatch redraw event
API-->>TUI: redraw notification
TUI->>T: Write cells to terminalA few notes on this flow:
- Input is asynchronous.
nvim_input()(src/nvim/api/vim.c) doesn't block on the keypress being processed; it just queues bytes for the input layer. - Drawing is deferred. The mode functions write to buffers and call
redraw_*flags. Actual redraw calls happen at the top of the loop inupdate_screen()(src/nvim/drawscreen.c), which callswin_update()per window andwin_line()per visible line. - UI events are buffered.
src/nvim/ui.ccollectsgrid_line,grid_cursor_goto,mode_change, etc., and flushes them to all attached UIs viasrc/nvim/api/ui.c. The same flush goes to the TUI, to a remote GUI, and to anyvim.ui_attachLua listener.
Generated code
A non-trivial fraction of the C source is generated at build time from Lua data files. The generators live in src/gen/:
| Input | Generator | Output |
|---|---|---|
src/nvim/api/*.c |
src/gen/gen_api_dispatch.lua |
api/private/dispatch_wrappers.generated.h, API metadata |
src/nvim/api/ui_events.in.h |
src/gen/gen_api_ui_events.lua |
Per-event UI wrappers |
src/nvim/eval.lua |
src/gen/gen_eval.lua |
funcs.generated.h, vimfn dispatch |
src/nvim/options.lua |
src/gen/gen_options.lua |
options.generated.h, options_enum.generated.h |
src/nvim/ex_cmds.lua |
src/gen/gen_ex_cmds.lua |
ex_cmds_defs.generated.h |
src/nvim/auevents.lua |
src/gen/gen_events.lua |
Autocmd event tables |
If you change one of those Lua data files you must rebuild — the C tree is not regenerated by git pull.
Major components by directory
| Directory | What lives there |
|---|---|
src/nvim/api/ |
The nvim_* API surface (buffer, window, ui, vim, ...). C functions here are auto-discovered by gen_api_dispatch.lua. |
src/nvim/event/ |
libuv glue: Loop, MultiQueue, RStream/WStream, child processes, signals, timers. |
src/nvim/eval/ |
Vimscript: typval_T, funcs.c, vars.c, userfunc.c, executor.c. |
src/nvim/lua/ |
C↔Lua bridge: executor.c, converter.c (typval ↔ Object ↔ Lua), treesitter.c, stdlib.c. |
src/nvim/msgpack_rpc/ |
RPC framing, packer/unpacker, server, channel multiplexing. |
src/nvim/os/ |
Platform abstractions (fs, env, proc, pty, signals, stdpaths, time). |
src/nvim/tui/ |
Built-in terminal UI: termkey input parser, terminfo, the tui drawing loop. |
src/nvim/vterm/ |
Vendored VTE (terminal-emulator-as-buffer) for :terminal. |
runtime/lua/vim/ |
The vim.* Lua stdlib: lsp, treesitter, diagnostic, pack, iter, fs, version, ... |
runtime/lua/vim/_core/ |
Compiled-in Lua modules (load before VIMRUNTIME is valid). |
runtime/plugin/, runtime/pack/ |
Built-in plugins (man, editorconfig, tutor, ...). |
test/functional/, test/unit/ |
Busted-based test suites. |
cmake.deps/ |
Bundled dependency build (libuv, LuaJIT, libluv, lpeg, libvterm, unibilium, tree-sitter). |
The full taxonomy is in Systems and Features.
Build pipeline
graph TD
deps[cmake.deps/<br/>libuv, LuaJIT, libluv,<br/>lpeg, libvterm, treesitter]
src[src/nvim/<br/>C sources]
lua_data[Lua data files<br/>options.lua, ex_cmds.lua,<br/>eval.lua, auevents.lua]
runtime[runtime/lua/, runtime/doc/]
deps --> cmake[CMake top-level]
src --> cmake
lua_data -->|src/gen/*.lua| generated[*.generated.h]
generated --> cmake
cmake --> nvim[build/bin/nvim]
runtime --> nvim
nvim --> tests[test/functional<br/>test/unit]make at the repo root runs the bundled-deps build (cmake.deps/) and then the main build. make distclean wipes build/ and .deps/. The build uses Ninja when available and ccache/sccache transparently. See Tooling for the full set of make targets.
Threads
The editor is single-threaded by design. The main thread runs the libuv loop, the input parser, the mode state machine, the renderer, and Lua. Worker threads exist for narrow tasks:
- TUI thread — only in the TUI client. Runs the libuv loop that talks to the terminal, posts events back to the main thread (
src/nvim/tui/tui.c). - shada async writer — for async writes of the shada file in some paths.
- Tree-sitter parsing —
tree-sitteritself can be invoked off-thread in some flows.
Lua callbacks always run on the main thread. vim.uv (a binding to libuv) must not be used to schedule work that mutates editor state without going through vim.schedule().
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.