The nx.* model
nxvim has its own, purpose-built plugin system. Every editor API lives in the
nx.* namespace (config and plugins alike), with a small set of familiar vim.*
names aliased to nx.* for convenience. This page explains
the model that shapes the whole API; Writing plugins is the
hands-on how-to, and the nx.* API Reference chapter lists every function.
The model in one sentence:
The server owns every UI surface and the frame; plugins are async, declarative providers of data and behavior.
Where neovim hands you buffer primitives and redraw hooks and says “draw your own completion menu,” nxvim hands you a completion engine and says “give it items.”
Compared to neovim’s plugin model
neovim’s plugin model fits neovim’s architecture. A plugin there is an imperative
program written against the editor’s runtime — synchronous, re-entrant editor
access; blocking reads (getcharstr, vim.wait pumping the loop); libuv as a public
API (vim.uv timers / processes); frame-time render hooks (decoration providers
running Lua inside redraw); and the open-ended vim.fn inventory. The plugins that
define that ecosystem’s UX — completion menus, fuzzy pickers, statuslines, popups,
tree sidebars — own frame time and input loops, and neovim’s single-process,
synchronous core is built to let them.
nxvim’s architecture is different (see Architecture), so its
plugin model is too. nxvim-core is pure and synchronous, Lua influences the editor
only through snapshot reads + queued effects drained at a settle point, and a
client-server boundary puts the server in charge of the frame and every UI
surface. Those properties — a pure core, a frame no script can stall, identical
behavior across every front end including the serverless WebAssembly build — are the
point of the design, and they call for plugins that are async, declarative
providers rather than imperative programs. Same goal (rich, scriptable UX); a model
shaped by a different runtime.
The five rules
The model is five rules — each one a property the architecture already enforces internally; the API just makes it the documented contract:
- Reads are snapshots.
nx.buf.lines(b)and friends read the state pushed at Lua entry. Documented, not disguised as live access. - Writes are queued effects. Applied at the settle point, not instantly. An
async writer guards with a changedtick (
nx.buf.edit{ tick = t, … }) and fails loud if the buffer moved under it. - Nothing blocks, ever. No wait-pumps, no blocking reads, no uv handles.
Anything that waits returns a promise you
nx.awaitinsidenx.async, or — for streaming — an async-iterator (nx.run_stream+nx.await_each). See Async & promises. - No frame-time Lua. Plugins publish decorations / segments / items whenever they like; the server folds them into the next frame. A plugin cannot make redraw slow.
- Registrations are data. A provider registers with a name + schema and is
called with a context carrying a generation token; it emits through the
context’s sink (
ctx.push) and signals completion by returning. Stale async responses are dropped by the engine.
Because Lua influences the editor through the same queues RPC clients use, every
nx.* registration has an RPC twin in principle — out-of-process providers, in any
language, are the same surface (later). The in-process Lua host is v1.
Providers, not programs
The inversion rule 5 implies: the server owns the engine, and a plugin is a thin source / segment / provider plugged into it. This is what keeps plugins small and the frame safe — the hot path (rendering, navigation, matching, input grab) lives in Rust, written once, and the plugin only supplies data.
| The neovim “shape” | In nxvim |
|---|---|
| Completion menu (the nvim-cmp shape) | nx.complete engine; plugins are sources |
| Statusline (the lualine shape) | nx.statusline; plugins register segments |
| Fuzzy finder (the telescope shape) | nx.picker engine; plugins are sources |
| Snippets (the LuaSnip shape) | nx.snippet engine (LSP grammar, tabstops) |
| File tree / sidebar / dashboard — a bespoke plugin UI in neovim | First-class content surfaces: nx.view in an nx.dock, nx.component for reactive ones |
| Decoration provider | nx.decor — viewport-scoped, recomputed off the frame |
The same applies to the UI itself. In neovim, plugin UIs are bespoke — a file
tree, a popup, a dashboard is each hand-built from buffer and window primitives, so
every plugin reinvents rendering, navigation, and input handling. nxvim ships those
as first-class APIs: nx.view (dockable content
surfaces), floating windows, the nx.ui widgets, and the reactive nx.component —
so a plugin describes a UI instead of drawing one.
vim.* aliases
The editor API is nx.*. For convenience, a small set of familiar vim.* names are
provided as thin aliases over their nx.* equivalents, so common config reads in
muscle-memory spellings — vim.g.mapleader, vim.o.number = true, a
vim.keymap.set block, an nvim_create_autocmd block, vim.cmd.colorscheme —
without learning a new vocabulary first. They’re aliases, not a second API: the same
objects, with nx semantics (snapshot reads, queued effects, settle-point
callbacks).
The aliased names (ADR 0002 has the canonical list):
- Variables / options / env —
vim.g/vim.b/vim.w,vim.o/vim.opt/vim.opt_local/vim.bo/vim.wo,vim.env. - Dispatch & keymaps —
vim.cmd,vim.keymap.set/del. - Pure helpers —
vim.tbl_*,vim.split,vim.trim,vim.startswith/endswith,vim.list_extend,vim.deepcopy,vim.inspect,vim.json. - Declarative registrations — a partial
vim.apiofnvim_create_autocmd/augroup/del/clear(→nx.on),nvim_create_user_command(→nx.command), andnvim_set_hl(→nx.hl.define), plusvim.filetype.add. - Callback-shaped async —
vim.notify,vim.schedule,vim.defer_fn,vim.ui.input/select, andvim.systemin its callback form. - Treesitter highlight toggle —
vim.treesitter.start/stop, mapping to thenx.bo.filetype/nx.bo.ts_highlightbuffer nouns.
The list is intentionally small — these convenience spellings, and nothing more;
everything else (LSP, treesitter, processes, the filesystem, …) is nx.*.
Colorschemes are data, not plugins
A colorscheme is pure data — a table of highlight-group definitions, registered
through the nx highlight API (the nvim_set_hl alias above). It never touches the
runtime model, so it crosses the snapshot/effect boundary intact: sourcing one is
just running Lua that fills the highlight registry. It uses the same vim.* aliases
as any other config — there’s no plugin host and no special case.
Dogfooding the nx.* API
The split: the core provides primitives — the engines and UI surfaces, in Rust:
completion, the picker (a float-list widget), statusline, snippets, nx.view /
nx.dock / floats, nx.decor, plus the tree-sitter / LSP / regex engines — and the
more complex UI behavior is implemented as plugins in Lua, composing those
primitives. A file explorer, a which-key popup, statusline extras, a completion or
picker source: each is a plugin, not bespoke Rust.
nxvim is the plugin API’s first and most demanding consumer — its first-party plugins
use the same public nx.* API a third party would, with no privileged access. That
keeps the API honest: a feature that can’t be expressed against nx.* is a gap to
close in it.
See also
- Writing plugins — the hands-on authoring guide.
- Async & promises — rule 3 in practice (
nx.promise/async/await). - ADR 0002 — native plugin system — the
decision record and the canonical list of
vim.*aliases. - The native plugin API design sketch — the full surface, the five rules, and six worked examples.