Open-Source Wikis

/

Neovim

/

Neovim

/

Architecture

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| EL

The nvim binary boots in one of two modes:

  1. Embedded server — the editor proper. It loads the runtime, opens buffers, runs autocmds, handles input, and serves API calls. Started by nvim --embed, by nvim --headless, by another nvim instance, or by a UI.
  2. TUI client — a thin process whose only job is to draw to the terminal. It is started automatically when you run plain nvim in 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_execute in src/nvim/normal.c.
  • Insert mode — insert_check / insert_execute in src/nvim/edit.c.
  • Command-line mode — command_line_check / command_line_execute in src/nvim/ex_getln.c.
  • Terminal mode — terminal_execute in src/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 terminal

A 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 in update_screen() (src/nvim/drawscreen.c), which calls win_update() per window and win_line() per visible line.
  • UI events are buffered. src/nvim/ui.c collects grid_line, grid_cursor_goto, mode_change, etc., and flushes them to all attached UIs via src/nvim/api/ui.c. The same flush goes to the TUI, to a remote GUI, and to any vim.ui_attach Lua 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 parsingtree-sitter itself 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.

Architecture – Neovim wiki | Factory