neovim/neovim
Event loop
Purpose
Neovim is an asynchronous editor whose every I/O — keystrokes, RPC traffic, child process output, file watchers, timers — flows through a single libuv-based event loop. The event-loop subsystem in src/nvim/event/ provides the loop itself, the multi-priority queue that routes events to the right consumer, and the read/write stream wrappers that adapt libuv's callbacks to the rest of the editor.
Directory layout
src/nvim/event/
├── defs.h Public types: Event, MultiQueue, Loop, Stream
├── loop.c/.h Top-level Loop struct + loop_poll_events
├── multiqueue.c/.h Multi-queue with parent/child relationships
├── proc.c/.h Child process supervisor (uv_spawn wrapper)
├── libuv_proc.c libuv-specific process glue
├── rstream.c/.h Read stream (file/socket/pipe → callbacks)
├── wstream.c/.h Write stream (callbacks → file/socket/pipe)
├── stream.c/.h Common stream helpers
├── socket.c/.h TCP/Unix socket wrappers
├── signal.c/.h SIGINT/SIGTERM handlers
└── time.c/.h TimersKey abstractions
| Type | File | Description |
|---|---|---|
Loop |
src/nvim/event/defs.h |
Owns a uv_loop_t and three MultiQueues (events, thread_events, fast_events). |
MultiQueue |
src/nvim/event/multiqueue.c |
A queue of Events. Can have a parent; pushing into a child also pushes into the parent. |
Event |
src/nvim/event/defs.h |
A (handler, argv) callback bundle. |
Stream |
src/nvim/event/stream.c |
Common base for read/write streams. |
RStream/WStream |
src/nvim/event/rstream.c, wstream.c |
Buffered I/O with callbacks. |
Proc |
src/nvim/event/proc.c |
A spawned child process plus its stdio streams. |
How it works
The editor has one main_loop declared in src/nvim/main.c:
typedef struct loop {
uv_loop_t uv;
MultiQueue *events;
MultiQueue *thread_events;
MultiQueue *fast_events;
queue_T loop_cbs;
uv_async_t async;
...
} Loop;The three queues exist because Vim's input loop predates async events. Events that are safe to run between keystrokes go into events; events posted from worker threads go into thread_events; events that are safe to run during a vgetc() (because they don't recurse into the editor state) go into fast_events.
graph LR
subgraph Sources
UV[libuv callbacks<br/>uv_read, uv_timer, ...]
Thr[Worker threads]
Defer[Deferred from C<br/>aucmd_defer, multiqueue_put_event]
end
UV --> FQ[fast_events]
UV --> EQ[events]
Thr --> TQ[thread_events]
Defer --> EQ
subgraph Consumer
loop_poll[loop_poll_events]
end
FQ --> loop_poll
TQ --> loop_poll
EQ --> loop_poll
loop_poll --> Editor[Editor state machine]loop_poll_events() (src/nvim/event/loop.c) is the function that the rest of the editor calls when it would otherwise block waiting for input. It pumps libuv (uv_run with UV_RUN_NOWAIT), drains the fast queue, optionally drains the deferred queue, and returns. The "optionally" is the key bit — when called from safe_vgetc() while in a state that can handle deferred events, loop_poll_events() flushes everything; when called from inside an unsafe path, it processes only fast events.
The bridge between this and the Vim-style vgetc() is K_EVENT (src/nvim/keycodes.h): a synthetic key value pushed into the input buffer when an async event needs to be observed. The state callback for the current mode (e.g. normal_execute) receives K_EVENT and processes any pending UI updates without consuming a real keystroke.
MultiQueue
src/nvim/event/multiqueue.c implements a queue with a "parent" slot. When you push into a child, the event also lands in the parent. This lets every spawned process (every job!) have its own queue while still routing events through the main loop:
Process *proc = ...;
MultiQueue *events = multiqueue_new_child(main_loop.events);
proc->events = events;The parent can drain everything; a child drain consumes only its own. Useful for batch-canceling all events for a specific subsystem: free the child, the parent is unaffected.
Streams
RStream (rstream.c) and WStream (wstream.c) wrap libuv's stream APIs (uv_read_start, uv_write) into callback-based, buffered I/O. Every input source — stdin in --embed mode, the TUI side of the pipe, RPC sockets, child process pipes — is read through an RStream and demultiplexed into the right queue. WStream does the symmetric job for outgoing bytes.
Process spawning
Proc (event/proc.c, event/libuv_proc.c) is the editor's uv_spawn wrapper. It owns the child handle plus an RStream for stdout, a separate RStream for stderr, and a WStream for stdin. :terminal, vim.system, jobstart, the LSP transport, and remote-RPC channels all spawn through this layer.
When a process exits, Proc posts an event onto its parent queue; consumers register an exit callback. There is no separate "wait" path — exits are events like any other.
Timers
event/time.c is the wrapper for uv_timer_t. It is what vim.defer_fn, setTimeout-style usage in vim.uv, and :redrawtimer resolve to.
Integration points
- Mode state machine —
safe_vgetc()insrc/nvim/getchar.cis the most common consumer. It callsloop_poll_events()so that deferred events are processed before returning a key. - API dispatch —
src/nvim/api/private/dispatch.cruns API requests inside the loop. Most requests synchronously process and respond; some (long-running) yield via the loop. - Lua via
vim.uv—runtime/lua/vim/uv/exposes a binding to the sameuv_loop_t. Lua can register callbacks directly without going through C. - Channels — every msgpack-rpc channel registers an
RStreamand aWStreamwith the loop. See API and msgpack-rpc.
Entry points for modification
- To add a new event source, register an
RStreamagainst the loop and post events onto an appropriateMultiQueue. The pattern is mirrored everywhereRStreamis used;src/nvim/channel.c#channel_create_internalis a small example. - To add a new "fast" path that runs during
vgetc(), push events ontomain_loop.fast_eventswithmultiqueue_put_event(main_loop.fast_events, ...). The handler must be reentrant and may not call into the editor core. - To debug what's queued, instrument
multiqueue_put_eventinsrc/nvim/event/multiqueue.c. There is no built-in logging for queue depth.
Key source files
| File | Purpose |
|---|---|
src/nvim/event/loop.c |
Loop lifecycle, loop_poll_events, integration with libuv |
src/nvim/event/defs.h |
Public types (Event, Loop, MultiQueue) |
src/nvim/event/multiqueue.c |
The multi-priority queue with parent/child relationships |
src/nvim/event/rstream.c, wstream.c |
Read/write streams |
src/nvim/event/proc.c, libuv_proc.c |
Child process supervisor |
src/nvim/event/socket.c |
TCP/Unix socket wrappers (used for RPC servers) |
src/nvim/event/time.c |
Timers |
src/nvim/event/signal.c |
Process signal handlers |
src/nvim/getchar.c |
safe_vgetc — the editor's input loop that pumps the event loop |
For the broader narrative, including how this interlocks with the Vim-inherited input model, read runtime/doc/dev_arch.txt (the "The event-loop" section).
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.