0.148.0
Added
- Dead-code gate —
npm run lint:deadcode(knip) is blocking in CI, andnpm run verifynow runs four static gates. It removed 70 unreferenced declarations across 24 files plus one dead file, each confirmed independently by knip and by type-aware ESLint before deletion, and each deletion cascaded until both tools reached a fixpoint. Nothing was allowlisted that could simply be deleted.- Deleted: superseded table manipulation helpers (Obsidian's native
TableEditoractions supersede them), unused treesitter JS-API and query-cache surface, snippet autocomplete helpers, andsrc/vim/jumplist-bridge.ts, whosecreateJumpListBridge()had no caller — the jump list works through theJumpListclass instead. knip.jsoncis JSONC specifically so every ignore entry carries its reason inline. The categories are: vendored fengari, dependencies selected by string name inwdio.conf.mts(framework: 'mocha',reporters: ['obsidian'],runner: 'local'), and the ambient__DEV__declaration. The bridge ignores were removed when it was wired up.- Fixed three test files importing
../../../../src/lib/fengari, one level above the repository root. Vite resolved it leniently so the tests passed; knip did not.
- Deleted: superseded table manipulation helpers (Obsidian's native
- Test-quality gate —
test/was in ESLint'sglobalIgnores, so 346 files and 4,259 test blocks had never been linted at all.@vitest/eslint-pluginnow coverstest/unit/**andeslint-plugin-wdiocoverstest/specs/**, both blocking. It surfaced 166 findings, all fixed rather than suppressed — noeslint-disablewas added.- 46
wdio/no-floating-promise: an async browser assertion that is built but never awaited resolves to a pending Promise, is truthy, and never throws, so the test passes regardless of what the browser did.wdio/await-expectships off in the plugin's own recommended set and is forced on here. - 57
vitest/expect-expect, 13no-conditional-expect, 10no-identical-title, 7valid-expect, 2no-standalone-expect, plus one tautological self-comparison and one constant-foldednull ?? {}that made a test a silent duplicate of the next one. wdio/no-pauseis off (2902 occurrences of an establishedbrowser.pause()idiom — a flakiness question, not a vacuity one), and the general lint backlog intest/is switched off there and separately owned. Neither is suppressed forsrc.expect-expectis syntactic and trusts any helper named inassertFunctionNameswithout inspecting it, so it proves a test asserts something, never that it asserts the right thing.luaExpectOkandrunwere added only after reading their definitions and a call site;runis scoped totest/unit/lua/**andtest/unit/fengari/**because the name is generic enough that trusting it suite-wide would let any future function so named satisfy the rule.
- 46
negative-controlskill (.agents/skills/negative-control/SKILL.md) — generalises the "the test MUST fail first" requirement fromissue-repro, which only applied to GitHub-issue bug fixes and mandated e2e tests. It now covers every new or modified test: unit tests written alongside a feature, tests added for existing code, and tests touched during a refactor. Ranks three techniques (red-first, sabotage the subject, invert the assertion), requires concrete observed failure values rather than "I verified it fails", and carries a nine-item vacuity checklist.issue-reprokeeps its own workflow and the two cross-reference each other.require()resolves synchronously — every.luafile underlua/is read into memory when the configuration loads, andrequire()resolves from that snapshot. This unblocks lazyrequire, which is the idiom nearly the whole modern Neovim plugin ecosystem is built on:vim.keymap.set('n', 's', function() require('plugin.jump').start() end)previously failed withmodule 'plugin.jump' not found: async APIs can only be called from async-capable callbacks, because reading from the vault is asynchronous and keymap callbacks run on the main state via a plainlua_pcallthat cannot yield. Only a cache miss was ever affected —package.loadedhits were already synchronous.- Plugin:
src/lua/module-snapshot.ts(new — walk, index, limits, atomic swap),src/lua/package.ts(snapshot-first resolution),src/lua/coroutine-runner.ts(isAsyncCapable),src/lua/loader.ts(awaited rebuild before user config, refresh after plugin fetch, skip reporting) - The asynchronous vault read is retained, but only for callers that can wait for it — top-level configuration, autocommands, timers. A synchronous caller that misses the snapshot is told the module is
not present in the configuration snapshot, naming both paths tried, rather than a generic "not found" indistinguishable from a typo. - Files added or edited after the configuration loads need a reload; there is no live watcher. The snapshot rebuilds on configuration reload and after a
vim.plugins.add()fetch, before the fetching coroutine resumes, so a freshly fetched plugin is immediately requirable. - Limits are reported, not silently applied: 512 KiB per file, 16 MiB total, 2,048 files, 32 directory levels. Skipped files are named in the console with a reason, because a file dropped for exceeding a budget would otherwise present as a missing module.
- The snapshot reader and the async-capability predicate reach the injected
requirechunk as chunk arguments, not globals, so sandboxed user Lua has no handle on them. - Measured against the flash.nvim diagnostic:
require_in_callbackwent fromblocked: … async APIs …toworks, andConfig.get().search.multi_windowfrom an error totrue. flash now runs to its LuaJIT FFI dependency, which is architectural and not fixable in a pure-Lua VM.
- Plugin:
vim.ui.selectandvim.ui.input— Neovim's UI-hook namespace, backed by the existing Telescope-style picker and input modal.vim.uiis a plain mutable table with no metatable, so dressing.nvim / telescope-ui-select / snacks can replace and restore its fields, which is the idiom the namespace exists for. Both are non-blocking: they return immediately and invoke their callback later on a coroutine thread, so they work from inside avim.keymap.setcallback — the most common call site, where a yield-based design would hard-error.on_choicereceives the original Lua value (items are commonly tables) plus a 1-based index;on_confirmdistinguishes''(empty confirm) fromnil(cancel).format_itemis applied eagerly, matching Neovim's own default implementation.- Plugin:
src/lua/ui-api.ts(new),src/lua/loader.ts(injection + wiring),src/main.ts(openUiSelectpicker binding) - Where no selection UI is available,
vim.ui.selectraises rather than settling withnil— a caller cannot distinguish anilsettle from "the user cancelled". vim.ui.open(path, opts?)openshttp(s)://targets in a new window and everything else with the system handler, returning Neovim'svim.SystemObj|nil, nil|stringshape. On mobile, or with no handler,nil, errmsgis the correct answer rather than a fudge.opts.cmdis rejected outright — arbitrary command execution is against the plugin's security posture.vim.ui.progress_status()returns'', which is exactly what Neovim returns when no progress is active.- An open picker is closed before the Lua state is destroyed, so a late selection cannot invoke into a closed
lua_State. - Design validated before implementation by
test/specs/spikes/spike-ui-callback-context.e2e.ts, which proves a keymap callback cannot yield but a callback it schedules can.
- Plugin:
- Picker reports cancellation —
PickerOptions.onCancelfires exactly once when the picker closes without a selection, andPickerModal.closeActive()closes a live picker. Needed by any caller that must distinguish "chose nothing" from "chose something", such as a Neovim-stylevim.ui.select.confirmSelectioncloses the modal before dispatching the selection, soonCloseruns first; adidConfirmflag set synchronously beforeclose()keeps a successful selection from also reporting a cancel.- Plugin:
src/picker/picker.ts(didConfirm,onClosecancel dispatch,closeActive),src/picker/types.ts(onCancel)
- Plugin:
nvim_set_decoration_provider(ns, opts)— real implementation of Neovim's per-redraw decoration callbacks (on_start→on_buf→on_win→on_end), backed by a CodeMirrorViewPluginthat coalesces work into onerequestAnimationFrameper frame rather than dispatching from insideupdate()(which CodeMirror rejects). Guarded by a transaction annotation, a re-entrancy flag, a per-view rAF handle, a runtime generation counter, a 100k instruction limit per callback, and a fault counter that disables a provider after 8 consecutive errors.on_winreturningfalseskips the rest of that provider's cycle, matching Neovim.- Plugin:
src/lua/decoration-provider.ts(new — manager + CM6 extension),src/lua/api.ts(handler),src/lua/loader.ts(wiring +registerStateCleanup),src/main.ts(extension registration) on_lineandon_rangeraise a Lua error naming the unsupported key rather than being silently accepted;ephemeralextmarks likewise. Erroring is closer to Neovim than silently persisting, and avoids the accumulating-stale-decoration failure.- Mechanism validated before implementation by
test/specs/spikes/spike-decoration-provider-raf.e2e.ts(Phase 0b): 7 assertions including two negative controls that reproduced the re-entrancy error and a synthetic feedback loop.
- Plugin:
- Extmark
hl_eol,strict, and priority ordering —nvim_buf_set_extmarknow parseshl_eol(extends the highlight to the end of the line containing the range end) andstrict(out-of-range positions clamp instead of dropping the mark). Overlapping marks are ordered bypriority, deterministically and independently of insertion order.- Plugin:
src/lua/api.ts(opts parsing),src/lua/extmarks.ts(hlEol/strictinExtmarkOpts, line-awarebuildDecorations, priority-aware sort) - Known gap:
priorityorders decorations but does not yet decide which wins visually — CM6 marks carry no z-index andDecoration.set(..., true)re-sorts. Recorded inKNOWN_LIMITATIONS.md.
- Plugin:
- LuaJIT
bitlibrary — Neovim runs LuaJIT, which Neovim documents as its permanent plugin interface, so plugins reach forbit.band/bor/lshiftrather than Lua 5.3's native&/|operators. The library was absent entirely; it is now available with LuaJIT's semantics, including signed 32-bit results (bit.bnot(0)is-1, not4294967295) and the distinction between logicalrshiftand arithmeticarshift. Implemented arithmetically rather than with native operators: Lua 5.3's&requires an exact integer representation, and this VM widens integers to 53 bits, so a value arriving as a float raised "number has no integer representation".- Plugin:
src/lua/engine.ts(luaCompatShims)
- Plugin:
require("ffi")fails with an accurate message — LuaJIT-only natives previously fell through to the module file read and surfaced whatever that failed with, which described the wrong problem. They now report that the module requires LuaJIT and that this runtime is a pure-Lua VM. Ordinary missing modules are unaffected.- Plugin:
src/lua/package.ts
- Plugin:
nvim__redrawis a warn-once stub again, deliberately truthy — it briefly read asnilso that flash'sif vim.api.nvim__redraw thenprobe would take its fallback. That was right forhighlight.cursor, whose fallback isnvim_buf_set_extmark, but wrong forhacks.setcursor, whose fallback is LuaJIT FFI and which is called unguarded on every keystroke throughUtil.get_char. One name, two opposite correct answers; the truthy stub avoids throwing on the hot path, at the cost ofhighlight.cursorno longer drawing its cursor highlight. Thenil-reading dispatch tier introduced for it has been removed rather than left with no members.- Plugin:
src/lua/api.ts
- Plugin:
- Vim regex translation —
vim.fn.searchpos,vim.fn.splitandvim.regexcompiled their pattern withnew RegExp, so Vim syntax silently matched nothing:\Vand\Care identity escapes in JavaScript, making\Valpha\Ca search for the literalValC. A shared translator now handles magic levels (\v,\m,\M,\V), the case flags\c/\C,\zs/\zeas lookbehind/lookahead,\</\>word boundaries,\%(non-capturing groups, and the Vim character classes.- Plugin:
src/lua/vim-regex.ts(new),src/lua/vim-search.ts,src/lua/fn.ts(split),src/lua/regex.ts - Breaking: these three now take Vim patterns rather than JavaScript ones, which is what Neovim documents them to take. At the default magic level
+,?,(,)and|are literal, so a JavaScript pattern such as\d+must be written\d\+.NEOVIM_API_STATUS.mdpreviously recorded the ECMAScript behaviour as a known deviation. - Found by auditing flash.nvim's API usage rather than by hitting it:
vim.fn.split(s, "\zs")is Vim's split-into-characters idiom and flash uses it to build label lists, so default label generation was broken independently of the search.
- Plugin:
- Indexed scope access:
vim.bo[buf],vim.b[buf],vim.wo[win],vim.w[win],vim.t[tab]— Neovim allows bothvim.bo.filetypeandvim.bo[bufnr].filetype, and plugin code uses the indexed form freely. Our proxies accepted string keys only, so an indexed access resolved toniland the caller failed withattempt to index a nil value. A numeric or nil key is now validated as handle0and returns the scope table. This closed the last of three blockers preventing flash.nvim from rendering:flash/cache.luareadsvim.bo[buf].filetypeandvim.b[buf].changedtick, and with both fixedflash.state.new{...}completes and writes extmarks into the document.- Plugin:
src/lua/api.ts(isScopeHandleKey, indexed branch on all five scope proxies)
- Plugin:
nvim_list_bufs()andnvim_tabpage_list_wins()— both previously warn-once stubs returning an empty list, which is never a valid answer: there is always at least the current buffer and window. Each now returns{0}, consistent withnvim_list_wins()and the current-handle APIs.nvim_tabpage_list_winsvalidates its argument through a newrequireTabpageZeroguard. Measured impact: flash.nvim'sCache:_update_wins()overwritesstate.winswith the filtered result ofnvim_tabpage_list_wins, so an empty list left it with zero windows, zero matches, and nothing rendered.- Plugin:
src/lua/api.ts(requireTabpageZero, both implementations, promoted intoSUPPORTED_NVIM_API_FUNCTIONS)
- Plugin:
- Unicode index conversion for
vim.fn—strchars(s, skipcc?),charidx(s, byteidx, countcc?), andbyteidx(s, nr)convert between UTF-8 byte offsets and Vim character indices.strcharscounts composing marks separately unlessskipccis set;charidxandbyteidxfold them into the preceding base character, matching Vim. All three are on flash.nvim's default label-positioning path.- Plugin:
src/lua/fn.ts(buildCharSpansbyte-span mapping, three registrations)
- Plugin:
vim.fn.wincol()andvim.fn.winlayout()—wincol()reports the cursor's screen column measured from the window edge, so the gutter counts, derived from CodeMirror geometry with a cursor-column fallback when geometry is unmeasurable.winlayout()reports a single leaf whose window handle matchesnvim_list_wins().wincolis on leap.nvim's search path;winlayoutis on flash.nvim's window-layout save path.- Plugin:
src/lua/window-info.ts(getCursorWinCol),src/lua/fn.ts(registrations)
- Plugin:
- Window-local option scope (
vim.wo) — replaces the warn-and-return-nilplaceholder with a real proxy.wrapreports CodeMirror's line-wrapping state; writes shadow the resolved value; every other key falls back to the global scope, matching Neovim where an unset:setlocalvalue resolves to the global one. Required by leap.nvim's core search loop.- Plugin:
src/lua/api.ts(readWindowOption, window-option shadow,vim.woproxy),src/lua/loader.ts(getWindowOptioncallback)
- Plugin:
vim.bo.iminsertandvim.bo.fileformat— buffer-local options read by flash.nvim and leap.nvim on every invocation, and by nvim-surround.- Plugin:
src/lua/loader.ts(getBufferOptioncases)
- Plugin:
Changed
- Global option fallbacks —
vim.o/vim.goresolve engine → shared shadow store → defaults (eventignore,selection,cmdheight,columns,lines,cpo, theme-derivedbackground) →nil. Unsupported writes can be retained as compatibility values without implementing their Neovim behavior.- Plugin:
src/lua/api.ts(global option reads/writes),src/lua/loader.ts(normalize returnedErrorobjects for unknown fork options)
- Plugin:
- Plugin query retention — downloads retain
.scmquery files under isolatedlua/{owner}__{repo}/queries/roots and refresh the snapshot before Lua resumes. Older cached plugins must be re-fetched to acquire previously discarded queries;.scmedits require a configuration reload. Resolution is bounded to 128 KiB/file, 4 MiB/snapshot, 512 KiB/combined query, 64 sources, and 16 inheritance levels, with diagnostics and affected-content skipping.- Plugin:
src/lua/plugin-fetch.ts,src/lua/plugin-store.ts(archive retention, isolated storage and refresh),src/treesitter/query-files.ts,src/treesitter/named-queries.ts(limits)
- Plugin:
nvim_create_namespacereturns unique IDs — previously hardcoded to0, now returns unique integer IDs per namespace name, matching Neovim behavior. Required for the extmark system and highlight namespace isolation.- ~30 previously stubbed
vim.*utilities now have real implementations — functions that were no-op stubs or returned placeholder values now work correctly (e.g.,vim.is_callable,vim.stricmp,vim.pesc, etc.) - Settings that decide which editor extensions are installed now apply without a restart —
reloadFeatures()never touchedvimExtensionSlot, which onlysetupVimSubsystems()populates, and that runs fromonload()andenableVim()only.animatedCursor,enableSnippets,snippetTriggerModeandenableUndoTreewere therefore restart-only, andenableUndoTreenever reached a reload path at all. Re-runningsetupVimSubsystems()is not an option — it is a one-shot builder that registers global handlers and constructs managers, Lua and autocmd state — andteardownVimSubsystems()is far too destructive for a settings change. Each gated feature now owns a nestedExtension[]that is pushed intovimExtensionSlotonce and whose contents are swapped in place, followed by a singleworkspace.updateOptions(). Built extensions are cached so their identity is stable, which is what allows CodeMirror to keep existing ViewPlugin instances alive across an unrelated reload. The snippet runtime sits in its own slot, separate from the completion and tab integrations, so changingsnippetTriggerModeleaves an in-progress snippet session intact. (#181)- Plugin:
src/main.ts(setSlotEnabled,populateRuntimeSlots,refreshRuntimeExtensionSlots, five feature slots, extension builders extracted fromsetupVimSubsystems),src/settings.ts(enableUndoTreeadded toRELOAD_KEYSand to the imperative handler)
- Plugin:
- Cursor shape changes now reach the animated cursor without a restart —
cursorShapeshas two independent consumers, and only one was broken. The fork readsstate.vim.cursorShapeslive on every render and already tracked settings changes at runtime. The animated cursor keeps its own copy, made bysetCursorShapes(), which onlysetupVimSubsystems()called — so with the animated cursor enabled a shape change did nothing until Obsidian restarted. A slot cannot fix this: the bundled vim extension is never gated, so the reload path re-pushes the value instead. (#181)- Plugin:
src/main.ts(reloadFeatures()re-appliessetCursorShapes)
- Plugin:
- The animated cursor could stay missing after scrolling back to the caret — caught by CI on Windows, where it never returned; on Linux it came back after ~650 ms, which the original "not null" assertion accepted. Three faults stacked.
wake()was not sticky: one arriving while a frame was in flight returned early onrunning, and that frame then parked the loop, discarding it. The blink's dark half is 600 ms and the warm gear also ticks every 600 ms, so a parked loop can land on the dark half of every blink and draw nothing indefinitely — a scroll now counts as movement for blink purposes and shows the cursor solid, as Neovim does. And the scroll listener deduplicated againstcachedScrollTop, which stops advancing while the caret is off-pane, so scrolling back to exactly the last resolved offset looked like no change and skipped the wake. Measured 645 ms → 55 ms. (#181)- Plugin:
src/vim/animated-cursor/manager.ts(wakeRequestedconsumed byscheduleNext),src/vim/animated-cursor/controller.ts(lastSeenScrollTop/Left,positionRetryUntil, blink reset on scroll) - The spec now bounds how long the cursor may take to come back rather than only that it does. "It came back eventually" is what hid all three behind the 600 ms warm frame that rescued Linux and not Windows. Each fault was reverted separately and observed failing the bound at 659 ms and 647 ms.
- Plugin:
- Teardown no longer resurrects the animated-cursor manager —
teardownVimSubsystems()destroys the manager, but CodeMirror destroys the controllers only on the laterupdateOptions(), soCursorController.destroy()calledgetAnimatedCursorManager()after teardown and built a replacement purely to deregister from it. The replacement had no canvas, rAF loop or listeners, so this leaked an object rather than causing a visible fault.destroy()now usespeekAnimatedCursorManager(), which never creates — correct regardless of which order the two steps happen in, unlike reordering teardown would be. (#181)- Plugin:
src/vim/animated-cursor/manager.ts(peekAnimatedCursorManager),src/vim/animated-cursor/controller.ts(destroy())
- Plugin:
Fixed
di(/di{/di[did nothing when the cursor sat before the pair —da(searched forward for the next pair when the cursor was not already inside one;di(did not, because the fallback intextObjectManipulationwas gated oninclusive. Neovim applies the same forward search to both variants (:h v_i(— "when the cursor is not inside a () block, find the next '('"), and the search is not limited to the current line.di"was unaffected because quote objects use a separate scanner that already moved the cursor to the first quote. (#178)- Fork:
~/Repos/codemirror-vim/src/vim.js(textObjectManipulation— forward pair search no longer gated oninclusive)
- Fork:
- Surround
ysi$/ysa$and every other registered text object were dropped —ysstores its text object in ays_motionsub-state and, on the next key, called the fork's built-intextObjectManipulationdirectly. That function only knows the built-in objects (( ) { } [ ] < > ' " ` b B w W p t s), so plugin-registered objects (i$,a$,i=,i~,i_,il,iC,io,i,…) resolved to nothing, the operation was cancelled, and the pendingysivanished from the chord display. Resolution now matches normal operator-pending: the exact key sequence is looked up in the keymap first, falling back to the built-in object so a registered object that shadows a built-in one (aB, blockquote vs{}block) only wins where it actually matches. The same helper is used byysdot-repeat. (#179)- Fork:
~/Repos/codemirror-vim/src/vim.js(runTextObjectMotion, used byhandleSurroundSubState'sys_motionbranch and byrepeatCommand)
- Fork:
vim.keymap.set("", lhs, rhs)only mapped normal mode — Neovim's:h map-modesdefines the empty mode string as Normal + Visual + Select + Operator-pending, butgetModeList()collapsed it to['n'], the same value it uses for a missing mode argument. A config shifting the home row (vim.keymap.set('', 'j', 'h')) worked in normal mode and silently reverted to the default motion the moment the user pressedvor an operator. The empty string now expands ton,v,s,o, and an empty string appearing as a table entry ({""}) expands the same way instead of contributing nothing. Insert mode is deliberately excluded, matching Neovim. (#180)- Plugin:
src/lua/api.ts(getModeList,EMPTY_MODE_EXPANSION) — affects bothvim.keymap.setandvim.keymap.del
- Plugin:
- Animated cursor did not follow the text while scrolling, and left an uncleared phantom behind —
CursorController.update()already comparedscrollDOM.scrollTop/scrollLeftagainst its cached values, but it only runs when CodeMirror produces aViewUpdate, and scrolling inside the already-rendered viewport produces no transaction at all. The only recovery was the 500 ms staleness check intick(), and in the warm gear the next tick is up to 600 ms away, so the cursor never caught up during a continuous scroll. Separately, once the caret scrolled outside the panecoordsToRect()returnednullandrefreshTarget()returned early without clearingcachedRect, sotick()kept repainting the last known rect together with its cached character — the reported "phantom letter". That phantom could not clear itself: a controller that draws always leaves a non-null dirty region, so the manager never reached its full-canvas clear. Apassivescrolllistener onscrollDOMnow marks the position dirty and wakes the manager, and a caret that leaves the pane drops its cached rect. The pane test became a vertical intersection rather than containment, so a caret line half-clipped by the pane edge still renders —draw()already clips to the same rectangle — instead of blinking out at the top and bottom of every scroll. (#181)- Plugin:
src/vim/animated-cursor/controller.ts(onScrolllistener registered in the constructor and removed indestroy(),refreshTarget()clearscachedRect/cachedShapeRect,coordsToRect()vertical intersection test)
- Plugin:
- The character under a block cursor stayed behind when scrolling — the other half of the "phantom letter" in #181, and a different mechanism from the stale rect above.
resolveBlockChar()cachescharTop/charHeight, the viewport coordinates of the character's DOM rect measured for #106 so the glyph is centred on tall lines, keyed only on the document position. Scrolling does not change the position, so the cache hit handed back coordinates measured before the scroll:fillRectdrew the block at the new screen position whilefillTextpainted the letter at the old one. Because the frame reports only the block's rect as dirty, the stranded letter fell outside every subsequentclearRectand stayed on the page. Canvas instrumentation after a 40 px scroll recorded the block at y 668.9 and the glyph still at y 711.9–723.9. The cache key now includes the cursor rect's screen top. (#181)- Plugin:
src/vim/animated-cursor/controller.ts(resolveBlockChar(pos, rectTop),cachedBlockCharTop)
- Plugin:
- Table navigation shared its pending-key state across split panes —
pendingDand the count buffer lived at module scope whileTableNavControlleris a CodeMirrorViewPlugin, instantiated once perEditorView. Pressingdin one pane and then a key in another consumed the first pane's pending state, sodfollowed bydin a different table deleted a row immediately instead of starting its owndd. A count typed in one pane applied to the next motion in another. Both now live per handler. Dot-repeat is deliberately left at module scope: Vim's.replays the last change across buffers.- Plugin:
src/vim/table-nav-keymap.ts(per-handler state,resetPending),src/vim/table-nav-controller.ts(holds its own handler)
- Plugin:
- Picker source timeouts leaked a live timer per call —
Promise.racesettles but does not cancel the loser, so everyitems(),search()andpreview()call against an external source left a 5-second timer holding its closure.search()runs on every keystroke. The timer is now cleared whichever side wins.- Plugin:
src/picker/api.ts(withTimeout)
- Plugin:
- Visual-line pending selection lost its expiry in split panes — the selection was stored per
EditorViewin aWeakMapbut guarded by a single module-scope TTL timer, so the second view to store a selection cleared the first view's timer and that entry never expired. The timer is now stored beside the selection it expires.- Plugin:
src/vim/visual-line-command-fix.ts
- Plugin:
- Treesitter-backed Markdown text objects — validate exact opening and closing delimiter runs and continue to enclosing nodes when a candidate does not match. This fixes nested strikethrough ranges, single/double dollar confusion, and asterisk/underscore aliasing. Inner objects reject cursor positions on their delimiters. Blockquotes exclude lazy continuation lines below the cursor's quote depth and reuse depth-aware prefix and newline handling.
- Plugin:
src/text-objects/delimiter.ts,src/text-objects/blockquote.ts,src/treesitter/js-api.ts,src/treesitter/runtime.ts
- Plugin:
- Treesitter-backed fold metadata — heading fold ranges and heading/code placeholder labels now read immutable plain data keyed by exact editor state, replacing unreachable tree-field lookups. Selection-only states retain metadata; heading-like lines inside fences do not trigger the regex heading fallback when metadata is present. Column-zero exclusive section ends exclude the following same-level heading, with trailing blank lines trimmed and nested sections retained. Frontmatter/callout precedence and placeholder formats are unchanged.
- Plugin:
src/fold/metadata.ts(new extraction and state cache),src/treesitter/bridge.ts(publication),src/fold/provider.ts,src/fold/placeholder.ts(metadata consumers),src/treesitter/tree-state.ts(removed obsolete tree field/effect) - The CM6 bridge is now actually installed.
createBridgeExtensionhad no caller anywhere, so the per-view incremental-parsingViewPluginhad never run — treesitter worked only becausejs-api.tsand the Luavim.treesitterAPI parse on demand.main.tsinstalls it throughenableTreesitterBridge()after the Markdown grammars load, via a mutable extension slot andworkspace.updateOptions(). JS syntax-aware consumers keep their existing fallbacks for the window before it is available, and the Lua API keeps its own parser cache.- Plugin:
src/main.ts(enableTreesitterBridge),src/treesitter/bridge.ts(createBridgeExtension)
- Plugin:
- Plugin:
vim.bowrites were silently discarded —setBufferOptionwas an empty function, so everyvim.bo.x = yassignment did nothing and the next read returned the computed default. Writes now round-trip through a per-file shadow store, andexpandtab,shiftwidth,softtabstop,tabstop, andtextwidthare forwarded to the vim engine. Plugins that save and restore a buffer-local option around an operation now observe their own value.- Plugin:
src/lua/api.ts(readBufferOption/writeBufferOptionshadow store),src/lua/loader.ts(ENGINE_BACKED_BUFFER_OPTIONSforwarding)
- Plugin:
- Guarded
nvim__redrawprobes crashed instead of degrading — thevim.apidispatch metatable raises on property read for unregistered names, so flash.nvim'sif vim.api.nvim__redraw thenand leap.nvim'spcall(vim.api.nvim__redraw, ...)both errored at the guard itself rather than falling back. Added a third dispatch tier,ABSENT_NVIM_API_FUNCTIONS, whose members read asnil: not raising (which crashes the probe) and not a warn-once stub (which is truthy, so flash would take the branch meant for hosts that have the API and silently lose the cursor highlight itselsebranch draws vianvim_buf_set_extmark).nilis also what leap'spcallexpects on a Neovim build without the API.- Plugin:
src/lua/api.ts(ABSENT_NVIM_API_FUNCTIONS, dispatch metatable)
- Plugin:
- Command-line, history, and mapping probes raised instead of degrading —
getcmdline,setcmdline,getcmdpos,getcmdwintype,wildmenumode,complete_info,histadd,histdel, andmapsetwere unregistered, so calls raised a Lua error. Registered as warn-once stubs. For the read-only probes the placeholder is exactly what Neovim returns when no command line, wildmenu, or completion popup is active.- Plugin:
src/lua/fn.ts(stub sets, newvoidReturnFnsset formapset)
- Plugin:
- Lua iterator pipelines — real
vim.iterfor list-like tables, map-like tables, iterator functions, and callable tables, with 26 methods.rpop,count, andsizeare extensions beyond Neovim 0.12;size()requires a list source and raises on function sources.- Plugin:
src/lua/iter.ts(embedded Lua implementation),src/lua/loader.ts(inject after namespace stubs)
- Plugin:
- Physical key observation —
vim.on_key(fn, ns?)registers, replaces, and removes namespace-scoped callbacks and returns the namespace ID. Observation is pre-mapping, not Neovim's post-mapping hook: both arguments contain the same physical input, mapped expansions/programmaticfeedkeysare not separately observed, and return values cannot discard keys.- Plugin:
src/lua/on-key.ts(registry, guarded dispatch, teardown),src/workspace/key-observer.ts(physical-key observation) - Plugin:
src/workspace/global-key-handler.ts(desktop/popout dispatch),src/main.ts(mobile dispatch through the existing safety handler) - Plugin:
src/lua/api.ts,src/lua/loader.ts(registration/wiring),src/lua/engine.ts(cleanup before Lua close),src/lua/stdlib.ts(remove old no-op shim)
- Plugin:
- Current-editor compatibility APIs —
vim.fn.getwininfo([winid])returns CM6 visible lines (1-based inclusive), viewport dimensions, and gutter offset; non-zero handles or no editor return an empty list.nvim_win_call(0, fn)/nvim_buf_call(0, fn)invoke directly with return/error propagation, andnvim_win_get_config(0)reports a non-floating window (relative = ''). Authoritative registration totals are 60 realvim.apiimplementations and 79 realvim.fnimplementations, excluding stubs and correcting earlier documentation counts.- Plugin:
src/lua/window-info.ts,src/lua/fn.ts(viewport geometry and registration),src/lua/loader.ts(adapter callback),src/lua/api.ts(current-handle APIs)
- Plugin:
- Named Treesitter query files — resolve
query.set()overrides, userlua/queries/{lang}/{name}.scm, lexically ordered pluginlua/{plugin}/queries/{lang}/{name}.scm, then bundled Markdown/Markdown inline/HTMLtextobjects. Supports;; extends, recursive;; inherits:with optional(language)syntax, cycle detection, lazy compilation, and cache invalidation.query.get_files()reports vault-relative physical paths only, omitting bundled constants.- Plugin:
src/treesitter/bundled-queries.ts,src/treesitter/query-files.ts,src/treesitter/named-queries.ts(bundled queries, file snapshot, resolution and limits) - Plugin:
src/lua/treesitter/api.ts,src/lua/treesitter/query-api.ts,src/lua/loader.ts(query APIs and awaited runtime/query preloading before user config)
- Plugin:
- Massive Lua API expansion (~260 Neovim API functions) — 65 new functions across
vim.fn,vim.api,vim.validate,vim.version, and the extmark system. Enables Lua ports of mini.surround, mini.ai, leap.nvim, flash.nvim, and nvim-surround.- Plugin:
src/lua/fn.ts(12 newvim.fn.*functions) - Plugin:
src/lua/api.ts(16 newnvim_*API functions) - Plugin:
src/lua/stdlib.ts(upgradedvim.validate,vim.keycode,vim.notify_once,vim.versionnamespace with 11 functions, ~30 previously stubbed utilities now real) - Plugin:
src/lua/loader.ts(callback wiring for all new functions) - Plugin:
src/lua/namespace-stubs.ts(removed'version'— now real implementation) - Plugin:
src/lua/extmarks.ts(new — Neovim extmark system: StateField, registry, effects, VirtualTextWidget, position tracking, query APIs) - Plugin:
src/ui/input-modal.ts(new — Obsidian Modal forvim.fn.input()prompt) - Plugin:
src/main.ts(registered extmark extension) - Styles:
styles.css(.vim-motions-input-modal-inputstyles)
- Plugin:
- 12 new
vim.fn.*functions:visualmode,winsaveview/winrestview,foldclosed/foldclosedend,shiftwidth,strdisplaywidth,strcharpart,maparg,getcharstr/getchar(async key input waiting),searchpos(regex buffer search),input(async user prompt via modal) - 16 new
nvim_*API functions:nvim_get_mode,nvim_strwidth,nvim_buf_is_valid,nvim_buf_get_text,nvim_del_current_line,nvim_list_wins,nvim_buf_get_keymap,nvim_get_vvar/nvim_set_vvar,nvim_get_option_value/nvim_set_option_value,nvim_buf_set_extmark,nvim_buf_get_extmarks,nvim_buf_get_extmark_by_id,nvim_buf_del_extmark,nvim_buf_clear_namespace vim.versionnamespace — 11 functions:vim.version()returns plugin version as{major, minor, patch},vim.version.parse(),vim.version.cmp(),vim.version.lt()/gt()/eq(),vim.version.range()withhas(),vim.version.last(), plus__tostring/__eq/__ltmetamethods on version objectsvim.validate()full Neovim spec — both old table form (vim.validate({ name = { value, "string" } })) and new positional form (vim.validate("name", value, "string")) withoptionalflag support (Neovim 0.11+ compatible)vim.keycode()key code translation — translates Neovim key notation to readable strings (e.g.,vim.keycode("<CR>")→"\r",vim.keycode("<Esc>")→ escape character)vim.notify_once()deduplication — shows each unique message only once per session- Extmark system —
nvim_buf_set_extmark(virtual text withvirt_text,virt_text_pos,hl_group,sign_text,priority),nvim_buf_get_extmarks(range queries and full-buffer listing),nvim_buf_get_extmark_by_id(single extmark lookup),nvim_buf_del_extmark(removal),nvim_buf_clear_namespace(bulk clearing). Enables flash.nvim, leap.nvim, and nvim-surround Lua ports. vim.fn.getcharstr()— async key input waiting that yields the Lua coroutine until a key is pressed. Enables interactive Lua plugins (mini.surround, mini.ai, leap.nvim)vim.fn.searchpos()— regex buffer search returning{line, col}position. Enables flash.nvim, nvim-surround, and leap.nvim Lua portsvim.fn.input()— async user input prompt via Obsidian modal. Enables nvim-surround and other interactive Lua pluginsoperatorfuncoption routes — direct Lua functions, function-name strings, andnilclearing now share handling throughvim.opt,vim.o,vim.go,nvim_get/set_option, andnvim_get/set_option_value. The previously advertisedvim.o.operatorfuncpath now works with the fork'sg@operator.- Plugin:
src/lua/api.ts(shared operator callback read/write helpers)
- Plugin:
- Termcode identity-function bug —
nvim_replace_termcodesemits real Neovim key bytes (<CR>becomes"\r"), andnvim_feedkeysdecodes them back to notation at the fork boundary. Binary termcodes also work in mappings and expression callback results.- Plugin:
src/lua/termcodes.ts(byte encoder/decoder),src/lua/api.ts(key API integration)
- Plugin:
- Query iterator source and row arguments —
iter_captures/iter_matchesnow use supplied or node-retained document text and read row bounds after the source argument, fixing predicates in file-loaded queries.- Plugin:
src/lua/treesitter/query-api.ts(source selection and argument positions)
- Plugin:
- Jump list stalled on unresolvable entries —
<C-o>/<C-i>now skip history entries whose file no longer resolves and land on the nearest valid entry in the direction of travel. Previously the history index advanced before path resolution was checked, so a dead entry aborted navigation and left the cursor on an unrelated file; an exhausted run of dead entries could also fall through to the fork's separate within-buffer history. Dead entries no longer consume the count, the scan is bounded by history length, and the index is left unchanged when no valid destination exists. Navigation skips rather than prunes;vault.on('delete')continues to prune eagerly. Affects any persisted history, regardless of whether the entry was recorded bygd, the picker, harpoon, oil, or hint mode.- Plugin:
src/vim/jumplist.ts(validity predicate, bounded scan, count semantics),src/workspace/global-defaults.ts(resolve before advancing the index)
- Plugin:
- Unhandled rejection on failed jump navigation —
openJumpEntry()was invoked as a bare floating promise, so a rejectingleaf.openFile()produced an unhandled rejection while the history index had already advanced, leaving navigation silently failed with nothing surfaced. Now caught and logged. Skipping past unresolvable entries widened this path's reachability, and its narrow time-of-check/time-of-use window is exactly the deleted-file case.- Plugin:
src/workspace/global-defaults.ts(rejection handling on the jump opener)
- Plugin:
Tests
- 4 e2e cases in
test/specs/animated-cursor-runtime-toggle.e2e.ts. Three failed on the unfixed build with no canvas ever created. The load-bearing one is the third: it stashes the canvas element, reloads an unrelated setting, and asserts the element is the same object — the manager drops its canvas when the last controller deregisters, so surviving element identity is evidence thatupdateOptions()left existing ViewPlugin instances alone rather than rebuilding them. The fourth extends that acrossenableUndoTree,enableSnippetsandsnippetTriggerModeto show the slots are independent. The first case's precondition, that no canvas exists while the setting is off, passes either way and keeps the rest attributable to the toggle. - 2 e2e cases in
test/specs/cursor-shapes-runtime.e2e.ts. The animated-cursor case failed on the unfixed build with the painted cursor still 20 px tall where an underline is 2. The fork case is explicitly a control: it passes either way, because that path was already correct, and it is labelled so nobody reads it as a reproduction. Neither can usesetPluginSettingAndReload— that helper assignssettings[key], so a dotted key becomes a flat property of that literal name and the realcursorShapesobject is never touched. The first version of this spec did exactly that and reported the fix as not working. spike-mutable-array-destroyfixed, after its own diagnostics identified the cause. The Windows-only failure was the spike asserting a guaranteeregisterEditorExtensionnever made: the keydown fired from anembedded:table-widgeteditor, which still had the plugin because embedded editors are handed extensions at construction and are not reconfigured byworkspace.updateOptions(). Windows CI had a table cell editor open where Linux did not. It now counts only editors that mechanism governs and focuses the leaf editor explicitly instead of clicking the first.cm-editorin the document. Not a regression — it failed identically onf1b5626.- The vim-toggle observer assertion now sums across every reconfigurable editor instead of the active one, and carries an editor inventory into its failure message. The first version sampled only the active editor — the same blind spot that made the Windows spike failure unreadable, reproduced one commit later. Windows runs with two editors and Linux with one, so a leak confined to the second editor would have been reported as clean.
- 1 e2e case in
test/specs/vim-toggle.e2e.tsasserting the fork's CM6 keydown observer is removed on disable and not duplicated on re-enable, across two cycles. Added while investigating the pre-existing Windows failure inspike-mutable-array-destroy, which shows aViewPluginbeing destroyed while its observer still fires — the same shape as the production vim extension. On Linux the live observer count goes 1 to 0 to 1 with no growth; skipping theupdateOptions()indisableVim()makes the assertion reportoff:1 on:1 off:1 on:1. The spike itself now reports live observer, plugin and editor counts in its failure message, so the next Windows run distinguishes "the state still lists the plugin" from "the key reached a different view". - 3 e2e cases in
test/specs/runtime-extension-toggles.e2e.tsclose the gap left by the slot work, which had only shown the slots do not disturb each other. Discrimination was established by sabotage — reducingrefreshRuntimeExtensionSlots()to the animated-cursor slot alone, leaving setup intact, which reproduces the original defect. Two failed:snippetTriggerModekept expanding on Tab after switching to completion only, and the undo tree recorded 3 more nodes while disabled. The third passed under sabotage and is labelled a control —createSnippetTabKeymapre-readsenableSnippetson every keypress, so that trigger already self-guarded. test/specs/animated-cursor.e2e.tsstrengthened. It was the spec that let #181 live: it enabled the cursor and then asserted setting values and caret positions, never that anything rendered, so it passed on a build where the extension was never installed and no canvas existed. It now reads pixels off the canvas and checks the painted cursor sits within 4 px ofcoordsAtPos, plus a case asserting nothing is painted while the feature is off. Verified by sabotage: with the extension never installed the movement case fails, while the three settings-only cases still pass — which is precisely the blind spot. Compared in absolute viewport coordinates rather than as a delta, because the canvas is shared between tests and an earlier sample can be paint left behind by another one.- 1 unit case in
test/unit/animated-cursor.test.tsfor the manager singleton:peekreturns null after teardown wheregetwould build a replacement.CursorControlleris not exported, so this covers the contract the call site depends on rather than the call site itself. - 7 e2e cases in
test/specs/vim-builtin/text-objects-builtin.e2e.tsfor #178, written before the fix. Five failed on the unfixed code (di{,di(,di[,dibbefore the pair, anddi(finding a pair on a later line — each returning the buffer unchanged). Two are negative controls that pass on the unfixed code:da{before the pair, proving theavariant's forward search was already there, anddi(positioned after the only pair, proving the fix does not search backwards. Expected values recorded againstnvim --clean. - 4 e2e cases in
test/specs/surround.e2e.tsfor #179, written before the fix and all four observed failing with the buffer unchanged (ysi$b,ysa$b,ysi=b, andysi$bfollowed by.). The fourth covers the dot-repeat call site, which resolved text objects through the same built-in-only path. TheysaBbcase intest/specs/vim-builtin/surround-golden.e2e.tsis the control for the shadowing rule: it failed with the first version of the fix, which let the registered blockquoteaBwin unconditionally over the built-in{}block, and drove the built-in fallback. - 5 e2e cases in
test/specs/lua-keymap-modes.e2e.tsfor #180, written before the fix. The visual case failed withline1 instead of 0 and the operator-pending case deleted both lines (Received: ""instead of"hell world\nsecond line"); both pass with the fix. Two of the five are negative controls that pass on the unfixed code — normal mode with"", proving the mapping loaded at all, and an explicit"n"mapping still leaving visual-modejas the default down motion, proving the expansion did not leak into mode-specific mappings. - A vacuous assertion in
test/unit/fengari/53bit-integers.test.ts, found byno-useless-escapeoncetest/began being linted. The Lua lives in a JS template literal, so[[\"]]collapsed to[["]]before Lua ever saw it andq:findsearched for a bare"— whichstring.format("%q", …)always adds as wrapping quotes. The assertion that%qescapes inner quotes had never run. Confirmed by sabotage (assertion failed!at Lua line 24), then restored. - The
expect-expectandno-floating-promisefixes were verified across 31 e2e specs (31/31 passing, 18m23s) and the full unit suite. 60 of the repaired unit assertions were individually inverted and observed failing before restoration. - 16 real-WASM regression cases in
test/unit/treesitter/text-object-ranges.test.tscover delimiter identity/width, enclosing same-type candidates, all four bold delimiter positions, and blockquote depth/newline boundaries. All 16 were observed failing with candidate filtering and the blockquote fix reverted, includingtrikinstead ofstrikeand unwantedafter/more outerselection. - 27 real-WASM unit cases in
test/unit/fold/metadata.test.ts(23 extraction, provider, placeholder and state-identity cases) andtest/unit/fold/bridge-metadata.test.ts(4 bridge lifecycle cases). Removing the column-zero exclusive-end adjustment was confirmed to fail the same-level-heading regression (fold end 21 instead of 12), then restored. - 7 unit cases in
test/unit/util/key-capture.test.tscovering release on settle, release on abort, abort idempotency, a key arriving after abort, non-interference between two captures, and lease balance across all four exit orderings. The sixwaitForKeycases intest/unit/easymotion-keypress.test.tswere updated for the{ promise, abort }shape. - 20 unit cases for the three async-prerequisite fixes:
test/unit/lua/vim-v-context.test.ts(6, including a negative control that reproduces the old clear-instead-of-restore behaviour),test/unit/lua/key-broker.test.ts(8, covering single-listener ownership, FIFO delivery, abort idempotency and lease release), and 4 added totest/unit/lua/coroutine-runner.test.tsfor abandonment release on destroy, timeout, and pre-registration rejection. 3 e2e cases intest/specs/lua-async-prereq.e2e.ts, driven throughvim.schedulebecause it is already async-capable. Thegetcharstrcase was confirmed failing (Received: ""— every keystroke swallowed) with the abort propagation removed, then passing with it restored. - 4 e2e cases in
test/specs/lua-require.e2e.tsfor #177 (module beside a custominit.lua, dot-separated submodule, vault-root modules still resolving, and resolution from a synchronous keymap callback), written first and confirmed failing against the vault-root-only behaviour. 7 unit cases acrosstest/unit/lua/module-snapshot.test.ts(multi-root indexing, shared budget ordering, one unreadable root not sinking the others) andtest/unit/lua/package-require.test.ts(non-vault-root resolution, earlier-root precedence, fall-through, error naming every candidate). - 9 unit cases in
test/unit/lua/module-snapshot.test.ts(nested walks, dot-directory exclusion, each limit, unreadable files, atomic replacement, listing failure) and 8 intest/unit/lua/package-require.test.ts(snapshot hit bypasses the adapter, resolution from a synchronous caller,init.luafallback, nested submodule, the snapshot-naming miss message, caching across synchronous calls, adapter fallback still reached by an async-capable caller). The first of the 8 is a negative control asserting that without a snapshot the synchronous path still fails — if it ever passes, the other seven have stopped discriminating. - 1 e2e case in
test/specs/lua-require.e2e.tsfor the reload boundary: a module created at runtime is not requirable, the error names the snapshot and both candidate paths, and a configuration reload makes it resolve.test/specs/lua-plugin-flash-diagnostic.e2e.tshad its characterization assertions flipped — they asserted the async-requireblocker that this work removes. - 2 e2e cases in
test/specs/animated-cursor-scroll.e2e.tsfor #181, written before the fix and both observed failing. The tracking case failed with the painted cursor 0 px from its pre-scroll position against a caret that had moved 40 px; the phantom case never reached an empty canvas at all across a 20-sample poll, because a stale cursor keeps the dirty region non-null and so suppresses the manager's full-canvas clear. Assertions are made against pixels read back from the animated-cursor canvas within the editor's pane rectangle — the surface the bug report shows — not against settings or caret coordinates.- Both carry negative controls that pass on the unfixed code: a cursor is painted before the scroll (otherwise the whole measurement is vacuous), the scroll genuinely moves the caret on screen, the caret genuinely leaves the pane, and scrolling back restores the cursor within 3 px of
coordsAtPos()— the last one guards the newcachedRect = nullpath against killing the cursor outright. - Two measurement hazards were found empirically and are encoded in the test: the canvas blinks on a 1200 ms cycle, so a single sample can land in a blink-off frame and miss a phantom that is present; and unrelated transient remnants can sit on the shared canvas until the next full-canvas clear, so "painted right now" is not by itself evidence of a phantom. The phantom check therefore waits for an empty canvas first, then holds for more than one blink cycle.
- The spec cannot use
reloadFeatures()to enable the animated cursor the waytest/specs/animated-cursor.e2e.tsdoes:reloadFeatures()does not rebuild the editor-extension slot, so the canvasViewPluginis never installed and no canvas exists. It persists the settings and reloads the plugin instead. - The tracking case gained a painted-height assertion after the delta check was found insufficient. The block shape and the character inside it derive their screen positions independently, so a glyph stranded at the pre-scroll position still satisfies a comparison of bounding-box tops — it only extends the box downwards. Observed at 56 px of painted height against a 19 px caret line before the glyph fix.
- Both carry negative controls that pass on the unfixed code: a cursor is painted before the scroll (otherwise the whole measurement is vacuous), the scroll genuinely moves the caret on screen, the caret genuinely leaves the pane, and scrolling back restores the cursor within 3 px of
- CI pre-fetch gained
dirssupport and commit-SHA pinning, and flash.nvim is now vendored for the diagnostic spec (scripts/fetch-test-plugins.sh,test/fixtures/test-plugins.json). A missing file or directory now fails the script instead of warning; the fetch step runs before build on Linux, macOS and Windows, so a drifted path fails loudly.test/specs/lua-plugin-flash-diagnostic.e2e.tsskips its flash-dependent cases when the fixture is absent. - 11 e2e cases in
test/specs/lua-vim-ui.e2e.tsforvim.ui(overridability, E7 keymap-callback invocation, non-blocking return,inputcancel vs empty confirm,opencontract, reload-while-open teardown, and P1–P4 of the third-party override idiom viatest-vault/lua/uiselect_shim.lua); 4 unit cases intest/unit/picker/picker-cancel.test.ts; 12 intest/unit/lua/decoration-provider.test.ts; 6 intest/unit/lua/extmarks.test.ts; 4 intest/unit/lua/api-compat.test.ts. - 11 unit tests in
test/unit/lua/fn.test.ts(Unicode index conversion,wincol/winlayoutgeometry and fallbacks, plugin-facing stub degradation,nvim__redrawguard survival) andtest/unit/lua/api.test.ts(vim.bowrite round-trip,vim.wocallback/global-fallback/shadow resolution) - New unit suites:
test/unit/lua/api-compat.test.ts,iter.test.ts,on-key.test.ts,termcodes.test.ts,treesitter-queries.test.ts, andplugin-query-fetch.test.ts. Covers option routes, current handles, iterator semantics, observer lifecycle, byte conversion, real WASM query compilation, resolution/modelines, plugin isolation, cache lifecycle, and limits. - Four
getwininfocases added totest/unit/lua/fn.test.ts; correctedtest/unit/lua/api.test.tsto expect"\r", not"<CR>", fromnvim_replace_termcodes. - 20 unit tests in
test/unit/lua/extmarks.test.tsfor extmark engine (set, get, delete, clear, virtual text, position tracking, range queries) - 27 new tests in
test/unit/lua/stdlib.test.tsforvim.validate,vim.version,vim.keycode,vim.notify_once - 16 new tests in
test/unit/lua/fn.test.tsfor newvim.fn.*functions - 11 new tests in
test/unit/lua/api.test.tsfor newnvim_*API functions - 1 updated test in
test/unit/lua/highlight.test.tsfornvim_create_namespaceunique IDs - 22 unit cases in
test/unit/jumplist.test.tsfor unresolvable-entry traversal: both directions past one and several consecutive dead entries, all-dead history, index unchanged on exhaustion, peek consistency, count clamping, and large counts terminating in the current file. - Rewrote the deleted-file case in
test/specs/jump-list.e2e.ts. It previously opened fixtures viaobsidianPage.openFile(), which does not record a plugin jump, so the scenario was never established and the assertion resolved against history leaked from earlier specs — making it order-dependent and passing for the wrong reasons. It now clears inherited history, navigates withgd(which does record), asserts the exact[A, B]history before deleting, and deterministically verifies the entry is gone. New fixtures:test-vault/fixtures/jump-list/. - Current unit-test snapshot: 102 files, 1987 passed, 6 skipped.
Documentation
KNOWN_LIMITATIONS.md: the animated-cursor section records the scroll-tracking fix, why a stale rect could never clear itself, and the pane intersection testCONTRIBUTING.md:controller.tsdescription covers the scroll listener, the cached-rect drop when the caret leaves the pane, and the intersection testAGENTS.md,CONTRIBUTING.md: the runtime extension-slot mechanism and the rule that a setting gating an extension must be handled inrefreshRuntimeExtensionSlots()and appear in a reload path in both settings implementationsREADME.md: animated-cursor resilience list includes scroll trackingAGENTS.md: fork description records the inner-bracket forward pair search andrunTextObjectMotion's registered-object-then-built-in resolution order forysKNOWN_LIMITATIONS.md: new "Bracket text objects outside the pair" section; the surround parity section records thatysnow reaches registered text objects and how shadowed keys resolvedocs/features/surround.md:ysaccepts the plugin's Markdown text objectsdocs/features/text-objects.md: bracket objects find the next pair ahead of the cursordocs/configuration/lua-config.md: new "Mode strings" table forvim.keymap.set, documenting every accepted mode character and the""expansion, plus a mapping example using itdocs/features/text-objects.md,KNOWN_LIMITATIONS.md: delimiter cursor semantics and depth-aware blockquote boundaries also apply to tree-backed selections.AGENTS.md,CONTRIBUTING.md: fold metadata module and tree lifetime contract; corrected the stale pre-rewrite bridge description.docs/features/workspace-navigation.md: parsed heading boundaries, indented headings, fenced-code exclusion and regex fallback.CHANGELOG.mddocs/configuration/lua-config.md: corrected thevim.fn.getcharstr()example, which showed the call inside avim.keymap.setcallback — the one context that cannot yield, so the documented snippet always raised. Replaced with thevim.scheduleform, plus the ten-second bound and the one-keypress-one-waiter ruleKNOWN_LIMITATIONS.md:getcharstr/getcharentry records shared-broker delivery order and the ten-second await boundAGENTS.md,CONTRIBUTING.md:key-broker.tsadded to the Lua module trees;coroutine-runner.tsdescription notes the abandonment hookAGENTS.md:require()resolution rewritten — snapshot-first, async only for runner-managed threads, chunk-argument injection, refresh points;module-snapshot.tsadded to the Lua compatibility module treeCONTRIBUTING.md:module-snapshot.tsadded to the source tree;package.ts,coroutine-runner.ts, andloader.tsdescriptions updatedKNOWN_LIMITATIONS.md: new "Module snapshot andrequire()" section — the reload boundary, refresh points, the four resource limits and their reportingREADME.md: Lua bullet notes synchronousrequire()resolutiondocs/configuration/lua-config.md: new "require() resolves synchronously" section with the lazy-require example, the async-fallback boundary, and a callout for files added after load; new "Where modules are searched" section covering both roots, their precedence, and the desktop-only out-of-vault caseNEOVIM_API_STATUS.md: corrected registration totals (60/97/157 forvim.api, 84/46/130 forvim.fn), reclassified the "Unlisted API surface" table with verified plugin reachability (REQUIRED/OPTIONAL/GUARD), documented why an unregistered name is worse than a stub, corrected the stalevim.o.eventignore/selection/cmdheight/columns/cporows that contradicted the documented resolution order, and refreshed the "Next candidates" matrixAGENTS.md:vim.booption set, newvim.woscope,vim.fncountCONTRIBUTING.md:fn.tsdescription and countREADME.md:vim.fncountKNOWN_LIMITATIONS.md: reconciled two contradictoryvim.fncounts (65 and 79) against the authoritative registry totaldocs/configuration/lua-config.md: newvim.fnrows,vim.botable additions and write semantics, new "Window-local options (vim.wo)" section, registered-surface countsAGENTS.md: API counts (60/79), all eight compatibility modules, injection/teardown and query loading architecture, test coverage; retains prior extmark/API documentation.CONTRIBUTING.md: synchronized source tree, API counts, compatibility boundaries, and unit-test conventions.KNOWN_LIMITATIONS.md: corrected termcodes and.scmblocker, corrected the overstated mini.ai claim, documented pre-mapping observation, iterator extensions, query limits/reloads/re-fetching, and remaining Treesitter integration gaps; updated API counts/list; documented navigation-time skipping of unresolvable jump-list entries.README.md: corrected advertisedvim.apicount 59 → 60 andvim.fncount 77 → 79; expanded the Lua feature bullet and clarified working fork-backedoperatorfuncsupport.docs/configuration/lua-config.md: documented all nine compatibility items, query directory conventions and limits, option defaults, examples, and API counts alongside the earlier API expansion.docs/development/architecture.md: corrected API counts, new compatibility modules, initialization/cleanup ordering, and query loading architecture.
Full Changelog: 0.147.0...0.148.0