Known approximations & missing features
nxvim tracks vim/neovim’s observable editing behavior, but it is a fresh Rust implementation, not a port — so some surfaces are only approximate, and some subsystems simply aren’t built yet. This page maps where nxvim diverges from neovim, so a config or plugin you bring over holds no surprises.
Two principles shape what follows:
- No silent stubs. Anything unimplemented fails loud — it raises
nxvim: not implemented: <name>rather than quietly returning a fake value, so a half-working feature never masquerades as a whole one. You find out at runtime exactly what tripped, instead of chasing mysterious wrong behavior. - Honest approximations. Where nxvim does something plausible but not fully faithful, that is flagged in the source right beside the code, and the larger ones are listed below.
Seeing what your own config hits
Every loud gap a running config trips is collected in nx._notimpl_hits, so you
can see precisely which gaps you actually hit — not just which ones exist:
:lua print(vim.inspect(nx._notimpl_hits))
The precise, always-current per-function detail lives in the code: greppable
INCOMPLETE: comments mark the silent approximations (each with its “why” and
the fix), and nx._notimpl raises mark the loud gaps. The sections below cover
the larger divergences and the whole subsystems that have no single line to point
at. If this page and the code ever disagree, the code is right — treat this
as a guide, not the registry.
Missing or partial subsystems
These are whole areas where the subsystem itself is absent or only partly built,
so a config that leans on one meets a nil value or a generic error rather than a
named gap — worth knowing up front. (Subsystems that are now fully built — the
tree-sitter platform with start/stop, on-disk and customized queries,
injections, incremental updates, and :TSInstall fetch/compile — aren’t listed;
only the edges that still diverge are.)
- Treesitter — two edges remain. The
vim.treesitterplatform is built (see the platform design and ADR 0001). What still diverges: (1) the decoration-provider highlighterhighlighter.newfails loud (nx._notimpl) — nxvim’sstart/stopdrives the in-core Rust engine instead, so a highlight-onlystartnever builds a Lua-sideLanguageTreeandhighlighter.active[buf].treereads nil until something callsget_parser; (2) Lua-driven indent (indentexpr=v:lua…/indent.lua) is unwired — it wants the live buffer mid-keystroke, which fights the snapshot bridge, so the Rust indent stays.query.getreturns nil for a missing on-disk query file. - Treesitter query resolution — additive, host-only. The query bridge
(design) merges a language’s
bundled base with runtimepath
queries/+after/queries/and the; inherits:chain — and:TSInstallfetches the inherited query sets too (javascript→ecma,jsx), so base js/ts highlighting carries theecmapatterns. Two edges remain: (1) the merge is additive concatenation, not neovim’s full replace-vs-extend precedence; (2) it resolves only the buffer’s own language — an injected child grammar still loads its query off disk raw, so an; inherits:-based child (e.g.javascriptinjected into markdown) paints only its own non-inherited captures until that child language is itself opened as a buffer (which installs its resolved overlay). - No
vim.uv/vim.loop. neovim exposes libuv as a public Lua API; nxvim does not — thevim.uv/vim.looptable does not exist, so a plugin reaching for it hits a loud nil index. Both the libuv handle surface (new_timer/new_check/new_fs_event/spawn/new_pipe, the plugin event-loop primitives) and the synchronousfs_*/ scalar primitives (fs_realpath,cwd,os_homedir,os_uname,hrtime,now) are gone. Async lives in thenxAPI (nx.run/nx.timer/nx.fs); the synchronous host info the LSP-config paths need is read throughvim.fn(executable/exepath/glob/filereadable/resolve/ …) instead. - Broad options surface. The set of honored options has grown well past the
indentation knobs (the authoritative list is
crates/nxvim-core/src/options.rs).:set(and:setlocal/vim.bo/nvim_{set,get}_option_value) honors: the search booleans (ignorecase/smartcase/wrapscan/hlsearch/incsearch); the window-local rendering options (number/relativenumber/cursorline/numberwidth/signcolumn/wrap/breakindent/showbreak/sidescroll/sidescrolloff/winhighlight/fillchars); the fold options (foldmethod/foldenable/foldcolumn/foldlevel); the buffer-local indentation options (tabstop/shiftwidth/softtabstop/expandtab/commentstring); and a set of nxvim-native options (scrollanim/scrollanimduration,qfdock,imagepreview,history/persisthistory,regexsyntax,switchbuf,laststatus/showtabline, …). nxvim breaks with vim’s defaults on indentation:tabstopdefaults to 4, withshiftwidth=0(“follow tabstop”) andsofttabstop=-1(“follow shiftwidth”) so the onetabstopknob drives the whole indent width.tabstop,softtabstop, andexpandtabdrive rendering and<Tab>;shiftwidthdrives the>>/<<shift operators and the LSP indent width.commentstringbacks thegc/gcccomment operator and defaults from the filetype (the ~20 most common languages) when unset. Still, the bulk of vim’s hundreds of options are missing — a write to an unmodeled option is recorded but inert — as are macros. - Legacy Vimscript (
eval.c). Deliberately not on the roadmap (guiding principle 2).vim.fn.*is a hand-written set of helper aliases, not an interpreter — unimplementedvim.fn.*entries are loud gaps, not a TODO to build an evaluator. :TSInstallapproximations. The command fetches/compiles grammars (nxvim_ts::install), with a pinned, checksum-verified Zig fetched on demand when no systemcc/clang/gcc/zig(or$NXVIM_CC) is found — on macOS, Linux, and Windows alike. Remaining: (1) grammars needingtree-sitter generate(no committedsrc/parser.c) fail loud rather than generating; (2) the nvim-treesitter ref is pinned in source — no:TSInstall-from-HEAD.- LSP semantic tokens approximations. Painted over the treesitter floor
(
crates/nxvim-server/src/lsp/semantic.rs): one resolvable group per cell (the merge picks the most-specific@lsp.*winner, it doesn’t blend neovim’s@lsp.type.<t>+ per-modifier stack); theme-gated (an undefined group is dropped so the floor shows); norange(onlyfull/full/delta);highlight_tokenis a loud gap (nx._notimpl— a Lua callback on the decode hot path);get_at_posreads the cached mirror even for astopped buffer; no per-client granularity (one cache per buffer); repaints mid-insert (update_in_insertalways on). Seedocs/plans/2026-06-08-lsp-semantic-tokens.md. - LSP inlay hints approximations. Painted inline, opt-in
(
crates/nxvim-server/src/lsp/inlay.rs).inlayHint/resolve(lazy per-hint label fill) andvim.lsp.inlay_hint.get(with a line-range filter) have landed. What still diverges: oneLspInlayHintgroup for all kinds (no Type/Parameter split); the fetch is whole-document — the viewport-scopedrangerequest is deferred; per-buffer enable only (no per-client granularity); horizontal-scroll (leftcol>0) + inline hints is best-effort; repaints mid-insert. Seedocs/plans/2026-06-08-lsp-inlay-hints.md. - Synchronous prompts — one caveat.
vim.fn.input/confirmreturn inline via a pumped coroutine (nx._pump), but only pumped entry points (:luachunk, keymap, user command) can prompt — Lua sourced at startup or off a bare callback has no coroutine to yield from. Seeexamples/sync-prompts/. nx.statuslinesegment registry — v1 deferrals. The lualine-shaped surface is built (nx.statusline.setup/segment/invalidate; built-ins composed innxvim_core::statusline::compose_segments, custom segments rendered per window and cached server-side; seedocs/plans/2026-06-15-nx-statusline-segments.md). Custom segments are now per-window: each is rendered once per window against that window’s{ buf, win, focused }, cached by(window, name), and re-rendered when the segment is invalidated or the window layout changes (split/close, focus move, or a window swapping its buffer) — soctx.focusedandctx.bufare correct in every window. The server orchestrates the re-render fromrun_pendingwith a fresh window mirror (EditHost::refresh_statusline_segments), so an invalidate fired from an autocmd that ran before the transition still renders against the settled layout. Layouts are also per-window /setlocal-able:nx.statusline.setup{win=…}sets a window-local layout that overrides the global one,setup{win=…, format=true}opts a window back to the'statusline'%-format even under a global segment layout (the per-region mix), andnx.statusline.reset(win)drops the override (EditHost::resolve_window_layout). Mouse-click segment regions have landed: a segment can carry anon_clickhandler, lowered to the%@func@…%Xstatusline syntax (with%nTtabline labels andlaststatus=3), so clicks dispatch back to Lua. What it does not do yet: (1) The custom segmentctxcarries{ buf, win, focused }but nowidth(the server doesn’t mirror the per-window statusline width to Lua). (2)git/lsp_progressare plugin segments (custom-segment examples), not built-ins. The built-in set ismode/filename/filepath/filetype/encoding/location/modified/readonly/diagnostics.
Shared limitations
A handful of the approximations above trace back to the same underlying limitation, so they tend to surface together. The notable ones:
| Limitation | What it affects |
|---|---|
| LSP helpers not window-arg-aware (always use the current window) | make_position_params(window) is now window-aware (reads the passed window’s buffer + cursor), and open_floating_preview returns real float handles (a relative="cursor" float over a scratch buffer, auto-closing on cursor move). Remaining: the completion-doc preview box is still a single bespoke box with no separate preview-window handle / completeopt matrix (the completion menu is server-owned chrome, not a window). Note: splits, floats, and tab pages themselves are implemented — see architecture.md Windows. |
| No multi-buffer name/disk registry | make_text_document_params (non-current bufnr → empty URI), locations_to_items & apply_workspace_edit for unopened files |
| Core honors a fixed set of buffer-local options | vim.bo / nvim_set_option_value writes other than filetype / tabstop / shiftwidth / softtabstop / expandtab / commentstring / regexsyntax / fileencoding / bomb are recorded but inert |
Diagnostic-display surfaces are approximations, not gaps — all four ship. underline, virtual_text (inline end-of-line message), signs (gutter glyph), and the on-demand float (vim.diagnostic.open_float) are implemented — see docs/plans/2026-06-08-diagnostic-display-surfaces.md. | vim.diagnostic.config keys other than underline / virtual_text / signs (virtual_lines, severity_sort, and the config.float pre-style defaults). open_float ignores its opts (scope/severity filters, format/header/prefix/border) — the default cursor-line scope shows, in the bottom panel (plain lines, like hover) not a cursor-anchored bordered popup. The virtual_text table honors prefix and the signs table its text glyph map; their format / severity filters and sign priority/culhl are not applied, the line’s most-severe diagnostic wins the one inline slot / sign cell, and the sign column is client-side only (a fixed 2 cells not subtracted from nxvim-core’s text width, so a full-width line under nowrap can clip its last two cells). |