Architecture
NeoVim’s architecture splits Vim’s monolith into three layers: a C event loop core, a MsgPack RPC protocol, and Lua runtime scripting. Every UI (terminal, GUI, remote) is a client speaking the same protocol.
The Big Picture
flowchart TD
subgraph Clients
tui[Terminal UI]
gui[GUI Client]
remote[Remote Client]
end
rpc[MsgPack RPC]
subgraph Core["NeoVim Core (C)"]
api[API Layer]
loop[Event Loop]
buffers[Buffer and Window Mgmt]
lua[Lua Runtime]
vimscript[Vimscript Eval]
end
tui --> rpc
gui --> rpc
remote --> rpc
rpc --> api
api --> loop
api --> buffers
api --> lua
api --> vimscript
- Every UI is just a client. The terminal UI is not special from the protocol’s perspective.
- MsgPack RPC is the seam between clients and the core.
- The API layer is the stable entry point into the event loop, buffer state, and scripting runtimes.
Event Loop
The core runs a single-threaded event loop built on libuv. All I/O (terminal input, RPC messages, child processes, file watchers) feeds through this loop.
Source: src/nvim/event/
Key files:
loop.c / loop.h - Main loop structure
multiqueue.c - Priority event queues
defs.h - Event types and structs
rstream.c / wstream.c - Read/write streams
process.c - Child process management
signal.c - Signal handling
socket.c - Unix/TCP socket I/O
The loop processes events from a MultiQueue - a priority-ordered set of event queues. This avoids the threading complexity that made Vim’s async story painful.
Gotcha: NeoVim is single-threaded for editor operations. Lua code runs on the main thread. Long-running Lua blocks the editor. Use
vim.system()orvim.uv(libuv bindings) for async work.
MsgPack RPC
Every NeoVim instance is an RPC server. The API is defined in src/nvim/api/*.c and exposed over MsgPack-RPC. This is how GUIs, plugins, and remote clients communicate.
Source: src/nvim/msgpack_rpc/
Key files:
channel.c - RPC channel management
packer.c - MsgPack serialization
unpacker.c - MsgPack deserialization
server.c - Listen on sockets
The protocol carries three message types:
| Type | Direction | Purpose |
|---|---|---|
| Request | Client -> NeoVim | Call an API function, get a response |
| Response | NeoVim -> Client | Return value or error |
| Notification | Either direction | Fire-and-forget event |
You can connect to any running NeoVim instance:
# Start NeoVim listening on a socket
nvim --listen /tmp/nvim.sock
# From another process, connect and call API
nvim --server /tmp/nvim.sock --remote-send ':echo "hello"<CR>'
Programmatically from Python, Lua, or any language with a MsgPack library:
# pip install pynvim
import pynvim
nvim = pynvim.attach('socket', path='/tmp/nvim.sock')
nvim.command('echo "hello from Python"')
buf = nvim.current.buffer
print(buf[:]) # Read all lines
API Layer
The API (src/nvim/api/) is the single source of truth for all editor operations. It has ~30 files covering:
| File | Scope |
|---|---|
buffer.c | Buffer operations (get/set lines, options, marks) |
window.c | Window layout, cursor, options |
tabpage.c | Tab page management |
vim.c | Global operations (commands, eval, options) |
extmark.c | Virtual text, highlights, decorations |
autocmd.c | Autocommand creation and management |
command.c | User command creation |
options.c | Option get/set |
ui.c | UI protocol events |
API functions are auto-generated into dispatch tables by scripts in src/gen/. When you call vim.api.nvim_buf_set_lines() from Lua, it goes through the same dispatch as a remote RPC call.
The C/Lua Bridge
LuaJIT is embedded in the C core. The bridge lives in src/nvim/lua/ (17 files):
Source: src/nvim/lua/
Key files:
executor.c - Lua state management, pcall wrappers
converter.c - C <-> Lua type conversion
treesitter.c - Tree-sitter bindings for Lua
spell.c - Spell checking bindings
secure.c - Sandboxed execution
The Lua runtime provides the vim.* namespace - a comprehensive API surface that wraps both the C API and higher-level abstractions. See Lua API for details.
Code Generation
NeoVim generates significant code at build time. Lua scripts in src/gen/ produce:
| Generator | Output |
|---|---|
gen_api_dispatch.lua | API function dispatch tables |
gen_eval.lua | Vimscript function bindings |
gen_options.lua | Option definitions and metadata |
gen_lsp.lua | LSP protocol types from spec |
gen_help_html.lua | HTML help documentation |
gen_declarations.lua | Header declarations |
gen_char_blob.lua | Embedded Lua bytecode |
This means the LSP types, API dispatch, and Vimscript bindings are always in sync with the C source.
UI Protocol
The TUI (Terminal UI) at src/nvim/tui/ is just one UI client. It connects to the core the same way an external GUI would, through the UI protocol - a stream of redraw events:
sequenceDiagram
participant Client as UI Client
participant Core as NeoVim Core
Client->>Core: Attach UI over MsgPack RPC
Core-->>Client: grid_resize / grid_line / grid_scroll
Core-->>Client: hl_attr_define / hl_group_set
Core-->>Client: mode_change / mode_info_set
Core-->>Client: cmdline_show / cmdline_pos
Core-->>Client: popupmenu_show / popupmenu_select
Core-->>Client: msg_show / msg_clear
- The core does not render pixels directly for external GUIs.
- It emits redraw events; the client decides how to present them.
External GUIs (Neovide, nvim-qt, firenvim) consume these same events. This is why NeoVim can run headless and be controlled entirely over RPC.
Vim Relationship
NeoVim started as a Vim fork. The relationship today:
| Aspect | Vim | NeoVim |
|---|---|---|
| Architecture | Monolithic, single-process | Event loop + RPC server |
| Scripting | Vimscript (+ Vim9script) | Vimscript + Lua (first-class) |
| UI | Built into the binary | Protocol-based, external GUIs |
| Async | Limited (jobs, timers) | libuv event loop, channels |
| LSP | None built-in | Full client built-in |
| Tree-sitter | None | Built-in parsing and highlighting |
| Defaults | Minimal | Sensible defaults (syntax on, filetype on, etc.) |
NeoVim still processes Vim patches for the shared editing core (buffer management, motions, text objects). The divergence is in everything around that core: UI, scripting, async, and tooling integration.