Open-Source Wikis

/

Neovim

/

Systems

/

Event loop

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       Timers

Key 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 machinesafe_vgetc() in src/nvim/getchar.c is the most common consumer. It calls loop_poll_events() so that deferred events are processed before returning a key.
  • API dispatchsrc/nvim/api/private/dispatch.c runs API requests inside the loop. Most requests synchronously process and respond; some (long-running) yield via the loop.
  • Lua via vim.uvruntime/lua/vim/uv/ exposes a binding to the same uv_loop_t. Lua can register callbacks directly without going through C.
  • Channels — every msgpack-rpc channel registers an RStream and a WStream with the loop. See API and msgpack-rpc.

Entry points for modification

  • To add a new event source, register an RStream against the loop and post events onto an appropriate MultiQueue. The pattern is mirrored everywhere RStream is used; src/nvim/channel.c#channel_create_internal is a small example.
  • To add a new "fast" path that runs during vgetc(), push events onto main_loop.fast_events with multiqueue_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_event in src/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.

Event loop – Neovim wiki | Factory