github saberzero1/motions 0.148.0

latest release: 0.149.0
4 hours ago

Added

  • Dead-code gatenpm run lint:deadcode (knip) is blocking in CI, and npm run verify now 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 TableEditor actions supersede them), unused treesitter JS-API and query-cache surface, snippet autocomplete helpers, and src/vim/jumplist-bridge.ts, whose createJumpListBridge() had no caller — the jump list works through the JumpList class instead.
    • knip.jsonc is JSONC specifically so every ignore entry carries its reason inline. The categories are: vendored fengari, dependencies selected by string name in wdio.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.
  • Test-quality gatetest/ was in ESLint's globalIgnores, so 346 files and 4,259 test blocks had never been linted at all. @vitest/eslint-plugin now covers test/unit/** and eslint-plugin-wdio covers test/specs/**, both blocking. It surfaced 166 findings, all fixed rather than suppressed — no eslint-disable was 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-expect ships off in the plugin's own recommended set and is forced on here.
    • 57 vitest/expect-expect, 13 no-conditional-expect, 10 no-identical-title, 7 valid-expect, 2 no-standalone-expect, plus one tautological self-comparison and one constant-folded null ?? {} that made a test a silent duplicate of the next one.
    • wdio/no-pause is off (2902 occurrences of an established browser.pause() idiom — a flakiness question, not a vacuity one), and the general lint backlog in test/ is switched off there and separately owned. Neither is suppressed for src.
    • expect-expect is syntactic and trusts any helper named in assertFunctionNames without inspecting it, so it proves a test asserts something, never that it asserts the right thing. luaExpectOk and run were added only after reading their definitions and a call site; run is scoped to test/unit/lua/** and test/unit/fengari/** because the name is generic enough that trusting it suite-wide would let any future function so named satisfy the rule.
  • negative-control skill (.agents/skills/negative-control/SKILL.md) — generalises the "the test MUST fail first" requirement from issue-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-repro keeps its own workflow and the two cross-reference each other.
  • require() resolves synchronously — every .lua file under lua/ is read into memory when the configuration loads, and require() resolves from that snapshot. This unblocks lazy require, 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 with module '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 plain lua_pcall that cannot yield. Only a cache miss was ever affected — package.loaded hits 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 require chunk as chunk arguments, not globals, so sandboxed user Lua has no handle on them.
    • Measured against the flash.nvim diagnostic: require_in_callback went from blocked: … async APIs … to works, and Config.get().search.multi_window from an error to true. flash now runs to its LuaJIT FFI dependency, which is architectural and not fixable in a pure-Lua VM.
  • vim.ui.select and vim.ui.input — Neovim's UI-hook namespace, backed by the existing Telescope-style picker and input modal. vim.ui is 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 a vim.keymap.set callback — the most common call site, where a yield-based design would hard-error. on_choice receives the original Lua value (items are commonly tables) plus a 1-based index; on_confirm distinguishes '' (empty confirm) from nil (cancel). format_item is applied eagerly, matching Neovim's own default implementation.
    • Plugin: src/lua/ui-api.ts (new), src/lua/loader.ts (injection + wiring), src/main.ts (openUiSelect picker binding)
    • Where no selection UI is available, vim.ui.select raises rather than settling with nil — a caller cannot distinguish a nil settle from "the user cancelled".
    • vim.ui.open(path, opts?) opens http(s):// targets in a new window and everything else with the system handler, returning Neovim's vim.SystemObj|nil, nil|string shape. On mobile, or with no handler, nil, errmsg is the correct answer rather than a fudge. opts.cmd is 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.
  • Picker reports cancellationPickerOptions.onCancel fires exactly once when the picker closes without a selection, and PickerModal.closeActive() closes a live picker. Needed by any caller that must distinguish "chose nothing" from "chose something", such as a Neovim-style vim.ui.select. confirmSelection closes the modal before dispatching the selection, so onClose runs first; a didConfirm flag set synchronously before close() keeps a successful selection from also reporting a cancel.
    • Plugin: src/picker/picker.ts (didConfirm, onClose cancel dispatch, closeActive), src/picker/types.ts (onCancel)
  • nvim_set_decoration_provider(ns, opts) — real implementation of Neovim's per-redraw decoration callbacks (on_starton_bufon_winon_end), backed by a CodeMirror ViewPlugin that coalesces work into one requestAnimationFrame per frame rather than dispatching from inside update() (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_win returning false skips 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_line and on_range raise a Lua error naming the unsupported key rather than being silently accepted; ephemeral extmarks 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.
  • Extmark hl_eol, strict, and priority orderingnvim_buf_set_extmark now parses hl_eol (extends the highlight to the end of the line containing the range end) and strict (out-of-range positions clamp instead of dropping the mark). Overlapping marks are ordered by priority, deterministically and independently of insertion order.
    • Plugin: src/lua/api.ts (opts parsing), src/lua/extmarks.ts (hlEol/strict in ExtmarkOpts, line-aware buildDecorations, priority-aware sort)
    • Known gap: priority orders decorations but does not yet decide which wins visually — CM6 marks carry no z-index and Decoration.set(..., true) re-sorts. Recorded in KNOWN_LIMITATIONS.md.
  • LuaJIT bit library — Neovim runs LuaJIT, which Neovim documents as its permanent plugin interface, so plugins reach for bit.band/bor/lshift rather 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, not 4294967295) and the distinction between logical rshift and arithmetic arshift. 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)
  • 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
  • nvim__redraw is a warn-once stub again, deliberately truthy — it briefly read as nil so that flash's if vim.api.nvim__redraw then probe would take its fallback. That was right for highlight.cursor, whose fallback is nvim_buf_set_extmark, but wrong for hacks.setcursor, whose fallback is LuaJIT FFI and which is called unguarded on every keystroke through Util.get_char. One name, two opposite correct answers; the truthy stub avoids throwing on the hot path, at the cost of highlight.cursor no longer drawing its cursor highlight. The nil-reading dispatch tier introduced for it has been removed rather than left with no members.
    • Plugin: src/lua/api.ts
  • Vim regex translationvim.fn.searchpos, vim.fn.split and vim.regex compiled their pattern with new RegExp, so Vim syntax silently matched nothing: \V and \C are identity escapes in JavaScript, making \Valpha\C a search for the literal ValC. A shared translator now handles magic levels (\v, \m, \M, \V), the case flags \c/\C, \zs/\ze as 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.md previously 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.
  • Indexed scope access: vim.bo[buf], vim.b[buf], vim.wo[win], vim.w[win], vim.t[tab] — Neovim allows both vim.bo.filetype and vim.bo[bufnr].filetype, and plugin code uses the indexed form freely. Our proxies accepted string keys only, so an indexed access resolved to nil and the caller failed with attempt to index a nil value. A numeric or nil key is now validated as handle 0 and returns the scope table. This closed the last of three blockers preventing flash.nvim from rendering: flash/cache.lua reads vim.bo[buf].filetype and vim.b[buf].changedtick, and with both fixed flash.state.new{...} completes and writes extmarks into the document.
    • Plugin: src/lua/api.ts (isScopeHandleKey, indexed branch on all five scope proxies)
  • nvim_list_bufs() and nvim_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 with nvim_list_wins() and the current-handle APIs. nvim_tabpage_list_wins validates its argument through a new requireTabpageZero guard. Measured impact: flash.nvim's Cache:_update_wins() overwrites state.wins with the filtered result of nvim_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 into SUPPORTED_NVIM_API_FUNCTIONS)
  • Unicode index conversion for vim.fnstrchars(s, skipcc?), charidx(s, byteidx, countcc?), and byteidx(s, nr) convert between UTF-8 byte offsets and Vim character indices. strchars counts composing marks separately unless skipcc is set; charidx and byteidx fold them into the preceding base character, matching Vim. All three are on flash.nvim's default label-positioning path.
    • Plugin: src/lua/fn.ts (buildCharSpans byte-span mapping, three registrations)
  • vim.fn.wincol() and vim.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 matches nvim_list_wins(). wincol is on leap.nvim's search path; winlayout is on flash.nvim's window-layout save path.
    • Plugin: src/lua/window-info.ts (getCursorWinCol), src/lua/fn.ts (registrations)
  • Window-local option scope (vim.wo) — replaces the warn-and-return-nil placeholder with a real proxy. wrap reports CodeMirror's line-wrapping state; writes shadow the resolved value; every other key falls back to the global scope, matching Neovim where an unset :setlocal value resolves to the global one. Required by leap.nvim's core search loop.
    • Plugin: src/lua/api.ts (readWindowOption, window-option shadow, vim.wo proxy), src/lua/loader.ts (getWindowOption callback)
  • vim.bo.iminsert and vim.bo.fileformat — buffer-local options read by flash.nvim and leap.nvim on every invocation, and by nvim-surround.
    • Plugin: src/lua/loader.ts (getBufferOption cases)

Changed

  • Global option fallbacksvim.o/vim.go resolve engine → shared shadow store → defaults (eventignore, selection, cmdheight, columns, lines, cpo, theme-derived background) → 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 returned Error objects for unknown fork options)
  • Plugin query retention — downloads retain .scm query files under isolated lua/{owner}__{repo}/queries/ roots and refresh the snapshot before Lua resumes. Older cached plugins must be re-fetched to acquire previously discarded queries; .scm edits 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)
  • nvim_create_namespace returns unique IDs — previously hardcoded to 0, 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 restartreloadFeatures() never touched vimExtensionSlot, which only setupVimSubsystems() populates, and that runs from onload() and enableVim() only. animatedCursor, enableSnippets, snippetTriggerMode and enableUndoTree were therefore restart-only, and enableUndoTree never reached a reload path at all. Re-running setupVimSubsystems() is not an option — it is a one-shot builder that registers global handlers and constructs managers, Lua and autocmd state — and teardownVimSubsystems() is far too destructive for a settings change. Each gated feature now owns a nested Extension[] that is pushed into vimExtensionSlot once and whose contents are swapped in place, followed by a single workspace.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 changing snippetTriggerMode leaves an in-progress snippet session intact. (#181)
    • Plugin: src/main.ts (setSlotEnabled, populateRuntimeSlots, refreshRuntimeExtensionSlots, five feature slots, extension builders extracted from setupVimSubsystems), src/settings.ts (enableUndoTree added to RELOAD_KEYS and to the imperative handler)
  • Cursor shape changes now reach the animated cursor without a restartcursorShapes has two independent consumers, and only one was broken. The fork reads state.vim.cursorShapes live on every render and already tracked settings changes at runtime. The animated cursor keeps its own copy, made by setCursorShapes(), which only setupVimSubsystems() 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-applies setCursorShapes)
  • 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 on running, 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 against cachedScrollTop, 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 (wakeRequested consumed by scheduleNext), 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.
  • Teardown no longer resurrects the animated-cursor managerteardownVimSubsystems() destroys the manager, but CodeMirror destroys the controllers only on the later updateOptions(), so CursorController.destroy() called getAnimatedCursorManager() 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 uses peekAnimatedCursorManager(), 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())

Fixed

  • di(/di{/di[ did nothing when the cursor sat before the pairda( searched forward for the next pair when the cursor was not already inside one; di( did not, because the fallback in textObjectManipulation was gated on inclusive. 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 on inclusive)
  • Surround ysi$/ysa$ and every other registered text object were droppedys stores its text object in a ys_motion sub-state and, on the next key, called the fork's built-in textObjectManipulation directly. 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 pending ysi vanished 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 by ys dot-repeat. (#179)
    • Fork: ~/Repos/codemirror-vim/src/vim.js (runTextObjectMotion, used by handleSurroundSubState's ys_motion branch and by repeatCommand)
  • vim.keymap.set("", lhs, rhs) only mapped normal mode — Neovim's :h map-modes defines the empty mode string as Normal + Visual + Select + Operator-pending, but getModeList() 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 pressed v or an operator. The empty string now expands to n, 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 both vim.keymap.set and vim.keymap.del
  • Animated cursor did not follow the text while scrolling, and left an uncleared phantom behindCursorController.update() already compared scrollDOM.scrollTop/scrollLeft against its cached values, but it only runs when CodeMirror produces a ViewUpdate, and scrolling inside the already-rendered viewport produces no transaction at all. The only recovery was the 500 ms staleness check in tick(), 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 pane coordsToRect() returned null and refreshTarget() returned early without clearing cachedRect, so tick() 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. A passive scroll listener on scrollDOM now 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 (onScroll listener registered in the constructor and removed in destroy(), refreshTarget() clears cachedRect/cachedShapeRect, coordsToRect() vertical intersection test)
  • 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() caches charTop/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: fillRect drew the block at the new screen position while fillText painted the letter at the old one. Because the frame reports only the block's rect as dirty, the stranded letter fell outside every subsequent clearRect and 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)
  • Table navigation shared its pending-key state across split panespendingD and the count buffer lived at module scope while TableNavController is a CodeMirror ViewPlugin, instantiated once per EditorView. Pressing d in one pane and then a key in another consumed the first pane's pending state, so d followed by d in a different table deleted a row immediately instead of starting its own dd. 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)
  • Picker source timeouts leaked a live timer per callPromise.race settles but does not cancel the loser, so every items(), search() and preview() 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)
  • Visual-line pending selection lost its expiry in split panes — the selection was stored per EditorView in a WeakMap but 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
  • 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
  • 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. createBridgeExtension had no caller anywhere, so the per-view incremental-parsing ViewPlugin had never run — treesitter worked only because js-api.ts and the Lua vim.treesitter API parse on demand. main.ts installs it through enableTreesitterBridge() after the Markdown grammars load, via a mutable extension slot and workspace.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)
  • vim.bo writes were silently discardedsetBufferOption was an empty function, so every vim.bo.x = y assignment did nothing and the next read returned the computed default. Writes now round-trip through a per-file shadow store, and expandtab, shiftwidth, softtabstop, tabstop, and textwidth are 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/writeBufferOption shadow store), src/lua/loader.ts (ENGINE_BACKED_BUFFER_OPTIONS forwarding)
  • Guarded nvim__redraw probes crashed instead of degrading — the vim.api dispatch metatable raises on property read for unregistered names, so flash.nvim's if vim.api.nvim__redraw then and leap.nvim's pcall(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 as nil: 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 its else branch draws via nvim_buf_set_extmark). nil is also what leap's pcall expects on a Neovim build without the API.
    • Plugin: src/lua/api.ts (ABSENT_NVIM_API_FUNCTIONS, dispatch metatable)
  • Command-line, history, and mapping probes raised instead of degradinggetcmdline, setcmdline, getcmdpos, getcmdwintype, wildmenumode, complete_info, histadd, histdel, and mapset were 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, new voidReturnFns set for mapset)
  • Lua iterator pipelines — real vim.iter for list-like tables, map-like tables, iterator functions, and callable tables, with 26 methods. rpop, count, and size are 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)
  • Physical key observationvim.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/programmatic feedkeys are 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)
  • Current-editor compatibility APIsvim.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, and nvim_win_get_config(0) reports a non-floating window (relative = ''). Authoritative registration totals are 60 real vim.api implementations and 79 real vim.fn implementations, 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)
  • Named Treesitter query files — resolve query.set() overrides, user lua/queries/{lang}/{name}.scm, lexically ordered plugin lua/{plugin}/queries/{lang}/{name}.scm, then bundled Markdown/Markdown inline/HTML textobjects. 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)
  • 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 new vim.fn.* functions)
    • Plugin: src/lua/api.ts (16 new nvim_* API functions)
    • Plugin: src/lua/stdlib.ts (upgraded vim.validate, vim.keycode, vim.notify_once, vim.version namespace 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 for vim.fn.input() prompt)
    • Plugin: src/main.ts (registered extmark extension)
    • Styles: styles.css (.vim-motions-input-modal-input styles)
  • 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.version namespace — 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() with has(), vim.version.last(), plus __tostring/__eq/__lt metamethods on version objects
  • vim.validate() full Neovim spec — both old table form (vim.validate({ name = { value, "string" } })) and new positional form (vim.validate("name", value, "string")) with optional flag 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 systemnvim_buf_set_extmark (virtual text with virt_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 ports
  • vim.fn.input() — async user input prompt via Obsidian modal. Enables nvim-surround and other interactive Lua plugins
  • operatorfunc option routes — direct Lua functions, function-name strings, and nil clearing now share handling through vim.opt, vim.o, vim.go, nvim_get/set_option, and nvim_get/set_option_value. The previously advertised vim.o.operatorfunc path now works with the fork's g@ operator.
    • Plugin: src/lua/api.ts (shared operator callback read/write helpers)
  • Termcode identity-function bugnvim_replace_termcodes emits real Neovim key bytes (<CR> becomes "\r"), and nvim_feedkeys decodes 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)
  • Query iterator source and row argumentsiter_captures/iter_matches now 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)
  • 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 by gd, 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)
  • Unhandled rejection on failed jump navigationopenJumpEntry() was invoked as a bare floating promise, so a rejecting leaf.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)

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 that updateOptions() left existing ViewPlugin instances alone rather than rebuilding them. The fourth extends that across enableUndoTree, enableSnippets and snippetTriggerMode to 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 use setPluginSettingAndReload — that helper assigns settings[key], so a dotted key becomes a flat property of that literal name and the real cursorShapes object is never touched. The first version of this spec did exactly that and reported the fix as not working.
  • spike-mutable-array-destroy fixed, after its own diagnostics identified the cause. The Windows-only failure was the spike asserting a guarantee registerEditorExtension never made: the keydown fired from an embedded:table-widget editor, which still had the plugin because embedded editors are handed extensions at construction and are not reconfigured by workspace.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-editor in the document. Not a regression — it failed identically on f1b5626.
  • 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.ts asserting 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 in spike-mutable-array-destroy, which shows a ViewPlugin being 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 the updateOptions() in disableVim() makes the assertion report off: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.ts close the gap left by the slot work, which had only shown the slots do not disturb each other. Discrimination was established by sabotage — reducing refreshRuntimeExtensionSlots() to the animated-cursor slot alone, leaving setup intact, which reproduces the original defect. Two failed: snippetTriggerMode kept 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 — createSnippetTabKeymap re-reads enableSnippets on every keypress, so that trigger already self-guarded.
  • test/specs/animated-cursor.e2e.ts strengthened. 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 of coordsAtPos, 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.ts for the manager singleton: peek returns null after teardown where get would build a replacement. CursorController is 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.ts for #178, written before the fix. Five failed on the unfixed code (di{, di(, di[, dib before the pair, and di( 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 the a variant's forward search was already there, and di( positioned after the only pair, proving the fix does not search backwards. Expected values recorded against nvim --clean.
  • 4 e2e cases in test/specs/surround.e2e.ts for #179, written before the fix and all four observed failing with the buffer unchanged (ysi$b, ysa$b, ysi=b, and ysi$b followed by .). The fourth covers the dot-repeat call site, which resolved text objects through the same built-in-only path. The ysaBb case in test/specs/vim-builtin/surround-golden.e2e.ts is the control for the shadowing rule: it failed with the first version of the fix, which let the registered blockquote aB win unconditionally over the built-in {} block, and drove the built-in fallback.
  • 5 e2e cases in test/specs/lua-keymap-modes.e2e.ts for #180, written before the fix. The visual case failed with line 1 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-mode j as 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 by no-useless-escape once test/ began being linted. The Lua lives in a JS template literal, so [[\"]] collapsed to [["]] before Lua ever saw it and q:find searched for a bare " — which string.format("%q", …) always adds as wrapping quotes. The assertion that %q escapes inner quotes had never run. Confirmed by sabotage (assertion failed! at Lua line 24), then restored.
  • The expect-expect and no-floating-promise fixes 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.ts cover 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, including trik instead of strike and unwanted after / more outer selection.
  • 27 real-WASM unit cases in test/unit/fold/metadata.test.ts (23 extraction, provider, placeholder and state-identity cases) and test/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.ts covering 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 six waitForKey cases in test/unit/easymotion-keypress.test.ts were 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 to test/unit/lua/coroutine-runner.test.ts for abandonment release on destroy, timeout, and pre-registration rejection. 3 e2e cases in test/specs/lua-async-prereq.e2e.ts, driven through vim.schedule because it is already async-capable. The getcharstr case 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.ts for #177 (module beside a custom init.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 across test/unit/lua/module-snapshot.test.ts (multi-root indexing, shared budget ordering, one unreadable root not sinking the others) and test/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 in test/unit/lua/package-require.test.ts (snapshot hit bypasses the adapter, resolution from a synchronous caller, init.lua fallback, 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.ts for 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.ts had its characterization assertions flipped — they asserted the async-require blocker that this work removes.
  • 2 e2e cases in test/specs/animated-cursor-scroll.e2e.ts for #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 new cachedRect = null path 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 way test/specs/animated-cursor.e2e.ts does: reloadFeatures() does not rebuild the editor-extension slot, so the canvas ViewPlugin is 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.
  • CI pre-fetch gained dirs support 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.ts skips its flash-dependent cases when the fixture is absent.
  • 11 e2e cases in test/specs/lua-vim-ui.e2e.ts for vim.ui (overridability, E7 keymap-callback invocation, non-blocking return, input cancel vs empty confirm, open contract, reload-while-open teardown, and P1–P4 of the third-party override idiom via test-vault/lua/uiselect_shim.lua); 4 unit cases in test/unit/picker/picker-cancel.test.ts; 12 in test/unit/lua/decoration-provider.test.ts; 6 in test/unit/lua/extmarks.test.ts; 4 in test/unit/lua/api-compat.test.ts.
  • 11 unit tests in test/unit/lua/fn.test.ts (Unicode index conversion, wincol/winlayout geometry and fallbacks, plugin-facing stub degradation, nvim__redraw guard survival) and test/unit/lua/api.test.ts (vim.bo write round-trip, vim.wo callback/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, and plugin-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 getwininfo cases added to test/unit/lua/fn.test.ts; corrected test/unit/lua/api.test.ts to expect "\r", not "<CR>", from nvim_replace_termcodes.
  • 20 unit tests in test/unit/lua/extmarks.test.ts for extmark engine (set, get, delete, clear, virtual text, position tracking, range queries)
  • 27 new tests in test/unit/lua/stdlib.test.ts for vim.validate, vim.version, vim.keycode, vim.notify_once
  • 16 new tests in test/unit/lua/fn.test.ts for new vim.fn.* functions
  • 11 new tests in test/unit/lua/api.test.ts for new nvim_* API functions
  • 1 updated test in test/unit/lua/highlight.test.ts for nvim_create_namespace unique IDs
  • 22 unit cases in test/unit/jumplist.test.ts for 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 via obsidianPage.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 with gd (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 test
  • CONTRIBUTING.md: controller.ts description covers the scroll listener, the cached-rect drop when the caret leaves the pane, and the intersection test
  • AGENTS.md, CONTRIBUTING.md: the runtime extension-slot mechanism and the rule that a setting gating an extension must be handled in refreshRuntimeExtensionSlots() and appear in a reload path in both settings implementations
  • README.md: animated-cursor resilience list includes scroll tracking
  • AGENTS.md: fork description records the inner-bracket forward pair search and runTextObjectMotion's registered-object-then-built-in resolution order for ys
  • KNOWN_LIMITATIONS.md: new "Bracket text objects outside the pair" section; the surround parity section records that ys now reaches registered text objects and how shadowed keys resolve
  • docs/features/surround.md: ys accepts the plugin's Markdown text objects
  • docs/features/text-objects.md: bracket objects find the next pair ahead of the cursor
  • docs/configuration/lua-config.md: new "Mode strings" table for vim.keymap.set, documenting every accepted mode character and the "" expansion, plus a mapping example using it
  • docs/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.md
  • docs/configuration/lua-config.md: corrected the vim.fn.getcharstr() example, which showed the call inside a vim.keymap.set callback — the one context that cannot yield, so the documented snippet always raised. Replaced with the vim.schedule form, plus the ten-second bound and the one-keypress-one-waiter rule
  • KNOWN_LIMITATIONS.md: getcharstr/getchar entry records shared-broker delivery order and the ten-second await bound
  • AGENTS.md, CONTRIBUTING.md: key-broker.ts added to the Lua module trees; coroutine-runner.ts description notes the abandonment hook
  • AGENTS.md: require() resolution rewritten — snapshot-first, async only for runner-managed threads, chunk-argument injection, refresh points; module-snapshot.ts added to the Lua compatibility module tree
  • CONTRIBUTING.md: module-snapshot.ts added to the source tree; package.ts, coroutine-runner.ts, and loader.ts descriptions updated
  • KNOWN_LIMITATIONS.md: new "Module snapshot and require()" section — the reload boundary, refresh points, the four resource limits and their reporting
  • README.md: Lua bullet notes synchronous require() resolution
  • docs/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 case
  • NEOVIM_API_STATUS.md: corrected registration totals (60/97/157 for vim.api, 84/46/130 for vim.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 stale vim.o.eventignore/selection/cmdheight/columns/cpo rows that contradicted the documented resolution order, and refreshed the "Next candidates" matrix
  • AGENTS.md: vim.bo option set, new vim.wo scope, vim.fn count
  • CONTRIBUTING.md: fn.ts description and count
  • README.md: vim.fn count
  • KNOWN_LIMITATIONS.md: reconciled two contradictory vim.fn counts (65 and 79) against the authoritative registry total
  • docs/configuration/lua-config.md: new vim.fn rows, vim.bo table additions and write semantics, new "Window-local options (vim.wo)" section, registered-surface counts
  • AGENTS.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 .scm blocker, 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 advertised vim.api count 59 → 60 and vim.fn count 77 → 79; expanded the Lua feature bullet and clarified working fork-backed operatorfunc support.
  • 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

Don't miss a new motions release

NewReleases is sending notifications on new releases.