See individual releases on GitHub for per-version discussion and asset downloads.
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
[Unreleased]
[0.120.1] - 2026-08-21
Changed
- Removed
!importantfrom table-nav CSS — replacedoverflow: visible !importanton.vim-motions-table-nav-mode .cm-table-widgetwith higher-specificity selectors (.cm-editor .vim-motions-table-nav-mode.cm-table-widget). The added.cm-editorancestor provides enough specificity to override Obsidian’s built-inoverflow: hiddenwithout!important.- Styles:
styles.css(increased selector specificity, removed!important)
- Styles:
Documentation
CHANGELOG.mdAGENTS.md: added “Never use!importantin CSS” coding conventionCONTRIBUTING.md: added!importantavoidance guideline to code style section
[0.120.0] - 2026-08-21
Fixed
- Table cell cursor bounce-back on macOS (continued) — the
MessageChannel-basedscheduleCrossing()from 0.119.0 still raced with Obsidian’s table widget focus handlers on macOS Electron. Replaced withrequestAnimationFrame, which defers the cross-cell focus change until after the full event dispatch cycle and paint frame complete — guaranteeing Obsidian’s table widget keydown handlers have finished before the plugin changes cell focus. (#136)- Plugin:
src/vim/table-cell-motions.ts(scheduleCrossingnow usesrequestAnimationFrameinstead ofMessageChannel)
- Plugin:
- Table-nav viewport does not follow cursor in long tables — when navigating down through a table taller than the viewport with
enableTableNav=true, the highlighted cell went off-screen becausenavigate()only updated the CSS highlight class without scrolling. CM6 treats the native table widget as an opaque block decoration and cannot scroll to positions within it. Fixed by registering anEditorView.scrollHandlerfacet that intercepts scroll requests during table-nav mode, reads the highlighted cell’s DOM bounding rect, and adjustsscrollDOM.scrollTopdirectly — the CM6-sanctioned mechanism for custom scroll behavior that is not overridden by viewport reconciliation. Addedoverflow: visibleCSS override on the table widget during nav mode to prevent the widget’soverflow: auto hiddenfrom blockingscrollIntoViewpropagation. (#136)- Plugin:
src/vim/table-nav-controller.ts(syncCursorToActiveCell,tableNavScrollHandlerextension) - Styles:
styles.css(overflow: visibleon.vim-motions-table-nav-mode.cm-table-widget)
- Plugin:
Added
- Vim/Neovim built-in gap coverage — systematic effort to close ~50 gaps in vim/neovim built-in command coverage across 8 implementation batches.
- Fork:
@:repeat last ex command — newrepeatLastExCommandaction replays the most recent ex command. Added to defaultKeymap.- Fork:
~/Repos/codemirror-vim/src/vim.js(repeatLastExCommandaction + defaultKeymap entry)
- Fork:
- Fork:
&repeat last:son current line — newrepeatLastSubstituteaction re-executes the last:ssubstitution. Added to defaultKeymap.- Fork:
~/Repos/codemirror-vim/src/vim.js(repeatLastSubstituteaction + defaultKeymap entry)
- Fork:
- Fork:
ZZwrite+quit andZQquit without saving — mapped in defaultKeymap viaexArgs: { input: 'wq' }andexArgs: { input: 'q' }respectively.- Fork:
~/Repos/codemirror-vim/src/vim.js(defaultKeymap entries)
- Fork:
- Fork: Insert
<C-a>re-insert previously inserted text — newreinsertPreviousInsertaction replays the last insert-mode text at the cursor.- Fork:
~/Repos/codemirror-vim/src/vim.js(reinsertPreviousInsertaction)
- Fork:
- Fork: Insert
<C-e>copy character from line below — newcopySameColumnBelowaction copies the character at the same column from the line below.- Fork:
~/Repos/codemirror-vim/src/vim.js(copySameColumnBelowaction)
- Fork:
- Fork: Insert
<C-y>copy character from line above — newcopySameColumnAboveaction copies the character at the same column from the line above.- Fork:
~/Repos/codemirror-vim/src/vim.js(copySameColumnAboveaction)
- Fork:
<C-w>w/<C-w>Wcycle panes — cycle focus to the next or previous pane in the workspace.- Plugin:
src/workspace/navigation.ts
- Plugin:
<C-w>pfocus previous pane — jump to the previously accessed pane using leaf ID tracking.- Plugin:
src/workspace/navigation.ts - Plugin:
src/main.ts(previousLeafIdtracking extracted from Harpoon-gated handler into unconditional handler)
- Plugin:
gmgo to middle of screen line — positions cursor at the horizontal midpoint of the visible editor area.- Plugin:
src/workspace/navigation.ts
- Plugin:
gogo to character offset — jumps to the Nth byte offset in the buffer (with count prefix).- Plugin:
src/workspace/navigation.ts
- Plugin:
g8show UTF-8 byte sequence — displays the UTF-8 hex byte values for the character under the cursor.- Plugin:
src/workspace/navigation.ts
- Plugin:
gFgo to file with line number — opens the file path under the cursor, optionally jumping to a line number suffix (e.g.,file.md:42).- Plugin:
src/workspace/navigation.ts
- Plugin:
<C-g>show file info — displays filename, line count, cursor position, and percentage through file in a notice.- Plugin:
src/workspace/navigation.ts
- Plugin:
<C-^>/<C-6>alternate file switching — switch between the current and alternate (previously edited) file, matching Neovim’s<C-^>behavior.- Plugin:
src/main.ts(alternateFilePath/lastMarkdownFilePathfields +<C-^>/<C-6>mapping)
- Plugin:
<C-]>follow link under cursor — alias forgd(go to definition).- Plugin:
src/main.ts
- Plugin:
<C-t>pop from link follow — alias for jump list backward navigation.- Plugin:
src/main.ts
- Plugin:
zs/ze/zH/zLhorizontal scroll commands — scroll the viewport horizontally without moving the cursor.- Plugin:
src/workspace/navigation.ts
- Plugin:
- Ex commands for
:move,:copy, and:normal— add:m/:moveline moves,:t/:copy/:coline copies, and:normal/:normal!key dispatch from the ex line.- Plugin:
src/workspace/commands.ts(line transfer helpers +:normalkey feeding)
- Plugin:
:tabmoveno-op registration — registered as a no-op with a notice (Obsidian has no tab reorder API).- Plugin:
src/workspace/commands.ts
- Plugin:
- No-op crash guards — 21 commands registered as no-ops to prevent crashes on unrecognized keys: window commands (
<C-w>=,<C-w>_,<C-w>|,<C-w>r,<C-w>R,<C-w>x), spelling (]s,[s,z=,zg,zw), normalU,<C-l>,g<C-a>,g<C-x>, insert<C-r>=,<C-k>,<C-v>,<C-x>family.- Plugin:
src/workspace/navigation.ts
- Plugin:
Tests
- 1 new e2e spec file:
test/specs/table-nav-scroll.e2e.ts(viewport scrolling in long tables with constrained scroller height, #136) - 1 new fixture file:
test-vault/fixtures/table-nav/LongTable.md(30-row table for scroll testing) - Updated
test/neovim-command-index.yaml— 50 new entries (287→337 total, 313 tested + 21 skip + 3 pending). - 5 new e2e spec files:
test/specs/vim-builtin/new-commands.e2e.ts(11 tests:@:,&,ZZ,ZQ, insert<C-a>/<C-e>/<C-y>)test/specs/vim-builtin/link-nav-window-cycle.e2e.ts(13 tests:<C-^>,<C-]>,<C-t>,<C-w>w/W/p)test/specs/vim-builtin/ex-move-copy-normal.e2e.ts(19 tests::m,:t,:normal)test/specs/vim-builtin/minor-motions-scroll.e2e.ts(18 tests:gm,go,g8,gF,<C-g>,zs/ze/zH/zL)test/specs/vim-builtin/noop-commands.e2e.ts(10 tests: all no-op crash guards)
Documentation
CHANGELOG.mdAGENTS.md: updated fork description with new actions, updated test file organizationKNOWN_LIMITATIONS.md: added:m/:taddress parsing limitationdocs/reference/keybindings.md: added new normal-mode, insert-mode, window, g-prefix, z-prefix, and ex command entriesdocs/features/ex-commands.md: added:m/:t/:normalediting commands sectiondocs/features/workspace-navigation.md: added pane cycling, alternate file, link navigation, and document info sectionsCONTRIBUTING.md: updatedtable-cell-motions.tsandtable-nav-controller.tsdescriptionsKNOWN_LIMITATIONS.md: documented table-nav viewport scrolling fix
[0.119.0] - 2026-08-20
Fixed
- Table cell cursor bounce-back on macOS —
scheduleCrossing()in the cross-cell motion overrides usedsetTimeout(0)to defer cell focus changes after vim’s motion return. On macOS Electron,setTimeout(0)is subject to timer clamping (1–4ms minimum delay), during which Obsidian’s native table widget event handlers re-assert focus on the original cell — producing cursor bounce-back. Users withj→gj/k→gkremappings were especially affected because the remapping adds an extra key processing step. Fixed by replacingsetTimeout(0)withMessageChannelport messaging, which dispatches a macrotask that fires before timers in the browser event loop. Token-based deduplication ensures rapid key repeats coalesce correctly. (#136)- Plugin:
src/vim/table-cell-motions.ts(scheduleCrossingnow usesMessageChannelinstead ofsetTimeout(0))
- Plugin:
- Cross-cell motions broken when
enableTableNav=false— the 0.118.0 fix for #136 incorrectly gatedapplyTableCellMotions()onenableTableNav, which removed the motion overrides entirely when table nav was disabled. This brokej/k/h/lcross-cell navigation in native table cell editors. Reverted the gate — cross-cell motions are independent ofenableTableNav, as originally designed. (#136)- Plugin:
src/main.ts(removedenableTableNavcheck from bothapplyTableCellMotionsregistration sites)
- Plugin:
Tests
- Added native-mode cross-cell tests in
test/specs/table-nav-disabled.e2e.ts(#136): j cross row, k cross row, l cross cell, j exit table, no-overlay assertion, cell editor opens — using WebDriver$().click()for reliable cell entry andwaitUntilfor async assertions - Added raw-mode tests with
j→gj/k→gkremappings (j down, k up) - Updated
test/specs/table-cell-vim-mode.e2e.ts: cross-cell j/l tests usewaitUntilfor deterministic assertions - Updated
test/specs/table-cursor-suppression.e2e.ts: #127 tests useG/ggjumps instead of j/k table traversal; #135 tests use WebDriver cell click +waitUntilfor table-nav entry
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: reverted cross-cell motions documentation to reflect independence fromenableTableNavCONTRIBUTING.md: updatedtable-cell-motions.tsdescriptiondocs/features/tables.md: reverted table modes matrix and cell editor behavior descriptiondocs/configuration/settings.md: reverted Table navigation setting description
[0.118.0] - 2026-08-20
Fixed
- Table movement broken when
enableTableNav=false(macOS) —applyTableCellMotions()overrodemoveByLines,moveByCharacters, andmoveByDisplayLinesglobally whenevertableWidgetModewasnative, regardless ofenableTableNav. When table nav was disabled, these overrides still interceptedj/k(andgj/gk) inside native table cells, callingscheduleCrossing()withsetTimeout(0)which raced with Obsidian’s native cell focus management on macOS — producing cursor bounce-back. Users withj→gj/k→gkremappings (common vimrc/Lua pattern) were especially affected because the remapping routes throughmoveByDisplayLines. Fixed by gatingapplyTableCellMotions()onenableTableNav— when the user disables table nav, the motion overrides are not installed. Obsidian’s native table cell editor handles cross-cell navigation on its own. (#136)- Plugin:
src/main.ts(addedenableTableNavcheck to bothapplyTableCellMotionsregistration sites)
- Plugin:
Changed
- Cross-cell motions now respect
enableTableNav—h/j/k/lcrossing cell boundaries in native table mode was previously always active regardless ofenableTableNav. Cross-cell motions now only activate whenenableTableNavistrue. When disabled, Obsidian’s native table cell editor handles cell boundary navigation directly.
Tests
- Updated
test/specs/table-nav-disabled.e2e.ts(#136): replaced flaky native table cross-cell tests withenableTableNav=falseno-overlay assertion; added 2 raw mode tests withj→gj/k→gkremappings (j down, k up) - Updated
test/specs/table-cell-vim-mode.e2e.ts:enableTableNav=falsecross-cell tests now assert cursor stays within cell (no cross-cell override)
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: updated cross-cell motions documentation to reflectenableTableNavgating; updated table modes matrix; updatedtablewidgetmode descriptionCONTRIBUTING.md: updatedtable-cell-motions.tsdescription to reflectenableTableNavgatingdocs/features/tables.md: updated table modes matrix and cell editor behavior description for disabled table-navdocs/configuration/settings.md: updated Table navigation setting description
[0.117.0] - 2026-08-20
Fixed
- Note Composer “Extract current selection” does nothing in V-LINE mode — in visual-line mode, the codemirror-vim fork sets a cursor-only CM6 selection (to prevent Live Preview from uncollapsing hidden markup). This caused
editor.somethingSelected()to returnfalse, so commands that check for a selection silently failed. The existingexecuteCommandwrapper expanded the selection before command execution, but Obsidian’s command palette invokescheckCallback()directly on the command object, bypassingexecuteCommandentirely. Fixed by wrapping every command’scheckCallbackto expand the visual-line selection before the callback runs, and wrappingaddCommandto cover commands registered after plugin load. (#137)- Plugin:
src/vim/visual-line-command-fix.ts(wrapcheckCallbackon all commands +addCommandhook; extract sharedwithExpandedSelectionhelper)
- Plugin:
- Cursor flashing at previous cell during table navigation — when table navigation was enabled, the vim cursor layer remained visible at the previous cell position after entering the table and after navigating between cells with
h/j/k/l. Three root causes: (1)tryEnter()never dispatched theenterTableNavstate effect, soisTableNavActive()always returnedfalse— themainEditorTableCursorGuardcontinued running during table-nav and could clear cursor suppression. (2) The vim cursor layer (.cm-vimCursorLayer) on the main editor was not proactively cleared during navigation, allowing stale cursor elements to remain visible. (3)cellEditorCursorGuard.destroy()unconditionally calledclearCursorSuppressedForView()on the parent editor, undoing the controller’s suppression even while table-nav was active. Additionally, cell editors inside the table widget are destroyed and recreated during entry, each creating a freshBlockCursorPluginwith a visible cursor layer — the controller now suppresses these on every ViewUpdate viasuppressWidgetCursorLayers(). (#135)- Plugin:
src/vim/table-nav-controller.ts(dispatchenterTableNaveffect,clearVimCursorLayer()helper,suppressWidgetCursorLayers()on every update + entry + rAF safety net) - Plugin:
src/vim/table-cell-cursor-guard.ts(cellEditorCursorGuard.destroy()guards onisTableNavActive())
- Plugin:
Tests
- 3 regression tests in
test/specs/visual-line-command.e2e.ts(#137):note-composer:split-filecheckCallbackreturnstruein V-LINE,editor.somethingSelected()returnstruein V-LINE,executeCommandByIdaffects all selected lines in V-LINE - 3 regression tests in
test/specs/table-cursor-suppression.e2e.ts(#135): main editor cursor suppression during navigation, rapid multi-directional navigation, no visible cursor anywhere on initial entry - 3 regression tests in
test/specs/table-nav-disabled.e2e.ts(#136): cursor movement through raw tables withenableTableNav=false— j down, k up, j exits table
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: updated visual-line command passthrough description to reflectcheckCallbackwrapping for command palette path (#137); updated table navigation cursor hiding entry; added #136 cross-reference to #132 cursor disappearing fixCONTRIBUTING.md: updatedtable-nav-controller.tsandtable-cell-cursor-guard.tsdescriptions
[0.116.0] - 2026-08-18
Changed
- E2E CI: custom Docker runner image — the e2e workflow now runs each spec inside a custom container image (
ghcr.io/<repo>/e2e-runner:latest) with Xvfb, herbstluftwm, Node.js 24, and Electron system dependencies pre-installed. The entrypoint starts the virtual display with readiness polling (pollsxdpyinfoandherbstclientevery 200ms, fails after 6s) instead of the previoussleep 1race condition. Eliminates per-runnerapt-get update+apt-get install(~30s per shard × 79 shards). The image is built and pushed to GHCR by.github/workflows/docker-e2e-runner.ymlon Dockerfile changes or manual dispatch.- New:
.github/docker/e2e-runner/Dockerfile(Ubuntu 24.04 base, Xvfb, herbstluftwm, dzen2, x11-xserver-utils, Electron deps, Node.js 24) - New:
.github/docker/e2e-runner/entrypoint.sh(Xvfb + herbstluftwm readiness polling,exec "$@"handoff) - New:
.github/workflows/docker-e2e-runner.yml(build + push to GHCR on Dockerfile changes) - Changed:
.github/workflows/e2e.yml(usescontainer:with the custom image, removedSetup virtual displaystep andactions/setup-node, added npm cache viaactions/cache)
- New:
Documentation
CHANGELOG.mdAGENTS.md: Added CI container image documentation to Automated testing sectionCONTRIBUTING.md: Added CI infrastructure note to Running E2E tests section
[0.115.2] - 2026-08-18
[0.115.1] - 2026-08-18
Fixed
- CI issue with obsidian-workflows.
[0.115.0] - 2026-08-18
Added
- Fold navigation motions (
zj,zk,[z,]z) —zjmoves to the start of the next foldable region (skipping child folds within the current heading’s range, matching Neovim’s sibling-fold semantics).zkmoves to the end of the previous foldable region.[z/]znavigate to the start/end of the enclosing foldable region. All four support counts (3zj), operator-pending mode (dzj), and record to the jump list. Ex command aliases::foldnext,:foldprev,:foldstart,:foldend.- Plugin:
src/fold/motions.ts(NEW —findNextFoldable,findPrevFoldable,findEnclosingFoldable,foldedRangesWithin,foldableRegionsWithin,foldNext,foldPrev,foldStart,foldEnd) - Plugin:
src/fold/commands.ts(registered motions + ex commands)
- Plugin:
- Fold state commands (
zn,zN,zi,zv,zF,zx,zX) —zndisables folding (opens all folds, prevents new folds).zNre-enables folding.zitoggles.zvopens folds to reveal cursor line.zFcreates a fold for [count] lines.zx/zXreapply fold level (preserving manual folds). Configure viaset foldenable/set nofoldenablein vimrc orvim.opt.foldenablein Lua.- Plugin:
src/fold/fold-enable.ts(NEW —foldEnableFieldStateField,isFoldingEnabledguard,zn/zN/ziactions) - Plugin:
src/fold/commands.ts(zv,zFactions) - Plugin:
src/fold/fold-level.ts(zx,zXactions) - Plugin:
src/vim/options.ts(foldenablevim option)
- Plugin:
- Heading fold provider with trailing blank line trimming — custom
foldServiceprovider for Markdown headings that trims trailing blank lines from fold ranges, matching Neovim’s treesitter fold boundaries. Overrides Obsidian’s built-infoldNodeProp-based heading folds.- Plugin:
src/fold/provider.ts(headingFoldfunction added tomarkdownFoldProvider)
- Plugin:
Changed
- Recursive fold operations (
zO,zC,zA,zD) — uppercase fold commands now operate recursively on all folds within the cursor’s foldable region using range containment. Previously mapped identically to lowercase variants.- Plugin:
src/workspace/navigation.ts(foldOpenRecursiveAction,foldCloseRecursiveAction,foldToggleRecursiveAction) - Plugin:
src/fold/commands.ts(foldDeleteRecursiveAction)
- Plugin:
- Fold-enable guards — fold-creating/closing actions (
zc,za,zM,zm,zf,zF) respect thefoldenablestate. Unfold operations (zo,zR,zr,zv) always work regardless offoldenable.- Plugin:
src/fold/commands.ts,src/workspace/navigation.ts,src/fold/fold-level.ts
- Plugin:
Tests
- 12 Neovim golden test definitions for fold motions in
test/neovim/test-definitions.ts(fold-motionssuite with treesitter fold setup) - 16 E2E tests in
test/specs/vim-builtin/fold-motions.e2e.ts: 12 golden comparison tests + 4 plugin-specific tests (operator-pendingdzj/dzk, no-op without foldable regions) - 3 known deviations registered in
test/neovim/deviations.ts:zkcount (treesitter fold hierarchy),[z/]z(fold body vs heading boundary)
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: UpdatedzO/zC/zAandzn/zNrows as fixed; updated golden test coverage table with new fold commandsCONTRIBUTING.md: Updatedsrc/fold/structure with new filesREADME.md: Updated Folding feature descriptiondocs/reference/keybindings.md: Added Fold commands sectiondocs/features/workspace-navigation.md: Added Fold motions, Fold state, Recursive fold sectionsdocs/configuration/settings.md: Addedfoldenableoption
[0.114.0] - 2026-08-18
Fixed
- Vim engine settings fields lock after typing on iPad (and other mobile) — the “Insert mode escape” field (and other vim engine settings like timeoutlen, operator shadow timeout) became greyed out and unresponsive after typing a single character on iPad with Magic Keyboard. Two root causes: (1)
vim.setOption()in theonChangehandler fired the option’snotifycallback, which calledonSettingOverrideand added the key tovimrcOverrides— makingisOverridden()returntrueandrefreshDomState()disable the field. TheclearSettingOverride()call ran beforevim.setOption(), so the override was re-added immediately after clearing. Fixed by movingclearSettingOverride()to aftervim.setOption()in bothsetControlValue(declarative/post-1.13 path) and all 9 imperativeonChangehandlers forVIM_OPTION_KEYSsettings. (2) The initial settings sync inreloadFeatures()calledvim.setOption()for non-default values (clipboard, textwidth, insertmodeescape, etc.) afterregisterVimOptions()had already setregistered = true, causingnotifyto fire and mark these settings as overridden. Fixed by makingregisterVimOptions()return an activation function —registeredis only set totruewhen the caller invokes it after the initial sync completes. (#125)- Plugin:
src/settings.ts(setControlValue—clearSettingOverridemoved aftervim.setOption+refreshDomState; 9 imperativeonChangehandlers — same reorder) - Plugin:
src/vim/options.ts(registerVimOptions— returns() => voidactivation function instead of settingregistered = trueinternally) - Plugin:
src/main.ts(captures activation function, calls it after initial settings sync block)
- Plugin:
[0.113.0] - 2026-08-17
Fixed
- Cursor disappears when entering a table in source mode or raw mode — when the cursor entered a table range in source mode or with
tableWidgetMode='raw', the vim cursor became invisible while editing still worked. Root cause:mainEditorTableCursorGuardsuppressed the vim cursor whenever the cursor was in a text range matching table syntax (findTableRanges()), without checking whether a native table widget was actually visible. In source mode there are no.cm-table-widgetelements; in raw mode they exist but are hidden viadisplay: none. In both cases, the cursor was suppressed with no alternative cursor shown. Fixed by adding ahasVisibleTableWidget()check that requires at least one.cm-table-widgetelement with a non-nulloffsetParentbefore suppressing. The check also short-circuits thefindTableRanges()document scan when no visible widgets exist. (#132)- Plugin:
src/vim/table-cell-cursor-guard.ts(hasVisibleTableWidget()function;mainEditorTableCursorGuard.update()— gates cursor suppression on visible widget presence)
- Plugin:
- Cell-edit
h/j/k/lunconditionally exits to table-nav in normal mode — when editing a table cell with table-nav enabled, pressingh/j/k/lin normal mode (after Escape from insert mode) immediately exited to table-nav and navigated to the adjacent cell, even when the cursor had room to move within the cell. Root cause: thecellEditScopehjkl handlers only checkedisVimIdle()— if idle, they unconditionally calledexitCellEditToNav()+navigate()without checking whether the cursor was at a cell boundary. Fixed by adding acursorAtCellBoundary()method that checks cursor position against cell content bounds:hexits only atch <= 0,latch >= lineLen - 1,jat last line,kat first line. When the cursor is not at the boundary, the handler returnsundefinedto let vim process the key as normal in-cell movement. (#131)- Plugin:
src/vim/table-nav-controller.ts(cursorAtCellBoundarymethod;installCellEditScopehjkl handlers — boundary check beforeexitCellEditToNav)
- Plugin:
Tests
- 5 regression tests for cursor visibility in source mode and raw table mode in
test/specs/table-cursor-source-mode.e2e.ts(issue #132): 3 source mode tests (cursor layer state unchanged on table line, after traversal, on data row) + 2 raw mode tests (widget hidden, cursor layer stable during repeated traversal) - 5 regression tests for cell-edit hjkl boundary behavior in
test/specs/table-nav-mode.e2e.ts(issue #131):lmid-cell stays in cell,hmid-cell stays in cell,lat end exits to nav,hat start exits to nav, insert→Escape→lstays in cell - Systematic e2e test audit — audited all 126 non-spike e2e test files across 8 parallel analysis passes. Fixed ~60 individual test assertions across 40 files: replaced vacuous
toContain(already-present-substring)assertions with exact buffer equality, added register preservation checks, converted conditional early-returns to mandatory assertions or visiblethis.skip()calls, removed 2 exact duplicate tests, and fixed 10 test name/behavior mismatches. - Test infrastructure hardening — 6 structural improvements to the test infrastructure:
- Global
afterTesthook inwdio.conf.mts: cleans up overlays (hint, easymotion, which-key, ex-suggest), picker modals (via Escape dispatch), generic modals (via close-button click), notices, and Vim state (double<Esc>) between every test. Includes verification pass that force-removes surviving elements. - Strict helpers in
test/helpers.ts:setupEditor,sendVimEscape,getEditorValue,getCursorPos,getCursorLine,getSelection,focusEditor,ensureLivePreview,ensureSourceModenow throw with context (e.g.,"setupEditor: no MarkdownView (active leaf type: graph)") instead of silently returning defaults. waitUntil-based synchronization:setupEditorwaits for content match,loadSingleFileWorkspacewaits for MarkdownView,ensureLivePreview/ensureSourceModewait for mode change — replacing fixedbrowser.pause()delays.- Settings mutation reliability:
setPluginSettingnow awaitssaveSettings(). NewsetPluginSettingAndReloadhelper sets + saves + callsreloadFeatures()+ waits for settle. - Golden enforcement:
testWithNeovimnow throws"Missing golden case"when no golden data exists (unless the test is a known deviation), preventing silent passes. - Hint-mode link navigation:
findHintLabelForLinkupdated to usegetBoundingClientRect()with CSS var fallback, wider CM6 selectors (.cm-link,.cm-url,[data-href]), and active-leaf scoping (.workspace-leaf.mod-active .cm-editor).
- Global
- Hint-mode-links fully unblocked — 15 previously-skipped hint-mode link navigation tests now pass. Root causes fixed: (1) vault fixture files created under
test-vault/fixtures/hint-mode/to trigger Obsidian’s full rendering pipeline (CM6 link decorations, metadata cache), (2)before()hook warms link cache by opening all fixtures, (3)findHintLabelForLinkscoped to active leaf’s.cm-editor. - New unit tests — 6 new unit test files (96 tests total):
oil-parser.test.ts(15 tests): buffer line parsing, id/type/name extraction,.mdauto-append, Windows line endings, names with spacesoil-diff.test.ts(11 tests): rename/delete/create detection, foreign ids, move resolution across multi-buffer diffsvimrc-parser.test.ts(35 tests): all 13 command types, noremap detection, context inference, icon/color extraction, comments, multi-line parsingflash-labeler.test.ts(10 tests): label assignment, distance sorting, 2-char labels, reuse, skipCharsfold-persistence.test.ts(7 tests): load/save round-trip, removePath, renamePath, TTL eviction, max entries evictionpair-util.test.ts(12 tests): symmetric/asymmetric delimiters, nesting, multiline, scan limits, empty pairs
- New e2e tests — 3 new e2e test files (11 tests total):
insert-escape.e2e.ts(6 tests):jk/jjescape sequences, character cleanup, timeout behavior, non-matching sequences, empty configscrolloff-cursorline-smoke.e2e.ts(4 tests): scrolloff setting persistence + cursor positioning, cursorline enable/disable cyclecontext-actions-smoke.e2e.ts(1 test)::contextactionscommand opens a modal
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added cursor disappears in source/raw mode as fixed (#132); updated cross-cell navigation description —h/j/k/lnow move within cell when cursor is not at boundary (#131)CONTRIBUTING.md: Updatedtable-cell-cursor-guard.tsdescription withhasVisibleTableWidget()check; updatedtable-nav-controller.tsdescription withcursorAtCellBoundaryboundary checkdocs/features/tables.md: Added note about cursor visibility fix in source/raw mode; updated cell-edit behavior description —h/j/k/lmove within cell before boundary exitAGENTS.md: Updated test helpers description (strict behavior,waitUntilsynchronization,setPluginSettingAndReload); addedafterTesthook and vault fixtures documentation; added golden enforcement description; updated unit test listCONTRIBUTING.md: Updated test infrastructure tree (vault fixtures, snippets subdirs,test-wrapper.tsgolden enforcement); updated shared helper descriptions (strict behavior,waitUntil); added vault fixture and afterTest cleanup guidance to key testing rules
[0.112.0] - 2026-08-16
Fixed
- Cursor shape dropdowns always disabled in Settings UI — the 5 cursor shape dropdowns (Normal, Insert, Visual, Replace, Operator-pending) on the Appearance page were permanently disabled even when Obsidian’s built-in Vim mode was off. Root cause: Obsidian’s
addSettingTab()immediately callsgetSettingDefinitions()and caches the result for rendering and search indexing. Inonload(),addSettingTab()ran beforecreateBundledVimExtension(), so thedisabledcallbacks closed overforkActive = false(aconstcaptured at the top ofgetSettingDefinitions()). The callbacks always returnedtrue(disabled) regardless of the actual fork activation state. Fixed by replacing the capturedforkActiveconst in all 5disabledcallbacks with a directisBundledVimActive()call, so Obsidian’srefreshDomState()always evaluates the current state. Additionally,this.declarativeSettingTab.update()is now called aftercreateBundledVimExtension()to refresh the cachedgetSettingDefinitions()result — this updates the static description text which cannot use a callback. (#128)- Plugin:
src/settings.ts(5 cursor shapedisabledcallbacks —!forkActive→!isBundledVimActive()) - Plugin:
src/main.ts(store setting tab reference asdeclarativeSettingTab; calldeclarativeSettingTab.update()aftercreateBundledVimExtension())
- Plugin:
- Animated cursor suppression not synced on
reloadFeatures()—setCursorSuppressed(this.settings.animatedCursor)was only called during initial plugin load (onload()), not duringreloadFeatures(). Any runtime setting change that calledreloadFeatures()(settings UI toggle, vimrcset smoothcursor, Luavim.opt.smoothcursor) did not update the global cursor suppression flag in the codemirror-vim fork. The animated cursor canvas would draw but the native CM6 block cursor was not suppressed, causing both cursors to render simultaneously. Also fixed the born-brokentable-cursor-suppression.e2e.tstest (5 of 6 failures since commit99e5fea) whoseenableAnimatedCursor()helper set the setting and calledreloadFeatures()but never triggered the global suppression. (#127)- Plugin:
src/main.ts(reloadFeatures()— addedsetCursorSuppressed(this.settings.animatedCursor)call)
- Plugin:
- Doubled cursors when animated cursor is disabled — when animated cursor was disabled, the native CM6 text caret (thin blinking bar) appeared alongside the fork’s vim cursor (block/hollow) in normal, operator-pending, and replace modes after entering and leaving insert mode. Root cause: the fork’s
BlockCursorPlugin.update()relied on a CSSbaseThemerule to hide native cursor layers, but mode transitions (insert removes.cm-vimMode, normal re-adds it) and CM6’sdrawSelectionextension left native layers visible due to CSS specificity conflicts. Fixed in the fork by unconditionally hiding native CM6 cursor layers and settingcaretColorto match the vim cursor color (var(--interactive-accent)) in insert mode. (#129)- Fork:
~/Repos/codemirror-vim/src/block-cursor.ts(BlockCursorPlugin.update()— unconditional native layer hiding, mode-awarecaretColor) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(updatedsetCursorSuppressedAPI section)
- Fork:
- Doubled cursors in embedded editors (textarea vim overlay) — in the textarea vim overlay,
caretColorwas the accent color instead of transparent in normal mode, causing the native text caret to appear alongside the fork’s block cursor. Root cause:BlockCursorPlugin.update()checked the.cm-vimModeDOM class to determine insert/normal mode, but CM6 ViewPlugin update ordering meant the class wasn’t yet present when the block cursor plugin ran. Fixed in the fork by checkingthis.cm.state.vim.insertModedirectly instead of the DOM class. Also usessetProperty("caret-color", ..., "important")for CSS specificity robustness. (#130)- Fork:
~/Repos/codemirror-vim/src/block-cursor.ts(BlockCursorPlugin.update()— vim-state-basedcaretColorinstead of DOM class check) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(added “Vim-state-based caretColor” subsection)
- Fork:
- Escape does not close footnote popover — pressing Escape twice (insert→normal, then idle normal) in the footnote popover editor did not close the popover. The user had to click outside to dismiss it. Root cause: the fork’s
findKeyconsumed<Esc>unconditionally in idle normal mode, preventing the event from reaching Obsidian’s popover close handler. Fixed with a two-part approach: (1) the fork now exposessetIdleEscapeCallback(fn)which fires when Escape is pressed in idle normal mode, and (2) the plugin registers a callback (installEscapeGuard) that dismisses the popover viaHoverPopover.hide()for non-workspace-leaf editors while silently consuming Escape in workspace-leaf editors (preventing Obsidian hotkey interference). (#130)- Fork:
~/Repos/codemirror-vim/src/vim.js(setIdleEscapeCallbackAPI,wasIdleNormalpre-capture infindKey) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(addedsetIdleEscapeCallbackAPI section) - Plugin:
src/vim/escape-guard.ts(NEW —installEscapeGuardwithHoverPopover.hide()dismissal) - Plugin:
src/main.ts(installEscapeGuard(this.app)call in feature registration)
- Fork:
- Invisible cursor in footnote popover with animated cursor enabled — the animated cursor canvas (
z-index: 15) renders behind Obsidian’s popover (z-index: 30). The fork’s vim cursor was also suppressed (globalsetCursorSuppressed(true)), resulting in no visible cursor. Fixed by detecting editors inside.popoveror.modal-containerin theCursorControllerand un-suppressing the fork’s vim cursor for those views (setCursorSuppressedForView(view, false)). The animated cursortick()skips rendering for above-canvas editors. (#130)- Plugin:
src/vim/animated-cursor/controller.ts(isAboveCanvasflag, per-view un-suppression for popover/modal editors,tick()early return)
- Plugin:
- Stale cursor suppression after animated cursor toggle — when animated cursor was disabled at runtime,
CursorController.update()returned early without clearing the per-view suppression override set in the constructor, leaving the fork’s vim cursor hidden. Also, the constructor unconditionally suppressed the cursor regardless ofconfig.enabled. Fixed by gating constructor suppression onconfig.enabledand callingclearCursorSuppressedForView()in the disabled early-return path. (#130)- Plugin:
src/vim/animated-cursor/controller.ts(constructor gates onconfig.enabled,update()clears per-view override when disabled)
- Plugin:
Changed
- Internal API type safety — obsidian-typings migration (round 2) — eliminated 23 additional
as unknown ascasts across 16 source files by leveraging@obsidian-typings/obsidian-public-latestv6.32.0 typed APIs. Totalas unknown ascount reduced from 90 → 67. The remaining 67 casts are inherent to plugin architecture (dynamic settings indexing, codemirror-vim fork adapter access, external plugin window globals, fengari Lua bridge, minAppVersion compatibility guards).src/util/commands.ts:app.commands.executeCommandById()andapp.commands.commandsaccessed directly via typedCommandsinterface; customObsidianCommandnarrowed toPick<Command, 'id' | 'name'>src/util/leaf.ts:leaf.idandleaf.pinnedused directly (required properties viaWorkspaceItem/WorkspaceLeafaugmentation);getViewFilePath()/getViewFileBasename()useinstanceof FileViewguard instead ofas unknown as { file? }castsrc/util/vault.ts:ConfigItemimported from@obsidian-typings/obsidian-public-latestreplacing customVaultConfigKeytype inferencesrc/workspace/global-defaults.ts:mdView.getMode()called directly (typed asMarkdownViewModeType)src/editors/embeddable-editor.ts:app.embedRegistryaccessed directly;editorApp.scopeaccessed directly (official API);workspace.activeEditorassignment typed viaMarkdownFileInfosrc/oil/keybindings.ts,src/oil/manager.ts:app.internalPlugins.getEnabledPluginById('file-explorer')returns typedFileExplorerPluginInstancewithrevealInFolder(item: TAbstractFile)src/oil/oil-view.ts:this.leaf.updateHeader()called directly (typed onWorkspaceLeafaugmentation)src/oil/manager.ts:app.openWithDefaultApp(path)called directly (typed onAppaugmentation)src/vim/native-table-adapter.ts:EditModetype extendsMarkdownEditViewinstead ofRecord<string, unknown>;view.editModeaccessed directly;isInLivePreview()usesview.getMode()+editMode.sourceModeinstead ofgetState()cast;getEditModeForView()usesinstanceof MarkdownViewguardsrc/vim/table-cell-cursor-guard.ts:mdView.editor.cmaccessed directly (typed asEditorViewviaEditoraugmentation)src/ui/global-ex-command.ts:this.inputElaccessed directly (officialSuggestModal.inputEl)src/lua/loader.ts:view.getViewType()called directly (officialViewAPI) — 3 instancessrc/settings.ts:this.display()andthis.refreshDomState()— reverted, casts retained to bypassobsidianmd/no-unsupported-apiand@typescript-eslint/no-deprecatedlint rules (pluginminAppVersionis 1.7.2; these APIs require/deprecate at 1.13.0)src/picker/sources/tasks.ts:app.plugins.plugins['obsidian-tasks-plugin']accessed directly via typedPluginsinterface
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated cursor shapes section withaddSettingTab()caching fix (#128); added doubled cursors fix (#129); added #130 fixes (doubled cursors in embedded editors, Escape popover dismiss, invisible cursor in popovers, stale cursor suppression)AGENTS.md: Updated codemirror-vim fork cursor suppression description (vim-state-basedcaretColor,setIdleEscapeCallbackAPI)CONTRIBUTING.md: Addedescape-guard.tsto codebase structure; updatedcontroller.tsdescription withisAboveCanvasflag and popover/modal fallbackdocs/features/animated-cursor.md: Updated embeddable editors section with popover/modal fallback and z-index explanation- Fork
DIFFERENCES.md: AddedsetIdleEscapeCallbackAPI section; added “Vim-state-based caretColor” subsection; updatedfindKeyEscape handling description
[0.111.0] - 2026-08-16
Added
- Table-nav overlay mode — when the cursor enters a table in Live Preview, a navigation overlay activates, allowing cell navigation with
h/j/k/lwithout entering the cell editor. Supports structural commands (o/O,dd,dc,J/K,H/L,I/A,=) and cell editing entry viai/a/c/s/Enter. Escape exits table-nav. Fork-only feature.- Plugin:
src/vim/table-nav-controller.ts(KeyScope-based interception, freshcmTile.widgetreferences, hidden cell editor during navigation) - Plugin:
src/vim/table-nav-state.ts(overlay state tracking) - Plugin:
src/vim/table-nav-keymap.ts(navigation and structural command mappings) - Plugin:
src/vim/native-table-adapter.ts(extended with overlay support) - Styles:
styles.css(overlay and hidden editor styling)
- Plugin:
Changed
table-cell-cursor-guard.ts— now checksisTableNavActive()to avoid cursor suppression conflicts during table navigation.- Cross-cell motions decoupled from table-nav —
applyTableCellMotions()(h/j/k/l cross-cell navigation in native cell editors) is now gated ontableWidgetMode === 'native'only, independent ofenableTableNav. Previously required bothenableTableNavandtableWidgetMode === 'native'. This enables a third usage mode: native table editor with vim cell editing and cross-cell navigation, without the table-nav overlay. TheenableTableNavsetting now controls only the nav overlay and structural motions (]|/[|,]c/[c).- Plugin:
src/main.ts(bothonloadandreloadFeaturespaths — removedenableTableNavfromapplyTableCellMotionsgate) - Plugin:
src/settings.ts(updatedenableTableNavdescription in both declarative and imperative settings UI)
- Plugin:
Fixed
- Cursor snaps back to table after exiting table-nav — after navigating/editing in table-nav mode and exiting, the cursor could snap back to the last table cell position. Root cause:
exitTable()calledplaceCursorAround()beforedestroyTableCell(), and Obsidian’s cell editor destruction triggers internal blur/focus/selection side-effects that overrode the cursor position. Fixed by reordering: destroy the cell editor first, then deferplaceCursorAround()torequestAnimationFrameso Obsidian’s teardown handlers finish before the final cursor placement.- Plugin:
src/vim/table-nav-controller.ts(exitTable— destroy-before-place, deferred cursor placement viawindow.requestAnimationFrame)
- Plugin:
- Cell-editor normal-mode navigation bypasses table-nav — when in a cell editor in normal mode (Escape pressed once to exit insert, but not again to exit cell edit),
j/k/h/lat cell boundaries crossed to adjacent cells via the motion overrides, bypassing the table-nav controller. This left table-nav in an inconsistent state. Fixed by registeringh/j/k/lkey handlers on thecellEditScope(ObsidianScope). When vim is idle in the cell editor, these handlers exit to nav mode and navigate within the overlay. The Scope fires before vim’s key observer, so the keys are intercepted before the motion overrides run.- Plugin:
src/vim/table-nav-controller.ts(installCellEditScope— addedh/j/k/lhandlers that checkisVimIdleand callexitCellEditToNav+navigate)
- Plugin:
- Cursor flashing in Normal mode after table interaction — the table cursor guard and table-nav controller used
setCursorSuppressedForView(view, false)to unsuppress the cursor when leaving a table. This sets an explicit per-view override that conflicts with the animated cursor controller’s global suppression (setCursorSuppressed(true)), causing the native CM6 cursor to become visible and flash alongside the canvas cursor. Additionally,mainEditorTableCursorGuard.destroy()did not restore suppression state when the cursor was inside a table at destruction time, leaving a staletrueoverride through plugin recreation.cellEditorCursorGuard.update()force-unsuppressed the cell cursor on every update cycle (same anti-pattern removed fromCursorControllerin commit 62444df). All unsuppress paths now useclearCursorSuppressedForView()(which removes the per-view override, falling back to global state) instead ofsetCursorSuppressedForView(view, false). (#127)- Plugin:
src/vim/table-cell-cursor-guard.ts(mainEditorTableCursorGuard— added constructor to store view reference;destroy()now clears per-view override and resumes animated cursor whencursorInTableis true;update()usesclearCursorSuppressedForViewwhen leaving table;cellEditorCursorGuard— removed per-updatesetCursorSuppressedForView(cellView, false)force-unsuppress;destroy()usesclearCursorSuppressedForViewfor parent) - Plugin:
src/vim/table-nav-controller.ts(enterCellEdit,exitTable,destroy— all useclearCursorSuppressedForViewinstead ofsetCursorSuppressedForView(view, false)) - Plugin:
src/vim/bundled-vim.ts(exposedisCursorSuppressedForViewonCodeMirrorAdapterbridge for test access)
- Plugin:
Tests
- 6 regression tests for cursor suppression after table interaction in
test/specs/table-cursor-suppression.e2e.ts(issue #127) - Rewrote
test/specs/table-cell-vim-mode.e2e.ts“Native table cell navigation” suite for table-nav architecture: nav-mode highlight position checks (j/k/h/l), entry/exit tests (Escape, i→cell edit→Escape→nav), dd row deletion in nav mode, re-entry test (j back into table re-activates table-nav), cursor stability test (exit + insert mode round-trip doesn’t snap cursor back). Replaced “Settings gating” suite with “Table mode combinations” covering all 3 modes: native+tablenav (overlay activates), native+notablenav (cross-cell j/k/h/l works without overlay), raw (widget hidden) - Fixed 3 spike test files (
spike-table-fresh-ref,spike-cell-introspect,spike-table-nav-overlay) by disabling table-nav inbeforehooks so cell editor introspection tests can accesseditMode.tableCelldirectly - Fixed
spike-table-nav-overlayeditorInfoField test: useobsidian.editorInfoFieldfromexecuteObsidiancallback instead ofwindow.require('obsidian')
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added cursor-snap fix; updated table widget section with 3-mode table; updated cross-cell motion decouplingAGENTS.md: Updatedtable-nav-controller.tsdescription with deferred exit flow; updated table-cell-motions gatingCONTRIBUTING.md: Updatedtable-nav-controller.tsandtable-cell-motions.tsdescriptionsREADME.md: Updated table editing feature description with 3 mode combinationsdocs/features/tables.md: Updated table widget section with 3-mode architecture; clarified enableTableNav vs cross-cell motionsdocs/configuration/settings.md: UpdatedenableTableNavdescription
[0.110.0] - 2026-08-14
Added
- Animated cursor: cross-cell position handoff — when navigating between table cells via
h/j/k/l, a token-based handoff seeds the new cell’sCursorControllerwith the previous cell’s screen position via theAnimatedCursorManagersingleton. The handoff infrastructure is in place but the canvas transition animation is not visible due to CSS stacking contexts (the canvas atposition: fixedon.app-containerrenders behind table cell content). The native vim cursor (BlockCursorPlugin) serves as the steady-state renderer inside cells. See KNOWN_LIMITATIONS.md for details.- Plugin:
src/vim/animated-cursor/manager.ts(CellCrossingHandoffinterface,createCrossingToken/storeCrossingHandoff/consumeCrossingHandoffmethods,signalCellCrossing/getPendingCrossingToken/clearPendingCrossingTokenmodule-level functions) - Plugin:
src/vim/animated-cursor/controller.ts(cellTransitionActiveflag, crossing token consumption in constructor, position handoff in destroy, cell-aware tick/update that skips canvas drawing when no transition is active) - Plugin:
src/vim/table-cell-motions.ts(signalCellCrossing()call inscheduleCrossing) - Fork:
~/Repos/codemirror-vim/src/block-cursor.ts(table cell override documented in DIFFERENCES.md) - Styles:
styles.css(animated cursor canvas z-index bumped from 5 to 15)
- Plugin:
Changed
- Table editing: migrated to native Obsidian table editor — the plugin no longer suppresses Obsidian’s
cm-table-widgetor provides custom cell editors. In Live Preview, Obsidian’s native table editor handles cell editing, pipe escaping, wikilinks, cursor positioning, and<br>conversion. Vim is injected into native cell editors viaregisterEditorExtension(). ThetableWidgetModesetting is simplified from 4 values (off/cursor/always/embedded) to 2 (native/raw). Old values are automatically migrated.- Removed:
src/vim/table-widget-suppressor.ts(RangeSetBuildermonkey-patch) - Removed:
src/vim/table-render-widget.ts(custom widget) - Removed:
src/vim/table-nav-controller.ts(custom nav state machine) - Removed:
src/vim/table-cell-editor.ts(custom cell editors) - Removed:
src/vim/table-embedded-editor.ts(configuration bridge) - Added:
src/vim/table-nav-overlay.ts(nativeTableEditorAPI overlay) - Added:
src/vim/native-table-adapter.ts(typed abstraction layer) - Added:
src/types/table-editor.d.ts(runtime-discovered typings for 55 native methods)
- Removed:
Fixed
- Escape in hint mode exits embedded vim editor — pressing Escape to dismiss hint mode while inside an embedded vim editor (textarea vim overlay, Oil explorer, table cell editor) also exited the embedded editor. Root cause: Obsidian’s
Scopekeymap handlers fire independently of DOM event propagation —stopPropagation()in hint mode’s capture-phase listener does not prevent the Scope handler from receiving the event. The embedded editor’s Scope handler checkedisVimIdle()(which returnstrueduring hint mode, since hint mode is a plugin-level overlay, not a vim state) and calledonEscape(). Fixed by adding a guard in the embedded editor’s Escape handler that checksisHintModeActive(),isEasyMotionActive(), andisFlashActive()before evaluatingisVimIdle(). (#126)- Plugin:
src/editors/embeddable-editor.ts(Scope Escape handler — modal overlay active guard beforeisVimIdle()check)
- Plugin:
- Wikilinks in table cells work correctly — cursor displacement when typing
[[in table cells is fixed. The native editor handles wikilink rendering at the decoration layer, eliminating the sub-CM6 cursor displacement that affected the old custom widget. - Pipe character (
|) no longer swallowed in table cells — the native editor automatically escapes|as\|in the document source. Previously, Obsidian’s DOM-level table editor intercepted|before CM6’s input pipeline. <br>conversion handled natively — newlines in table cells are automatically converted to/from<br>by the native editor. ThecellBrToNewline/cellNewlineToBrutilities are removed.- Embedded table: Obsidian shortcuts (Ctrl+P, Cmd+O) now work in cell selection mode — modifier key combos in table-nav mode now call
e.stopPropagation()to prevent vim’seventObservers.keydownfrom consuming them as cursor movement, then manually feed the event to Obsidian’s keymap system viaapp.keymap.onKeyEvent(e). This two-step approach blocks vim (which would process<C-p>as cursor-up) while still triggering Obsidian’s hotkey bindings (command palette, file switcher, custom hotkeys). Uses the unofficialKeymap.onKeyEventAPI fromobsidian-typings. (#120)- Plugin:
src/vim/table-nav-controller.ts(handleTableNavKey—stopPropagation+app.keymap.onKeyEventfor modifier combos)
- Plugin:
- Embedded table: ex command dialog keys no longer consumed by table-nav — when vim’s ex command dialog is open (after pressing
:), table-nav keys likeh,j,k,l,a,i,care no longer intercepted by the table-nav handler. The handler now checksadapter.state.dialogand returns early when a dialog is active. (#120)- Plugin:
src/vim/table-nav-controller.ts(handleTableNavKey—adapter.state.dialogcheck)
- Plugin:
- Insert mode escape sequence and other vim engine settings not applied after restart — six vim engine settings (insertmodeescape, insertmodeescapetimeout, operatorshadowtimeout, tabstop, shiftwidth, expandtab) configured via the Settings UI were not synced to the vim engine on plugin load. Only clipboard, textwidth, and pcre were synced at startup. The remaining settings were stored in
data.jsonbut never pushed tovim.setOption()during initialization, so they silently reverted to defaults on every Obsidian restart. The vimrc/Lua code path was unaffected because it callsvim.setOption()directly. Additionally, on Obsidian 1.13+, the declarative settings system (setControlValue) did not forward any of the 9 vim engine settings tovim.setOption()— only the pre-1.13 imperativeonChangehandlers did. (#125)- Plugin:
src/main.ts(added init sync for insertmodeescape, insertmodeescapetimeout, operatorshadowtimeout, tabstop, shiftwidth, expandtab afterregisterVimOptions()) - Plugin:
src/settings.ts(VIM_OPTION_KEYSstatic set;setControlValueforwards vim engine settings tovim.setOption()withsetClipboardOption/setTextwidthside effects)
- Plugin:
Tests
- 1 regression test for hint mode Escape in
test/specs/textarea-vim.e2e.ts(issue #126): Escape in hint mode dismisses hint overlay but does not exit the textarea vim overlay — verified to fail without the modal overlay guard - 1 regression test for modifier combo in
test/specs/table-cell-vim-mode.e2e.ts: modifier combo does not move cursor during cell selection - 1 regression test for ex dialog in
test/specs/table-cell-vim-mode.e2e.ts: keys with table-nav meaning do not change active cell when ex dialog is open (verified to fail without dialog check)
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added hint mode Escape fix to textarea vim Escape behavior section (#126)AGENTS.md: Updated embeddable-editor description with modal overlay active guardCONTRIBUTING.md: Updated embeddable-editor description with modal overlay active guarddocs/features/hint-mode.md: Added note about embedded editor Escape isolationKNOWN_LIMITATIONS.md: Updated vim engine settings section — all 9 settings now synced at init (was 3); added declarative settings forwarding fix for Obsidian 1.13+ (#125)KNOWN_LIMITATIONS.md: Added animated cursor cross-cell transition as known limitation in table cell vim modality sectionCONTRIBUTING.md: Addedtable-cell-motions.tsandtable-cell-cursor-guard.tsto codebase structure; updatedmanager.tswith cross-cell handoff API; updatedcontroller.tswith cell transition architectureAGENTS.md: Updated animated cursor page ownership (unchanged —features/animated-cursor.md)docs/features/animated-cursor.md: Updated embeddable editors section with cross-cell transition details and known limitationdocs/features/tables.md: Added animated cursor note to cell editor section- Fork
DIFFERENCES.md: Added table cell override section documenting BlockCursorPlugin’s unsuppress behavior for.cm-table-widgeteditors
[0.109.1] - 2026-08-13
Fixed
- Embedded table: modifier key combos (Ctrl+P, Cmd+O) consumed by vim during cell selection — modifier key combos in table-nav mode now call
e.stopPropagation()to prevent vim’seventObservers.keydownfrom processing them as cursor movement commands (e.g.,<C-p>mapped to cursor-up). Previously, pressingCtrl+Pin cell selection mode moved the cursor up instead of opening the command palette. ThestopPropagation()blocks the event from reaching vim’s observer on the CM6 editor element while allowing Obsidian’s hotkey system (which uses Electron’sbefore-input-event) to handle it. (#120)- Plugin:
src/vim/table-nav-controller.ts(handleTableNavKey—stopPropagation()for modifier key combos)
- Plugin:
[0.109.0] - 2026-08-12
Added
- Embedded table: click-to-select cell — clicking a cell in the embedded table widget now selects that cell in table-nav mode. Works both when table-nav is already active (updates active cell) and when clicking from outside the table (enters table-nav at the clicked cell). The click handler is registered on the widget DOM via a module-level
setTableWidgetCellClickHandlercallback, coordinated betweentable-render-widget.tsandtable-nav-controller.ts. (#120)- Plugin:
src/vim/table-render-widget.ts(setTableWidgetCellClickHandler, click handler intoDOMfor embedded mode) - Plugin:
src/vim/table-nav-controller.ts(constructor registers callback;pendingClickCellfield for deferred cell selection on table-nav entry)
- Plugin:
Fixed
- Embedded table: click-outside handler exits table-nav during modal interaction — the capture-phase
mousedownlistener now checks for.modal-containerin the DOM before callingexitTable(). Previously, opening a modal (command palette, picker, settings) while in table-nav mode caused the click-outside handler to fire on the modal overlay, exiting table-nav and showing raw markdown. (#120)- Plugin:
src/vim/table-nav-controller.ts(installClickOutsideHandler— modal container check +target.closest('.modal-container')guard)
- Plugin:
- Embedded table: header-only tables (no data rows) no longer enter table-nav — tables with only a header and separator row (e.g.,
| H |\n|---|) are now skipped bycheckEntry(). Previously, entering such a table activated table-nav with only the header row navigable, which was confusing. (#121)- Plugin:
src/vim/table-nav-controller.ts(checkEntry—hasDataRowcheck beforeenterTableNav)
- Plugin:
Tests
- 1 regression test for click-to-select in
test/specs/table-cell-vim-mode.e2e.ts: clicking a cell updates active cell highlight to clicked position (verified to fail without handler) - 1 regression test for header-only table in
test/specs/table-cell-vim-mode.e2e.ts: header-only table does not enter table-nav (verified to fail without data-row check) - 1 regression test for click-outside table in
test/specs/table-cell-vim-mode.e2e.ts: cursor leaving table exits table-nav
[0.108.0] - 2026-08-12
Added
@obsidian-typings/obsidian-public-latestdevDependency — added community-maintained type definitions for Obsidian’s internal APIs. Replaces ~50 unsafeas unknown ascasts across 13 source files with properly typed access toeditor.cm,app.keymap,app.plugins,app.vault.getConfig(),app.metadataCache.resolvedLinks,WorkspaceLeaf.id/.pinned, and more. Build-only dependency — no runtime impact.- Plugin:
package.json(@obsidian-typings/obsidian-public-latestdevDependency) - Plugin:
tsconfig.json("types": ["@obsidian-typings/obsidian-public-latest"]) - Plugin:
src/util/editor.ts,src/util/keymap.ts,src/util/leaf.ts,src/util/metadata.ts,src/util/vault.ts,src/ui/global-ex-command.ts,src/ui/hint-mode.ts,src/workspace/commands.ts,src/lua/loader.ts,src/main.ts,src/vim/vim-api.ts,src/picker/sources/dataview.ts(cast removal)
- Plugin:
Fixed
- Obsidian native highlights not cleared on Escape — Obsidian’s
is-flashinghighlights (shown after following an internal link to a heading like[[Note#heading]]) now clear when Escape is pressed in normal mode. Previously, these highlights persisted until the user clicked elsewhere. Uses the unofficialeditor.removeHighlights('is-flashing')API (documented in obsidian-typings, used by obsidian-quiet-outline and others). (#122)- Plugin:
src/vim/mode-tracker.ts(clearNativeHighlightsmethod;vim-keypresshandler clearsis-flashingon<Esc>in normal mode)
- Plugin:
- Chord display breaks during surround commands — the status bar chord display now correctly accumulates all pending keystrokes during multi-key surround commands like
ysiwb,cs"(,yss", and count-prefixed variants like2ysiw*. Previously, the chord disappeared after the surround sub-state was entered (e.g.,ysshowed correctly butysiwas blank). (#123)- Fork:
~/Repos/codemirror-vim/src/vim.js(processAction— saves and restoresvim.statusaroundclearInputStatewhenvim.surroundStateis pending;handleSurroundSubState— clearsvim.statuson surround completion) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(added “Chord display preservation during surround sub-state” section)
- Fork:
- Embedded table: table-nav key handler suppressed during modals —
handleTableNavKeyandhandleCellEditKeynow check for.modal-containerin the DOM and return immediately when a modal (picker, command palette, settings) is open. Previously, keys typed into a picker input while in cell selection mode were consumed by the table handler (aentered cell edit,ssubstituted, etc.). (#120)- Plugin:
src/vim/table-nav-controller.ts(handleTableNavKeyandhandleCellEditKey— modal container check)
- Plugin:
- Cursor-aware table: cursor displacement guard for header-row jump — a
transactionFilter(tableCursorGuard) intercepts CM6 transactions that reposition the cursor to the table header row when the user was editing a data row. This prevents Obsidian’s Live Preview from snapping the cursor to the header during table creation or editing. Only active in cursor-aware mode (not embedded mode). (#121)- Plugin:
src/vim/table-render-widget.ts(tableCursorGuardtransaction filter, separated fromPrec.highStateField)
- Plugin:
Tests
- 2 e2e tests in
test/specs/native-highlight-escape.e2e.ts(issue #122): Escape in normal mode clearsis-flashinghighlight, Escape without highlights does not error - 7 e2e tests in
test/specs/surround-chord-display.e2e.ts(issue #123):ysiwb/ysiw"/yse)chord accumulation at each keystroke,yss"chord accumulation,ds"chord (passing baseline),cs"(chord accumulation,2ysiw*count-prefixed chord accumulation
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added #122 native highlight clearing as fixed; updated chord display section with surround sub-state fix (#123)AGENTS.md: Added@obsidian-typings/obsidian-public-latestto environment & tooling; updated mode-tracker description withclearNativeHighlightsCONTRIBUTING.md: Updated utility function descriptions to reflect typed access via obsidian-typings; updated mode-tracker description
[0.107.0] - 2026-08-12
Fixed
- Embedded table: cannot leave table downwards on last line — pressing
jat the last data row when the table is at the end of the document now inserts a newline and moves the cursor below the table instead of getting stuck. (#119)- Plugin:
src/vim/table-nav-controller.ts(exitTableAtBoundary— inserts\nwhen table is on last line instead of dispatching todoc.length)
- Plugin:
- Embedded table: unhandled keys swallowed in cell selection mode — the table-nav key handler previously consumed ALL keys with
preventDefault()/stopPropagation(). Now only consumes keys the handler actually processes; unhandled keys propagate to vim. Enables leader key sequences, which-key popups, and other vim key bindings during cell selection. (#120)- Plugin:
src/vim/table-nav-controller.ts(handleTableNavKey—handledflag gatespreventDefault/stopPropagation;pendingDdefault case propagates instead of consuming)
- Plugin:
- Embedded table: which-key popups in cell selection mode — a
WhichKeyOverlayinstance is now attached during table-nav mode using the main editor’s vim adapter. Previously only available in cell-edit mode. (#120)- Plugin:
src/vim/table-nav-controller.ts(attachNavWhichKey/detachNavWhichKey,setTableNavWhichKeyConfig) - Plugin:
src/vim/table-embedded-editor.ts(re-exportssetTableNavWhichKeyConfig) - Plugin:
src/main.ts(wiresembeddedWhichKeyConfigto table-nav controller)
- Plugin:
- Embedded table: picker focus stays on table widget — the
setActiveLeafoverride inembeddable-editor.tsnow allows focus transfer when a modal is open by checking for.modal-containerin the DOM. (#120)- Plugin:
src/editors/embeddable-editor.ts(setActiveLeafoverride — modal container check)
- Plugin:
- Embedded table: clicking outside table does not exit table-nav — a capture-phase
mousedownlistener onactiveDocumentexits table-nav when clicks land outside the widget. (#121)- Plugin:
src/vim/table-nav-controller.ts(installClickOutsideHandler/removeClickOutsideHandler)
- Plugin:
- Embedded table: stale table-nav state after document replacement — the ViewPlugin’s
update()now detects whendocChangedfires and the cursor is no longer in a table, exiting table-nav gracefully. (#119, #120)- Plugin:
src/vim/table-nav-controller.ts(update— stale state check ondocChanged)
- Plugin:
- Embedded table: cursor displacement when entering table-nav —
setActiveEditTableRange()is now called beforethis.view.dispatch()inenterTableNav(), preventing decoration rebuild from snapping the cursor to the header row. (#121)- Plugin:
src/vim/table-nav-controller.ts(enterTableNav— reorderedsetActiveEditTableRangebefore dispatch)
- Plugin:
- Embedded table: Escape in table-nav uses Obsidian Scope — table-nav mode now installs an Obsidian
Scopewith an Escape handler (matching the cell-editor pattern), in addition to the DOM capture-phase handler. (#120)- Plugin:
src/vim/table-nav-controller.ts(installNavScope/removeNavScope)
- Plugin:
Tests
- Unblocked 13 previously-skipped embedded table widget e2e tests in
test/specs/table-cell-vim-mode.e2e.ts— root cause: missingbrowser.reloadObsidian()and table-only document content preventing widget rendering. All cell editing tests (two-Escape, entry modes, register sharing,<br>round-trip, multi-table navigation) now pass. - 2 regression tests for #119 in
test/specs/table-cell-vim-mode.e2e.ts:jat last row exits table at end of document, cursor usable after exit - 2 regression tests for #120 in
test/specs/table-cell-vim-mode.e2e.ts: Escape in cell selection mode (skipped — WDIO DOM Escape routing limitation), unhandled keys not swallowed - 3 regression tests for #121 in
test/specs/table-cell-bridge.e2e.ts:jmoves through proper table rows, cursor not stuck on header, cursor not jumping back
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked 7 table widget sub-issues as fixed (#119, #120, #121); updated embedded table e2e test coverage noteAGENTS.md: Updated table-nav-controller description with click-outside handler, Scope-based Escape, which-key in table-nav, stale state cleanupCONTRIBUTING.md: Updatedtable-nav-controller.tsdescription
[0.106.0] - 2026-08-11
Fixed
- Bundled
table/table3snippets missing trailing newline — the$0final tabstop was inline on the last table row. When a table was inserted at the end of a document, there was no line below it, preventing cursor movement past the table. Fixed by adding a standalone$0as a separate final body element, matching the pattern used by theFrontmattersnippet. (#118)- Plugin:
src/snippets/bundled/obsidian-markdown.json(Table 2x2,Table 3x3— moved$0from inline on last row to standalone final element)
- Plugin:
- User-defined snippets duplicate bundled snippets instead of overriding them — when a user defined a snippet with the same prefix as a bundled one (e.g.,
table), both appeared in the completion menu and picker instead of the user snippet replacing the bundled one. Root cause:addToPrefixIndex()inSnippetRegistryinserted user entries before bundled entries but never removed the bundled entry, and entry IDs were source-qualified (bundled:Table 2x2vsuser:My Table) so both coexisted in theentriesMap. Fixed with priority-based override logic (user > lua > bundled): when a higher-priority source registers a prefix colliding with a lower-priority entry, the lower-priority entry is removed from the prefix index and, if orphaned (no remaining prefixes), from the entries Map. (#118)- Plugin:
src/snippets/registry.ts(sourcePrioritystatic method;addToPrefixIndexrewritten with priority-based filtering, orphan cleanup, and priority-sorted insertion)
- Plugin:
Tests
- 10 unit tests in
test/unit/snippets/registry.test.ts(issue #118): basic load and prefix indexing, user overrides bundled, lua overrides bundled, user overrides lua, full priority chain (user > lua > bundled), same-priority coexistence, multi-prefix partial overlap (keeps non-overlapping prefix), multi-prefix full overlap (removes orphaned entry), no-collision coexistence, bundled table snippet trailing newline validation - 7 e2e tests in
test/specs/snippets/snippet-override.e2e.ts(issue #118):tablesnippet trailing newline via Tab,table3snippet trailing newline via:snippet, table at end of document produces content after last row, user override expands correct body,lookupByPrefixreturns only user entry,getAllexcludes shadowed bundled entry, non-overridden bundled snippet still works
Documentation
CHANGELOG.mdCONTRIBUTING.md: Addedregistry.tsto snippets codebase structure with priority-based override descriptiondocs/features/snippets.md: Expanded override behavior documentation with priority order and Lua snippet override semantics
[0.105.1] - 2026-08-11
Fixed
- Embedded table mode: cursor jumps to first table after cell edit — follow-up to the multi-table fix in 0.105.0. After editing a cell in the second (or later) table and pressing Escape twice, the cursor jumped back to the first table. Two sub-bugs: (1)
activeEditTableRangewas cleared beforecloseCellEditor/tableRealigndispatches, causingbuildDecorationsto create aDecoration.replacefor the active table and displacing the cursor — fixed by keepingactiveEditTableRangeset throughout the exit and refresh lifecycle. (2) AftertableRealign,doRefreshAfterOpusedArray.find()with a 200-position threshold to re-locate the table, which returned the first table within range rather than the closest — fixed by replacing with a nearest-match loop. (#117)- Plugin:
src/vim/table-nav-controller.ts(exitCellEdit— removes prematuresetActiveEditTableRange(null)clear;doRefreshAfterOp— setsactiveEditTableRangebeforetableRealigndispatch, replacesArray.findwith nearest-match loop for post-realign table re-location)
- Plugin:
Tests
- 4 e2e test cases in
test/specs/table-cell-vim-mode.e2e.ts(issue #117, skipped — embedded widget rendering limitation in WDIO): entry into second table highlights correct widget, cell editor opens on second table not first, add row affects only second table, first table unaffected during second table navigation
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated multi-table fix withactiveEditTableRangelifecycle andArray.findnearest-match fixesCONTRIBUTING.md: Updatedtable-nav-controller.tsdescriptiondocs/features/tables.md: Added multi-table support note to embedded mode section
[0.105.0] - 2026-08-11
Added
- Which-key popups in embedded editors — which-key hints now appear in table cell editors (embedded mode) and textarea vim overlays. The popup renders in the parent note’s viewport using the same position and styling as the main editor’s which-key. User keymaps (vimrc, Lua) are fully available since the codemirror-vim keymap is global. Bundled vim mode only — embedded editors in built-in vim mode do not receive vim and are silently skipped.
- Plugin:
src/ui/which-key.ts(WhichKeyConfigexported interface;WhichKeyOverlay.forEmbeddedEditor()static factory for dependency injection;attach()injected-mode early return;showOverlay()injected container fallback with status-bar padding guard;onKeyPressGeneral()delay bypass for embedded editors;detachAdapter()try/catch for destroyed adapters;destroy()clears injected references) - Plugin:
src/vim/table-cell-editor.ts(setCellEditorWhichKeyConfig()exported setter;openCellEditor()deferred which-key creation viasetTimeout(0);closeCellEditor()which-key cleanup before editor destroy) - Plugin:
src/vim/textarea-vim-manager.ts(whichKeyConfigclass field;updateOptions()extended with which-key config;ActiveReplacement.whichKeyfield;replace()deferred which-key creation with.view-content→.modal-containerfallback;teardownActive()which-key cleanup) - Plugin:
src/main.ts(WhichKeyConfigimport;setCellEditorWhichKeyConfigimport; embedded config construction and wiring afterWhichKeyOverlaycreation)
- Plugin:
Fixed
- Embedded table mode does not handle multiple tables per note — in embedded table widget mode (
set tablewidget=embedded), when a note contained two or more tables, entering table-nav mode on any table other than the first always attached the cell highlight, key handlers, and cell editor to the first table’s DOM widget. Entering from below selected the last cell of the first table. Root cause:findWidgetEl()intable-nav-controller.tsqueried all.vim-table-renderedelements and returned the first match without considering whichTableRangethe cursor was in. Fixed by adding atableFromparameter tofindWidgetEl()and using CM6’sview.posAtDOM()to correlate each widget element with its document position, returning the nearest match to the active table’sfromoffset. (#117)- Plugin:
src/vim/table-nav-controller.ts(findWidgetEl— accepts optionaltableFromparameter, usesposAtDOMnearest-match withtry/catchfor detached elements;enterTableNav— passestable.fromexplicitly;devAssertimport and__DEV__assertion verifying widget position matches active table)
- Plugin:
- Enter in embedded table cell editor breaks table structure — pressing Enter in insert mode inside an embedded table cell editor (
set tablewidget=embedded) inserted a literal newline into the cell content. Upon exiting the table, the multi-line content was written back into the single-line markdown table row, breaking the table structure — the second line appeared outside the table. Fixed by converting newlines to<br>tags on cell editor close and converting<br>tags back to newlines on cell editor open, preserving multi-line cell content using standard HTML line breaks that Obsidian renders correctly within table cells. Existing<br>content in cells round-trips cleanly. (#115)- Plugin:
src/vim/table-utils.ts(cellBrToNewline,cellNewlineToBr— new pure utility functions for<br>↔ newline conversion) - Plugin:
src/vim/table-cell-editor.ts(openCellEditor— converts<br>to newlines on open;closeCellEditor— converts newlines to<br>on close)
- Plugin:
Tests
- 4 e2e test cases in
test/specs/table-cell-vim-mode.e2e.ts(issue #117, skipped — embedded widget rendering limitation in WDIO): entry into second table highlights correct widget, cell editor opens on second table not first, add row affects only second table, first table unaffected during second table navigation - 8 e2e tests in
test/specs/textarea-vim-which-key.e2e.ts: which-key appears after partial chord (d,g) in normal mode, dismisses on command completion (dd), dismisses on Escape, suppressed in insert mode, suppressed whenwhichKeyModeis off, cleans up on editor close (blur), cleans up on modal removal - 14 unit tests in
test/unit/table-cell-br.test.ts(issue #115):cellBrToNewline(7 tests —<br>,<br/>,<br />, case-insensitive, multiple tags, no-op, empty string),cellNewlineToBr(4 tests — single/multiple newlines, no-op, empty string), round-trip (3 tests — newline→br→newline, br→newline→br, mixed markdown content) - 2 e2e test cases in
test/specs/table-cell-vim-mode.e2e.ts(issue #115, skipped — embedded widget rendering limitation in WDIO): Enter in cell editor produces<br>and keeps table valid, round-trip of existing<br>in cell content
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked multi-table widget selection as fixed withposAtDOMposition matching; marked Enter-in-cell-editor table breakage as fixed with<br>conversion; added which-key in embedded editors note to table cell sectionCONTRIBUTING.md: Updatedtable-nav-controller.tsdescription withposAtDOM-based widget matching for multi-table support; updatedtable-utils.tsdescription withcellBrToNewline/cellNewlineToBrhelpers; updatedwhich-key.ts,table-cell-editor.ts, andtextarea-vim-manager.tsdescriptions with which-key overlay lifecycleAGENTS.md: Updated dual-vim architecture section with which-key in embedded editors via dependency injectiondocs/features/tables.md: Added multi-table support note to embedded mode section; added multi-line cell content note with<br>support in embedded mode; added which-key support note to embedded cell editor sectiondocs/configuration/which-key.md: Added embedded editors section documenting which-key in table cell editors and textarea vim overlays
[0.104.0] - 2026-08-10
Fixed
- Flash labels missing from top half of viewport with frontmatter scrolled off-screen — in Live Preview mode, when YAML frontmatter properties (~10-15 lines) were collapsed into a widget and scrolled off-screen, flash
f/F/t/Tlabels only appeared in the bottom half of the viewport. The number of missing lines matched the frontmatter line count. Root cause:getVisibleRange()insrc/easymotion/targets.tsusedview.lineBlockAtHeight()which relies on CM6’s height map — when the collapsed frontmatter widget was off-screen, height estimation errors causedcoordsAtPos()to returnnullfor targets near the viewport top, andfilterVisibleTargets()dropped them. Fixed by usingview.visibleRanges(actually-rendered document ranges) instead oflineBlockAtHeight. Also affected EasyMotion target scanning. (#114)- Plugin:
src/easymotion/targets.ts(getVisibleRange— replacedlineBlockAtHeightwithview.visibleRanges) - Plugin:
test/unit/flash-targets.test.ts(updated CM6 stub to providevisibleRanges)
- Plugin:
v$dcursor off-by-one — visual-modev$dleft cursor at ch:5 instead of ch:4 after deleting to end of line. Root cause:clipCursorToContentin the delete operator ran whilevim.visualModewas stilltrue(allowingch = text.length), andexitVisualModeran after the operator returned without re-clamping. Fixed by re-clippingoperatorMoveTothroughclipCursorToContentafterexitVisualModeinapplyOperator.- Fork:
~/Repos/codemirror-vim/src/vim.js(applyOperator— re-clip cursor afterexitVisualMode) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(added “Visual operator cursor re-clamping after exitVisualMode” section)
- Fork:
ssubstitute consumed by flash jump in Tier 1 tests — theskey did nothing innormal-editing.e2e.tsbecause the test vault hadflashJumpEnabled: true, which mappedsto flash jump mode instead of the built-in substitute (cl). Flash jump tests explicitly enable this setting in their ownbefore()hooks. Fixed by settingflashJumpEnabled: falseindata.jsonand adding a defensive disable in the test’sbefore()hook.- Plugin:
test-vault/.obsidian/plugins/vim-motions/data.json(flashJumpEnabled: false) - Plugin:
test/specs/vim-builtin/normal-editing.e2e.ts(defensive flash disable inbefore())
- Plugin:
vt.don multi-dot content consumed by flash labels —vt.don content with 2+ dot characters (e.g.,foo.bar.baz) deleted only 1 character because flash motions showed labels for the multiple.matches, consuming thedkey as a label character. Not a fork bug — flash working as designed (same as flash.nvim in Neovim). Fixed by settingenableFlash: falsein test vaultdata.jsonand disabling flash invisual-mode.e2e.tsbefore()hook. Flash-specific behavior is tested in dedicated test files.- Plugin:
test-vault/.obsidian/plugins/vim-motions/data.json(enableFlash: false) - Plugin:
test/specs/vim-builtin/visual-mode.e2e.ts(defensive flash disable inbefore())
- Plugin:
Added
vimHandleKeystest helper — new helper intest/helpers.tsthat dispatches all keys synchronously throughVim.handleKey()in a singleexecuteObsidiancallback, bypassing DOM event timing. Used for visual-mode compound operations that fail withvimRawKeysDOM dispatch.- Plugin:
test/helpers.ts(vimHandleKeysfunction)
- Plugin:
useHandleKeyflag onTestCaseDefinition— test cases can opt in tovimHandleKeysdispatch viauseHandleKey: true.testWithNeovimchecks this flag and routes tovimHandleKeysinstead ofdispatchVimKeys.- Plugin:
test/neovim/test-definitions.ts(useHandleKey?: booleanon interface, set on 6 visual-mode test cases) - Plugin:
test/neovim/test-wrapper.ts(importvimHandleKeys,useHandleKeyin config type, dispatch branching)
- Plugin:
- Deviation category classification — all deviations in
deviations.tsnow have acategoryfield (intentional,infra-limitation,upstream-bug,upstream-unsupported,recording-issue).findDeviation()export added.[INFRA-SKIP]console warnings emitted for infra-limitation deviations.- Plugin:
test/neovim/deviations.ts(interface + 25 entries classified +findDeviation()) - Plugin:
test/neovim/test-wrapper.ts(infra-skip logging in both live and golden paths)
- Plugin:
- Golden schema extended with
registersandvisualMode—GoldenCase.resultnow includes optionalregisters(unnamed register text + linewise flag) andvisualMode(charwise/linewise/blockwise). Recording captures these fields. Comparison is not yet enabled (register state leaks between tests in shared Obsidian session).- Plugin:
test/neovim/compare.ts(EditorStateextended,getObsidianState/getNeovimStatecapture registers and visual sub-mode) - Plugin:
test/neovim/golden.ts(GoldenCaseextended) - Plugin:
test/neovim/record-golden.ts(captures registers and visual mode) - Plugin:
test/neovim/client.ts(getRegisterType,getRawModemethods) - Plugin:
test/neovim/golden-data/*.json(24 files re-recorded with new fields)
- Plugin:
- Golden mode comparison —
testWithNeovim()golden path now comparesmodein addition tocontentandcursor. Mode mismatches that were previously invisible are now caught.- Plugin:
test/neovim/test-wrapper.ts(mode comparison in golden path)
- Plugin:
else { throw }guards on all 26SUITES.find()files — if a suite name is renamed intest-definitions.tsbut not in the spec file, the test runner produces an explicit failure instead of silently generating zero tests.- Plugin:
test/specs/vim-builtin/*.e2e.ts(26 files)
- Plugin:
Changed
- E2E test assertions strengthened — 35 tests that previously only checked
mode === 'normal'orassertPluginLoaded()now have content, cursor, or behavioral assertions. 16 workspace-layout ex-command tests renamed with[crash-guard]prefix. 1 tautological assertion fixed (toBeGreaterThanOrEqual(0)→toBe(0)).- Plugin:
test/specs/undo-tree.e2e.ts,test/specs/undo-tree-navigation.e2e.ts,test/specs/vim-builtin/ex-commands-expanded.e2e.ts,test/specs/vimrc.e2e.ts
- Plugin:
- Deviation count reduced — 6 visual-mode infra-limitation deviations resolved via
useHandleKey(V3j+J, vip+d, v+r, v+aw+d, vt.+d, v$+d). 1 deviation resolved via key-string fix (lua nmap change word—<Esc>literal →\x1bbyte). 1 reclassified frominfra-limitationtoupstream-bug(lua leader key mapping).
Tests
- 1 e2e test in
test/specs/flash-frontmatter-viewport.e2e.ts(issue #114): flash labels appear in top half of viewport when frontmatter properties are scrolled off-screen in Live Preview mode — verifies labels are not clustered in bottom half only
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added “E2E test infrastructure weaknesses” section with fixed/remaining items; markedssubstitute test failure as fixed; updated deviation-masked operations count and root causes; marked flash frontmatter viewport offset as fixedAGENTS.md: Updated deviation registry description with categories and[INFRA-SKIP]; updated test helpers withvimHandleKeys; updatedtargets.tsdescription withvisibleRangesCONTRIBUTING.md: AddedvimHandleKeysto helper list; addeduseHandleKeyflag documentation; updated deviations.ts description with categories; updatedtargets.tsdescription withvisibleRangesDIFFERENCES.md(fork): Added “Visual operator cursor re-clamping after exitVisualMode” section
[0.103.0] - 2026-08-08
Fixed
- Native Obsidian shortcuts (Tab, Shift+Tab, Ctrl+Shift+I, F-keys) consumed in Normal/Visual mode — since v0.99.0, unmapped functional keys were silently swallowed in Normal and Visual modes but worked in Insert mode. Root cause: the fork’s
findKeyguard (commit4aa1cc7) used/^<.+>$/to suppress unmatched angle-bracket keys in normal mode, which consumed ALL angle-bracket keys — including<Tab>,<S-Tab>,<C-S-I>,<F1>–<F12>, and other keys that should propagate to the host application. The guard was intended to catch<Space>(which bypassed the originalkey.length === 1check), but the regex was too broad. Fixed by narrowing to a whitelist of text-producing special keys (<Space>,<BS>,<Del>,<CR>) plus keys that must not propagate to the host (<Esc>,<Ins>), and preserving the Mac Alt character guard (<A-x>) from upstream PR #194.<Esc>is included becausehandleEsc()returnsundefinedin idle normal mode (intentional no-op), but the keydown event must still be consumed to prevent it from propagating to the DOM and triggering modal closes or scope pops. Functional/navigation keys now returnundefinedfromfindKey, allowing Obsidian to handle them natively. (#113)- Fork:
~/Repos/codemirror-vim/src/vim.js(findKey— narrowed key consumption guard from/^<.+>$/to/^<(Space|BS|Del|CR|Esc|Ins)>$/+ Mac<A-.>guard) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(updated “Unmatched angle-bracket keys consumed in normal mode” section)
- Fork:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated v0.99.0<Space>fix description — narrowed guard to prevent consuming functional keysAGENTS.md: Updated codemirror-vim fork description with narrowed key consumption guard
[0.102.0] - 2026-08-08
Fixed
- Vim
pwith non-text clipboard content silently does nothing — whenclipboard=unnamedorclipboard=unnamedplusis set, pressingp(or]p,[p,:put) with an image on the system clipboard did nothing. The fork’spasteaction callednavigator.clipboard.readText()which returns""for image-only clipboard content, andcontinuePaste()bailed on the empty string with no error handling. Fixed by adding a.catch()handler to thereadText()promise and afallbackToNativePaste()method that callsdocument.execCommand('paste')when the text clipboard is empty orreadText()rejects. This triggers Obsidian’s native paste pipeline, which creates an attachment and inserts![[Pasted image …]]. AprogrammaticPasteflag suppresses the fork’sgetOnPasteFnlistener during the fallback to prevent spurious insert-mode entry. The editor stays in normal mode after the fallback. Coversp,]p,[p,:put, and explicit"+pregister paste.P/gp/gPare overridden by the host plugin’spasteFromRegister()and are not affected (separate issue).- Fork:
~/Repos/codemirror-vim/src/vim.js(programmaticPasteflag,getOnPasteFnguard,pasteaction rewrite with.catch(),fallbackToNativePastemethod) - Fork:
~/Repos/codemirror-vim/src/types.ts(fallbackToNativePastetype signature invimActions)
- Fork:
Tests
- 3 e2e spike tests in
test/specs/spikes/spike-execcommand-paste.e2e.ts:execCommand('paste')with image clipboard triggers native image paste,execCommand('paste')with text clipboard inserts text (control), vimpwith image clipboard andclipboard=unnamedinserts image via native fallback
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added clipboard non-text paste fallback fix to yank-ring sectionAGENTS.md: Updated codemirror-vim fork description withfallbackToNativePasteandprogrammaticPasteflagdocs/configuration/settings.md: Added note about non-text clipboard fallback to clipboard setting description
[0.101.0] - 2026-08-08
Fixed
- Escape exit from textarea vim overlay leaks to parent scope — pressing Escape twice (insert → normal → exit) in the textarea vim editor caused the Escape keydown event to propagate to the parent modal’s DOM, closing it or switching the active leaf. Root cause:
handleEscapeAndRedispatch()calledteardownActive()synchronously inside the ObsidianScope.registerhandler, which destroyed the editor and popped the keymap scope mid-handler. TheisolateKeyEventsstopPropagation()handler was removed with the editor, so the DOM-level Escape continued propagating to parent elements. Fixed by deferring teardown viarequestAnimationFrame— the Scope handler returnstrue(consuming the event) while the editor’s scope is still on the stack. Also added a_destroyingflag toembeddable-editor.tsto prevent the blur event listener from double-popping the keymap scope whendestroy()is already handling cleanup. (#112)- Plugin:
src/vim/textarea-vim-manager.ts(handleEscapeAndRedispatch— deferred teardown viarequestAnimationFrame) - Plugin:
src/editors/embeddable-editor.ts(_destroyingflag, blur handler guard)
- Plugin:
- Escape exit from table cell editor leaks to parent scope — same scope-pop-mid-handler vulnerability as the textarea vim overlay. The
onEscapecallback intable-nav-controller.tscalledexitCellEdit()→closeCellEditor()→editor.destroy()→popKeymapScope()synchronously inside the Scope handler. Fixed with the same deferred-teardown pattern. (#112)- Plugin:
src/vim/table-nav-controller.ts(onEscapecallback — deferredexitCellEditviarequestAnimationFrame)
- Plugin:
Tests
- 1 e2e test in
test/specs/textarea-vim.e2e.ts(issue #112): Escape exit does not leak Escape keydown to parent modal container DOM and does not change active leaf — verifies zero Escape events propagate tomodal.containerEl, modal stays open, leaf ID unchanged
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated embedded editor Escape handler description with deferred teardown pattern,_destroyingguard, and scope-pop-mid-handler fixCONTRIBUTING.md: Updatedembeddable-editor.tsdescription with_destroyingguard; updatedtextarea-vim-manager.tsdescription with deferred teardown; updatedtable-nav-controller.tsdescription with deferredonEscapeAGENTS.md: Updated dual-vim architecture section with deferred teardown and_destroyingguard
[0.100.0] - 2026-08-07
Fixed
:snippetvisual selection not captured — running:snippet <name>from visual mode now correctly wraps the selected text. Previously,$TM_SELECTED_TEXT/$VISUALresolved to empty and the snippet was inserted at the cursor instead of replacing the selection. Root cause: vim’s ex-command dispatcher callsexitVisualMode()before the:snippethandler runs, collapsing the CM6 selection. Fixed by reading the visual selection from vim’s'<'/'>'marks (which surviveexitVisualMode) via thecmadapter parameter, extracting the text withcm.getRange(), and overridingctx.selectedTextbefore preprocessing. Visual line mode (V) normalizes the start toch: 0and extends the end to full line length. (Discussion #108)- Plugin:
src/snippets/commands.ts(recoverVisualSelection()— reads'<'/'>'marks +lastSelection.visualMode/visualLineflags;:snippethandler overridesctx.selectedTextand uses mark-derivedfrom/tooffsets for theapply()range)
- Plugin:
Tests
- 10 e2e tests in
test/specs/spikes/spike-snippet-visual-surround.e2e.ts(Discussion #108):$TM_SELECTED_TEXTwraps word selection viaviw, fills tabstop default in link snippet,$VISUALalias parity, multiline callout wrapping, empty selection regression guard, link snippet structure, Luafmt()wraps selection, Luat()/i()bold wrapping, Lua link structure, Lua empty selection placeholder - 12 e2e tests in
test/specs/spikes/spike-snippet-visual-edge-cases.e2e.ts(Discussion #108): visual line mode (V) wraps full line, multi-lineVjjwraps three lines, charwisevacross lines, visual block mode (<C-v>) produces non-empty text, stalelastSelectionuses marks (vimgvsemantics), bookmark invalidation falls back to empty, picker baseline, normal mode after visual:snippet(documented pre-existing), correct snippet text despite normal mode,v$end-of-line, single char at EOL,v$mid-line to end
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated snippet variable limitations section —$TM_SELECTED_TEXT/$VISUALtab-expand limitation clarified, visual-mode:snippetnow worksCONTRIBUTING.md: Addedcommands.tsto snippets codebase structure with visual selection recovery descriptiondocs/features/snippets.md: Updated$TM_SELECTED_TEXT/$VISUALcallout to confirm visual-mode:snippetworks
[0.99.0] - 2026-08-06
Added
set nopcre— Vim-style regular expressions — users can now switch from JavaScript/PCRE regexps to Vim-style regex syntax in search and substitution viaset nopcre(vimrc),vim.opt.pcre = false(Lua), or the Settings UI toggle (Settings → Vim Motions → Vim engine → PCRE). The codemirror-vim fork already implemented the fullpcreoption (regex translation with magic modes,\</\>word boundaries,\zs/\ze, backreference conversion); this change wires it into the plugin’s option tracking, settings UI, and documentation. Default:true(JavaScript regexps, no behavior change for existing users). (#111)- Plugin:
src/settings.ts(pcre: booleaninVimMotionsSettings,pcre: trueinDEFAULT_SETTINGS, toggle in both declarative and imperative settings UI — General page, Vim engine group) - Plugin:
src/vimrc/loader.ts(pcreadded toKNOWN_SET_OPTIONSandKNOWN_CM_VIM_OPTIONS) - Plugin:
src/main.ts(initialization sync —vim.setOption('pcre', false)when user has disabled PCRE) - Plugin:
test/unit/known-set-options.test.ts(pcreadded tonewOptionstest array)
- Plugin:
- 37 snippet variables (up from 16 documented) — expanded the snippet variable system to cover the full VSCode snippet specification, plus vim-ecosystem aliases. New variables:
$TM_SELECTED_TEXT(wired — was stubbed),$VISUAL(alias for$TM_SELECTED_TEXT, vim convention),$TM_CURRENT_LINE,$TM_CURRENT_WORD,$WORD(alias for$TM_CURRENT_WORD, vim convention),$TM_LINE_NUMBER(1-based),$TM_LINE_INDEX(0-based),$CLIPBOARD(wired via cache-ahead pattern — was stubbed),$RELATIVE_FILEPATH,$WORKSPACE_NAME,$WORKSPACE_FOLDER,$CURSOR_INDEX,$CURSOR_NUMBER,$CURRENT_MILLISECOND,$CURRENT_MILLISECONDS_UNIX,$CURRENT_TIMEZONE_NAME, plus previously undocumented$CURRENT_YEAR_SHORT,$CURRENT_MONTH_NAME_SHORT,$CURRENT_DAY_NAME_SHORT,$CURRENT_SECONDS_UNIX,$CURRENT_TIMEZONE_OFFSET.$CLIPBOARDuses a cache-ahead pattern (refreshed onwindow focusandvisibilitychange) to avoid making the synchronous snippet pipeline async. On mobile,$CLIPBOARDresolves to empty due to browser clipboard API restrictions.$TM_SELECTED_TEXT/$VISUALresolve to the editor selection at expansion time; in tab-expand mode, selection is not available (tab expansion requires an empty selection). (#110)- Plugin:
src/snippets/types.ts(PreprocessContext— addedcurrentLine,currentWord,lineNumber,lineIndex,workspaceNamefields) - Plugin:
src/snippets/variables.ts(added 15 new variable entries includingVISUAL,WORD,TM_CURRENT_LINE,TM_CURRENT_WORD,TM_LINE_NUMBER,TM_LINE_INDEX,RELATIVE_FILEPATH,WORKSPACE_NAME,WORKSPACE_FOLDER,CURSOR_INDEX,CURSOR_NUMBER,CURRENT_MILLISECOND,CURRENT_MILLISECONDS_UNIX,CURRENT_TIMEZONE_NAME; addedpad3()andgetTimezoneName()helpers) - Plugin:
src/main.ts(_clipboardCachefield,refreshClipboardCache()method, clipboard cache listeners onwindow focus+visibilitychange+ initial population;getSnippetPreprocessContext()rewritten to populate all fields from the active editor including selection, current line/word, line number, and workspace name)
- Plugin:
Fixed
- EasyMotion operator-pending inclusivity — EasyMotion motions (
f,t,e,s,ge,E,gE) now correctly include the target character in operator-pending mode, matching native Vim semantics. Previously, all EasyMotion motions were registered with emptymotionArgs, so the fork treated them as exclusive —y<leader><leader>fk{label}excluded the targetkfrom the yank. Visual mode was unaffected (it extends the selection directly without consulting theinclusiveflag). Backward motions (F,T) remain exclusive, matching native Vim. (#109)- Plugin:
src/easymotion/register.ts(EasyMotionDefinterface — addedmotionArgs?: Record<string, unknown>;EASYMOTION_DEFS— addedmotionArgs: { inclusive: true }to 8 of 17 defs matching Vim’s native inclusivity; registration loop — passesdef.motionArgs ?? {}tomapCommand)
- Plugin:
- Cursor stuck below YAML frontmatter in Live Preview with “Properties in document: Source” —
k,gk, and<Up>could not move into the frontmatter region when the editor was in Live Preview mode and Obsidian’s “Properties in document” setting was set to “Source”. In this configuration, the.metadata-containerDOM element exists but is hidden (display: none). The fork’sfocusBeforecallback found the hidden element viaquerySelector, focused it (no visible effect), andmoveByLines/moveByDisplayLinesreturned the original cursor position — leaving the cursor stuck. Fixed by adding asetPropertiesSource(fn: () => boolean)API to the fork, parallel tosetLivePreviewField. When the callback returnstrue, the frontmatter interception block is skipped entirely and the cursor moves through raw frontmatter text normally. The plugin passes() => getVaultConfig(app, 'propertiesInDocument') === 'source', evaluated per cursor movement so runtime setting changes take effect immediately. (#77)- Fork:
~/Repos/codemirror-vim/src/cm_adapter.ts(setPropertiesSourceAPI,_propertiesSourceFngate infindPosV) - Fork:
~/Repos/codemirror-vim/src/index.ts(exportsetPropertiesSource) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(addedsetPropertiesSourceAPI section, updated “Properties navigation” section with two-level gate) - Plugin:
src/vim/bundled-vim.ts(createBundledVimExtensionacceptsisPropertiesSourcecallback, callssetPropertiesSource) - Plugin:
src/main.ts(passespropertiesInDocument === 'source'callback) - Plugin:
src/types/codemirror-vim.d.ts(addedsetPropertiesSourcetype declaration)
- Fork:
- Escape in operator-pending mode exits embedded text area editor — pressing
dthenEscapein the textarea vim overlay exited the editor instead of clearing the pending operator. The Escape handler checkedvim.mode === 'normal'without accounting for operator-pending, surround, partial key sequences, and literal-character-await sub-states. Additionally, the CM6 keymap handler could never run because vim’seventObservers.keydowncallede.preventDefault()before CM6 keymaps processed the event. Fixed by moving Escape handling to an ObsidianScope.registerhandler (fires before vim’s observer) with a newisVimIdle()check covering all compound-command sub-states. (#112)- Plugin:
src/editors/embeddable-editor.ts(isVimIdlehelper,VimIdleStateinterface, Scope-based Escape handler replacing CM6 keymap handler)
- Plugin:
- Keydown events leak from embedded text area editor to parent modals — typing keys (e.g., Space) in insert mode inside the textarea vim overlay propagated
keydownevents to the parent modal, triggering unintended actions in third-party plugins (e.g., Spaced Repetition). Fixed with a new opt-inisolateKeyEventsoption onEmbeddableEditorOptionsthat stopskeydownandkeyuppropagation via CM6domEventHandlers. Only enabled for textarea-vim overlays; Oil and table-cell editors are unaffected. (#112)- Plugin:
src/editors/embeddable-editor.ts(isolateKeyEventsoption,domEventHandlerswithstopPropagation) - Plugin:
src/vim/textarea-vim-manager.ts(isolateKeyEvents: true)
- Plugin:
- Unmatched
<Space>inserted as text after failed multi-key sequence — pressing an unmapped key after a partial multi-key sequence (e.g.,<leader><leader><Space>where no EasyMotion motion matches) inserted a literal space character into the document. Root cause: the fork’sfindKeyusedkey.length === 1to suppress unmatched single-character keys in normal mode, butvimKeyFromEventconverts Space to"<Space>"(7 characters) via thespecialKeymap, bypassing the guard. The function returnedundefinedinstead of a consuming no-op, letting the keydown propagate to CM6’s text input handler. Fixed by replacing the guard withkey.length === 1 || /^<.+>$/.test(key)to match both plain characters and angle-bracket notation keys. (#112)- Fork:
~/Repos/codemirror-vim/src/vim.js(findKey— generalized key length guard) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(added “Unmatched angle-bracket keys consumed in normal mode” section)
- Fork:
Tests
- 4 e2e tests in
test/specs/easymotion-comprehensive.e2e.ts(issue #109): inclusivefyank includes target character, inclusiveedelete includes end-of-word character, exclusivewyank excludes target (regression), visual modefyank includes target (regression) - 1 unit test in
test/unit/known-set-options.test.ts(issue #111):pcreoption registered inKNOWN_SET_OPTIONSwith correct type and settingsKey, default value verified - 49 unit tests in
test/unit/snippets/variables.test.ts:resolveVariables()coverage for all 37 variables (selection/content, file/path, workspace/cursor, date/time, random), syntax variants ($VARand${VAR}), alias parity ($VISUAL=$TM_SELECTED_TEXT,$WORD=$TM_CURRENT_WORD,$RELATIVE_FILEPATH=$TM_FILEPATH,$WORKSPACE_FOLDER=$WORKSPACE_NAME), edge cases (empty fields, unknown variables, tabstop defaults, adjacent variables) - 20 e2e tests in
test/specs/snippets/snippet-variables-integration.e2e.ts(issue #110): file/path variables against liveWelcome.md(5 tests), editor content variables with cursor positioning (4 tests), line number variables 1-based/0-based (3 tests), workspace name and alias (2 tests), cursor index/number constants (2 tests), selection variable resolution viagetSnippetPreprocessContext()(3 tests), combined multi-variable expansion (1 test) - 3 e2e tests in
test/specs/vim-builtin/g-commands.e2e.ts(issue #77):kmoves up through source-rendered frontmatter,knavigates through multiple frontmatter properties,gkmoves up through source-rendered frontmatter. Tests setpropertiesInDocumentto'source'and ensure Live Preview mode, with save/restore of the original setting. - 13 unit tests in
test/unit/embedded-editor-idle.test.ts(issue #112):isVimIdlecoverage for null/undefined, idle normal, insert/visual/replace modes, operator pending, surround state, partial key buffer, expectLiteralNext, multiple sub-states, missing inputState, missing keyBuffer - 3 e2e tests in
test/specs/textarea-vim.e2e.ts(issue #112): operator-pending Escape does not exit overlay, idle normal Escape exits overlay, insert-mode typing does not leak keydown to parent modal
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: UpdatedKNOWN_CM_VIM_OPTIONSlist insetoption scope section to includepcre; added snippet variable limitations section ($CLIPBOARDmobile restriction,$TM_SELECTED_TEXTtab-expand limitation,snip.envdeferred, comment variables deferred); updated properties navigation section with “Properties in document: Source” edge case fix and updated test coverage; marked EasyMotion operator-pending inclusivity as fixed; added 4 new known limitations (linewisej/k,motionArgs.forward/clipToLine,EXTRA_DEFSbidirectional motions,easyMotionRepeatoperator-pending)CONTRIBUTING.md: Updatedvariables.tsdescription in codebase structure; updatedbundled-vim.tsdescription withsetPropertiesSourcewiring; updatedeasymotion/register.tsdescription with per-motionmotionArgsfor operator-pending inclusivityREADME.md: Updated Snippets feature line with variable count and vim-ecosystem aliasesAGENTS.md: Updated codemirror-vim fork description withsetPropertiesSourceAPIdocs/features/snippets.md: Expanded variable table from 16 to 37 entries organized into sections (selection/content, file/path, workspace/cursor, date/time, random) with info callout about selection and clipboard behaviordocs/features/easymotion.md: Updated operator-pending section with inclusivity semantics; fixed stale dot-repeat notedocs/configuration/vimrc.md: Addedpcrerow to boolean options tabledocs/configuration/settings.md: Addedpcrerow to Vim engine settings tabledocs/configuration/lua-config.md: Addedpcrerow tovim.optoptions tableDIFFERENCES.md(fork): AddedsetPropertiesSourceAPI section, updated “Properties navigation” section with two-level gateKNOWN_LIMITATIONS.md: Updated embedded editor Escape handler description with Scope-based approach andisVimIdlesub-state detection; added key event isolation noteCONTRIBUTING.md: Updatedembeddable-editor.tsdescription withisVimIdlehelper, Scope-based Escape, andisolateKeyEventsoptionAGENTS.md: Updated dual-vim architecture section with Scope-based Escape handling for embedded editorsDIFFERENCES.md(fork): Added “Unmatched angle-bracket keys consumed in normal mode” section under Behavioral fixes
[0.98.0] - 2026-08-05
Fixed
- Animated cursor character displaced on lines with tall content — on lines containing tall inline elements (e.g., MathJax with
\dfrac), the character rendered beneath the block cursor shifted vertically. Root cause: the renderer’s baseline formula centered the character within thecoordsAtPos()rect height, which on some platforms/fonts returns the full line height instead of the per-character height. For a ~80px tall line with ~19px font height, this produced a ~30px downward shift. Fixed by using the actual DOM character bounding rect (Range.getBoundingClientRect()viaview.domAtPos()) for baseline calculation, falling back tocoordsAtPos()when the DOM rect is unavailable. (#106)- Plugin:
src/vim/animated-cursor/renderer.ts(BlockCharInfo— addedcharTop/charHeightfields;drawCursorShapeanddrawSmearCursor— baseline anchored to DOM char rect when available) - Plugin:
src/vim/animated-cursor/controller.ts(resolveBlockChar— extracts DOM character bounding rect viaRange.getBoundingClientRect())
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked animated cursor tall-line character displacement as fixedCONTRIBUTING.md: Updatedrenderer.tsdescription with DOM-based baseline calculationAGENTS.md: Updatedrenderer.tsdescription in animated cursor codebase structuredocs/features/animated-cursor.md: Added tall-line displacement fix to known limitations
[0.97.0] - 2026-08-05
Fixed
- Priority over Latex Suite and other CM6 extensions — the codemirror-vim fork’s keydown handler no longer depends on plugin load order to fire before other extensions that use
Prec.highest. The fork now uses a CM6eventObservers.keydown(DOM event observer) instead ofeventHandlers.keydown— in CM6’s dispatch order, observers run before handlers, guaranteeing vim processes keys first regardless ofPrecordering orcommunity-plugins.jsonorder. Previously, both the fork and Latex Suite registered keydown handlers atPrec.highest, and the first-registered handler won — making key handling dependent on which plugin loaded first. (#107)- Fork:
~/Repos/codemirror-vim/src/index.ts(movedkeydownfromeventHandlerstoeventObservers, addedsetKeyInterceptActiveAPI) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(added “Observer-based keydown dispatch” and “setKeyInterceptActive API” sections) - Plugin:
src/flash/state.ts(setFlashActiveandcancelFlashcallsetKeyInterceptActive) - Plugin:
src/easymotion/register.ts(createMotionTriggerandcreateCharMotionTriggerbracket try/finally withsetKeyInterceptActive) - Plugin:
src/ui/hint-mode.ts(waitForHintKeysetssetKeyInterceptActiveon entry and cleanup)
- Fork:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated Latex Suite interaction section with observer-based keydown dispatch fixAGENTS.md: Updated codemirror-vim fork description with observer-based keydown dispatch andsetKeyInterceptActiveAPIdocs/guides/ecosystem-compatibility.md: Updated extension priority description with observer-based mechanism
[0.96.0] - 2026-08-04
Fixed
- Hint mode dropdown menus appear at top-left corner — synthetic click events dispatched by hint mode lacked
clientX/clientYcoordinates, causing Obsidian’s dropdown menus (vault switcher, context menus, etc.) to position at(0, 0)instead of near the clicked element. Fixed by computing the element’s center fromgetBoundingClientRect()and passing coordinates to allMouseEventandPointerEventdispatches. Also replacedel.click()with a coordinate-awareMouseEventdispatch. (#104)- Plugin:
src/ui/hint-mode.ts(getElementCenterhelper, coordinate injection inhintActivate— openInNewPaneMouseEvent, normal clickPointerEvents, andel.click()replacement)
- Plugin:
Added
- Hint mode right-click (context menu) action — new
gfbinding in non-editor views and Shift+label modifier in editor context to open the right-click context menu on any hint target. Dispatches acontextmenuMouseEventwith proper coordinates fromgetElementCenter(). Also available as:hintcontextmenu(:hintco) ex command andvim-motions:hint-context-menuObsidian command. Shift key normalization inwaitForHintKey()ensures Shift-held characters match lowercase labels correctly. (#104)- Plugin:
src/ui/hint-mode.ts(hintContextMenuaction,contextMenuincreateHintAction/createHintActions,shiftKeyinHintResult, Shift→contextMenu upgrade in action selection,e.key.toLowerCase()normalization when Shift held),src/workspace/global-defaults.ts(gfbinding,contextMenuinhintActionsparameter type),src/main.ts(:hintcontextmenuex command,vim-motions:hint-context-menuObsidian command,contextMenuinhintActionstype)
- Plugin:
Tests
- 5 e2e tests in
test/specs/hint-mode.e2e.ts(issue #104):gffrom graph view shows hint overlay,gflabel dispatches contextmenu event with non-zero coordinates, Shift+label dispatches contextmenu in editor, Shift+label matches lowercase labels (case-sensitivity regression),hint-context-menuObsidian command registered
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated hint mode actions with context menu action, coordinate fix, and Shift modifierAGENTS.md: Updatedhint-mode.tsdescription withgetElementCenter,hintContextMenu, and Shift modifierCONTRIBUTING.md: Updatedhint-mode.tsdescriptionREADME.md: Updated Vimium-style hints feature line withgfdocs/features/hint-mode.md: Addedgfto non-editor keybinding table, Shift modifier to editor context,vim-motions:hint-context-menuto commandsdocs/reference/keybindings.md: Addedgfto non-editor view bindings tabledocs/features/ex-commands.md: Added:hintcontextmenuto hint ex commands table
[0.95.0] - 2026-08-04
Fixed
- Linewise visual select highlighting not visible inside callouts — in visual-line mode (
V), callout content did not show the selection highlight in two scenarios: (1) When the callout was collapsed as a widget (cm-embed-block cm-callout), thecm-vim-linewise-widget-selectionbackground was overridden by the callout’s own styling. (2) When the cursor was inside the callout (unfolded as.cm-lineelements withHyperMD-quoteclasses), Obsidian’s.HyperMD-quote { background-color: var(--blockquote-background-color) }rule overrode thecm-vim-linewise-selectionbackground due to CSS cascade ordering. Fixed by increasing CSS specificity of the selection rules to (0,5,0) via.cm-editor .cm-scroller .cm-contentancestor chain, outranking Obsidian’s (0,4,0) blockquote rule without using!important. (#103)- Plugin:
styles.css(increased specificity on.cm-vim-linewise-selectionand.cm-vim-linewise-widget-selectionrules)
- Plugin:
- Hint mode does not label the vault switcher — the vault switcher button (
.workspace-drawer-vault-switcher) in the left sidebar was not discoverable by hint mode because its CSS class was not in theOBSIDIAN_SELECTORSlist. The element is a plain<div>without button semantics (role="button",<button>tag, etc.), so it was not matched by any standard or Obsidian-specific selector. Fixed by adding.workspace-drawer-vault-switchertoOBSIDIAN_SELECTORS. (#104)- Plugin:
src/ui/hint-mode.ts(added.workspace-drawer-vault-switchertoOBSIDIAN_SELECTORS)
- Plugin:
- Animated cursor displaced rightward at end-of-line in visual mode — with animated cursor enabled, the block cursor rendered one character past the last visible character when visual selection reached the end of a line. Root cause:
refreshTarget()stepped back fromsel.headin forward selections to render the cursor on the last selected character, but thech !== '\n'guard prevented the step-back whensel.headpointed to a newline (end of line). Fixed by replacing the character-based guard with a line-boundary guard (pos > line.from), which correctly handles end-of-line, empty lines, and document end. (#105)- Plugin:
src/vim/animated-cursor/controller.ts(refreshTarget— line-boundary guard replacing character guard)
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated visual-line widget highlight section with callout CSS specificity fix; added vault switcher to hint mode target list; added animated cursor EoL visual mode fix
[0.94.0] - 2026-08-01
Fixed
- Gutter settings ignored when set via vimrc or Lua —
set nonumber,set signcolumn=no,vim.opt.number = false, and other gutter-related settings (number,relativenumber,numberwidth,linenumbermode,cursorline,cursorlineopt,signcolumn,statuscolumn,foldcolumn) had no effect when configured via.obsidian.vimrcor.obsidian.init.lua. Root cause: vimrc/Lua overrides were in-memory only and never persisted, but gutter CM6 extensions are created at startup from persisted values. Fixed with aconfigOverridespersistence system: after vimrc/Lua loading, override values are captured and persisted indata.json. On next startup,configOverridesare merged on top of base settings before CM6 extensions are created, so gutters use the correct values from the start. Also added gutter reconfiguration calls toreloadFeatures()for in-session changes. (#101)- Plugin:
src/main.ts(loadSettings— configOverrides extraction and merge;captureConfigOverrides— new shared capture method;saveSettings— persist configOverrides;reloadFeatures— gutter reconfiguration;softReloadVimrc— clear stale overrides and re-capture;clearSettingOverride— new helper) - Plugin:
src/settings.ts(replacedvimrcOverrides?.deletewithclearSettingOverrideacross all onChange handlers — covers both declarative and imperative paths, fixes missingluaOverridesdeletion)
- Plugin:
preVimrcSettingsshallow copy — nested objects (cursorShapes,modePrompts,pickerKeymap) shared references withthis.settings, causingsaveSettings()to accidentally persist overridden cursor shapes. Fixed with deep copy.- Plugin:
src/main.ts(line 677 — deep copy nested objects inpreVimrcSettingssnapshot)
- Plugin:
- Clipboard/textwidth falsely shown as “Set by vimrc” — the initial settings restoration at startup called
onSettingOverride()forclipboardandtextwidth, writing tovimrcOverrideseven without a vimrc file. Fixed by using direct side-effect calls.- Plugin:
src/main.ts(replacedonSettingOverridecalls with directsetClipboardOption/setTextwidthcalls)
- Plugin:
- Oil explorer loses focus after committing staged changes — after making changes in Oil (e.g., deleting a file) and committing with
:w, the Oil editor lost focus when the confirmation dialog was confirmed or dismissed. Two bugs: (1)OilConfirmModal.onClose()never resolved the promise when the user pressedEscto dismiss the modal, causingcommit()to hang permanently. Fixed by adding aresolvedguard —onClose()resolvesfalsewhen no button was clicked. (2) After the confirmation dialog closed (via Confirm, Cancel, or Esc), focus was never returned to the Oil editor. Fixed by callingview.focusEditor()on both the cancel and commit paths. (#100)- Plugin:
src/oil/manager.ts(OilConfirmModal—resolvedguard flag,onCloseresolves on Esc dismissal;commit—view.focusEditor()after confirm and cancel paths)
- Plugin:
:sortcursor positioning —:sort(and ranged:2,3sort) now positions the cursor at the first line of the sorted range, matching Neovim. Previously the cursor stayed at line 0 regardless of the sort range.- Fork:
~/Repos/codemirror-vim/src/vim.js(exCommands.sort—cm.setCursorafterreplaceRange)
- Fork:
CTRL-V $ dcursor overshoot — after a block visual delete to end-of-line (CTRL-V jj $ d), the cursor column is now clamped to the remaining line length. Previously the cursor could land past the last character on shortened lines.- Fork:
~/Repos/codemirror-vim/src/vim.js(operators.delete— block visual cursor clamping)
- Fork:
- Hint mode: modifier keydown event propagates to Obsidian handlers — pressing
Ctrlalone during hint mode could trigger Obsidian’s own key handlers because the modifier-only early return inwaitForHintKey()did not callpreventDefault()orstopPropagation(). The event leaked through to Obsidian’s hotkey system via bubble-phase listeners, potentially causing side effects depending on the user’s Obsidian configuration. Fixed by addinge.preventDefault()ande.stopPropagation()before the early return for modifier-only keys. (#98)- Plugin:
src/ui/hint-mode.ts(waitForHintKeyhandler —preventDefault+stopPropagationon modifier-only keydown)
- Plugin:
- Hint mode: count prefix (
2F) focus restoration races with async navigation — when using2Fon an internal link target,hintActivate()firednavigateWithJump()without awaiting it (viavoid), thensetActiveLeaf(originalLeaf)ran synchronously. The asyncopenLinkText()insidenavigateWithJumpcould resolve after the focus restoration and steal focus back to the new tab — a race condition that manifested on slower machines or with heavier vaults. Similarly,duplicateLeaf()was fire-and-forgotten. Fixed by makinghintActivateasync and awaiting bothnavigateWithJump()andduplicateLeaf(). ThecreateHintActioncallback now awaits the action result before restoring focus, making the behavior deterministic. (#98)- Plugin:
src/ui/hint-mode.ts(hintActivate—async,await navigateWithJump,await duplicateLeaf;hintOpenNew— returnsPromise<boolean>;createHintAction—asynccallback,await action(),Promise.resolve()wrappers for sync actions)
- Plugin:
Added
- 27 new vimrc/Lua configurable options — the following settings were previously only configurable via the Settings UI and are now available via
:setin vimrc andvim.optin Lua:subword,picker,pickerleadermappings,pickermatcher,pickeromnisearch,pickertasks,pickerdataview,ripgrep,ripgreppath,ripgrepargs,grepmode,oil,oilhiddenfiles,oilconfirmdeletethreshold,oilsort,hinthotkey,undotreeposition,undotreeautoopen,imswitching,impreset,imbinarypath,imobtainargs,imswitchargs,imdefaultnormal,imrestorebehavior,imdefaultinsert. All options work identically across Settings UI, vimrc, and Lua.- Plugin:
src/vimrc/loader.ts(27 newKNOWN_SET_OPTIONSentries)
- Plugin:
- Runtime invariant system —
invariant()(always-on, type-narrowing) anddevAssert()(dev-only, stripped from production) helpers insrc/util/invariant.ts. 21 invariants placed across 9 source files protecting mode transitions, dual-vim architecture, settings resolution, Lua engine lifecycle, extension cleanup, cursor state, and cell editor singleton. Violations are logged to console, rate-limited via Notice, and inspectable via the:violationsex command.__DEV__build-time flag via esbuilddefineenables dev-only checks in development/watch builds and strips them from production.- Plugin:
src/util/invariant.ts(new —invariant,devAssert,getViolations,clearViolations),src/types/globals.ts(new —__DEV__global type declaration),esbuild.config.mjs(defineoption),vitest.config.ts(defineoption)
- Plugin:
:violationsex command — displays accumulated invariant violations with timestamps.:violations!clears the violation log.- Plugin:
src/workspace/commands.ts(:violationsand:violations!registration)
- Plugin:
Tests
- 57 new unit tests across 5 new test files:
test/unit/invariant.test.ts(12 tests): invariant/devAssert helpers, violation cap, rate limiting, stack traces, shallow copytest/unit/mode-tracker.test.ts(11 tests): getDialogPrefix, resolveMode logictest/unit/dual-vim.test.ts(7 tests): bundled-vim lifecycle, bridge install/uninstall, invariant triggertest/unit/settings-resolution.test.ts(16 tests): DEFAULT_SETTINGS completeness, settings merge, configMode migration, signcolumn migration, idempotencytest/unit/lua/lifecycle.test.ts(8 tests): sandboxed state creation/destruction, instruction guard, coroutine runner lifecycle
- 3 expanded tests in
test/unit/animated-cursor.test.ts: manager register/deregister, destroy clears all, MAX_CONTROLLERS warning - 70 new Neovim golden test cases (490 → 560): operator+motion combos (+28), visual mode operations (+15), insert mode operations (+12), ex command operations (+15)
- 2 Neovim deviations closed (22 → 20):
:2,3sortcursor positioning,CTRL-V $ delete to EOLcursor overshoot - 30 unit tests in
test/unit/known-set-options.test.ts: KNOWN_SET_OPTIONS coverage guard (every non-excluded settings key has an entry, excluded keys list has no stale entries, no settingsKey points to non-existent setting), 27 new option entry validations (type, settingsKey, validValues) - 6 e2e tests in
test/specs/gutter-vimrc-lua.e2e.ts: gutter settings via Lua config (enable/disable line numbers, enable/disable sign column, disable all gutter elements, hybrid line numbers) - 4 e2e tests in
test/specs/oil-poc.e2e.ts(issue #100): oil retains focus after no-op commit, oil retains focus after confirmed destructive commit, oil retains focus after cancelled destructive commit, oil retains focus after Esc-dismissing the confirm modal - 3 e2e tests in
test/specs/hint-mode.e2e.ts(issue #98):Fon wikilink opens in new tab via command,openNew(2)on wikilink keeps focus on original leaf after first hint, Ctrl keydown stopped from propagating during hint mode
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked gutter vimrc/Lua reconfiguration as fixed; marked preVimrcSettings shallow copy as fixed; marked clipboard/textwidth false override as fixed; updatedsetoption scope section with configOverrides persistence; marked Oil focus loss after commit as fixeddocs/features/oil-explorer.md: Added focus retention after commit noteAGENTS.md: Updated hint mode page ownership withhinthotkeyREADME.md: Updated vimrc configurable settings countCONTRIBUTING.md: Added configOverrides persistence andclearSettingOverridehelper to conventions; updated vimrc loader descriptiondocs/configuration/vimrc.md: Added 27 new options to vimrc tables; updated override behavior section with configOverrides persistence and gutter restart notedocs/configuration/lua-config.md: Added 27 new options to vim.opt tableKNOWN_LIMITATIONS.md: Marked:sortcursor andCTRL-V $cursor deviations as fixedCONTRIBUTING.md: Added invariant system to codebase structure, updated testing instructions forbuild:devAGENTS.md: Updated manual testing instructions forbuild:dev, added:violationscommand, updated golden test count and deviation countREADME.md: Addednpm run test:unitto development commandsDIFFERENCES.md(fork): Added:sortcursor positioning and block visual delete cursor clamping sectionsdocs/features/ex-commands.md: Added:violationsand:violations!ex commandsKNOWN_LIMITATIONS.md: Updated hint mode modifier key fix withstopPropagation; updated count prefix focus fix with asynchintActivateAGENTS.md: Updatedhint-mode.tsdescription with asynchintActivateand modifierstopPropagation
[0.93.0] - 2026-07-31
Fixed
- Hint mode: pressing Ctrl/Shift/Alt/Meta alone clears labels — pressing any modifier key alone during hint mode dismissed the overlay. Root cause:
waitForHintKey()inhint-mode.tstreated modifier-only keydown events (wheree.keyis"Control","Shift", etc.) as unmatched first characters, triggering cleanup. The global key handler (global-key-handler.ts:228-234) already filtered modifier-only keys correctly. Fixed by adding the same guard at the top ofwaitForHintKey()’s handler. (#98)- Plugin:
src/ui/hint-mode.ts(waitForHintKeyhandler — modifier-key guard beforepreventDefault)
- Plugin:
- Hint mode: count prefix (
2F) shifts focus to new tab immediately — when using a count prefix (e.g.,2F) in non-editor context, the first hint activation shifted focus to the newly opened tab, causing the second round of hints to appear on the wrong tab. Root cause:hintActivate()withopenInNewPane=truecallsnavigateWithJump()orduplicateLeaf(), both of which focus the new leaf. The nextrun(count-1)then showed hints on the new tab. Fixed by saving the original active leaf beforewaitForHintKeywhen count > 1, and restoring focus to it after each activation before scheduling the next round. (#98)- Plugin:
src/ui/hint-mode.ts(createHintAction—originalLeafcapture +setActiveLeafrestore before recursiverun)
- Plugin:
- Hint mode:
<leader><leader>hignores count prefix — thehintModeaction defined viadefineActiondid not acceptActionArgs, soactionArgs.repeat(the count from vim’s input state) was never passed toactivate(). Count prefix only worked in non-editor context (via global key handler). Fixed by accepting(_cm, actionArgs)and passingactionArgs.repeat. (#98)- Plugin:
src/main.ts(defineAction('hintMode')— acceptactionArgs, passrepeattoactivate)
- Plugin:
Tests
- 6 e2e tests in
test/specs/hint-mode.e2e.ts(issue #98): Ctrl alone keeps labels, Shift alone keeps labels, Alt alone keeps labels, Meta alone keeps labels, Ctrl then label char still narrows labels,2Fkeeps focus on original graph view leaf
[0.92.1] - 2026-07-31
Fixed
- Ctrl hotkeys broken on active tab after closing Oil explorer — after closing Oil (via
q,:q, orcloseOil()),Ctrl-based hotkeys (<C-d>,<C-f>,<C-b>, etc.) stopped working on the restored file until the user switched to another tab and back. Root cause:OilView.onClose()calledremoveChild(editor)which triggersunload()but notdestroy(). ThepopKeymapScopecall that removes the Oil-specific ObsidianScope(withCtrl+T/S/H/L/Chandlers) lives indestroy(), so the scope remained pushed on the keymap stack after Oil was gone — intercepting Ctrl keys and silently consuming them. Fixed by callingthis.editor.destroy()beforeremoveChild()inonClose(). (#93)- Plugin:
src/oil/oil-view.ts(onClose— explicitdestroy()beforeremoveChild)
- Plugin:
Tests
- 2 e2e tests in
test/specs/oil-poc.e2e.ts: Ctrl keys work after closing Oil viacloseOil()(scope cleanup regression), Ctrl keys work after opening and closing Oil multiple times (scope stack leak detection)
[0.92.0] - 2026-07-31
Fixed
- Oil
<C-t>/<C-s>/<C-h>keybindings intercepted by Obsidian default hotkeys — pressing<C-t>in Oil opened an empty Obsidian tab instead of the file under cursor.<C-s>triggered Obsidian’s save and<C-h>triggered search & replace. Root cause: Obsidian’s default hotkeys (Ctrl+T= new tab,Ctrl+S= save,Ctrl+H= search & replace) fire at the Electron level before the embeddable editor’s vim key handler receives the event. The vim mapping (vim.map('<C-t>', ':oilopentab<CR>', 'normal')) never executed. Fixed by registeringCtrl+T,Ctrl+S,Ctrl+H,Ctrl+L, andCtrl+Con the embeddable editor’s ObsidianScope(the same mechanism that already interceptsMod+Enter). Scope-registered keys fire before Obsidian’s default hotkeys. Navigation keys (<C-t>,<C-s>,<C-h>) blur the editor before calling the manager action so thesetActiveLeafguard in the embeddable editor allows the new leaf through. Non-navigation keys (<C-l>refresh,<C-c>close) call the manager directly. The ex commands (:oilopent,:oilopensv,:oilopensh,:oilrefresh,:oilclose) continue to work viavim.defineExfor users who prefer typing them. (#93)- Plugin:
src/editors/embeddable-editor.ts(registerScopeKey()method onEmbeddableMarkdownEditorinterface andConcreteEmbeddableEditorclass — delegates to the internal ObsidianScope),src/oil/oil-view.ts(registerOilScopeKeys()— registers 5 Ctrl-key combos on the editor scope with blur-before-navigate for cross-leaf actions)
- Plugin:
Tests
- 1 e2e test in
test/specs/oil-poc.e2e.ts:<C-t>opens file in new tab and focuses it (regression test — verifies active file is the target, active view type is markdown, Oil view still exists, leaf count increased)
[0.91.0] - 2026-07-30
Fixed
-
Which-key popup disappears quickly in non-editor views — in non-editor views (reading view, graph, canvas, etc.), the which-key popup appeared and vanished after ~500ms instead of staying visible until the user completed the key sequence. Root cause: the global key handler’s 1000ms
SEQUENCE_TIMEOUTfiredresetSequence()unconditionally, dismissing the popup even when partial completions existed. In editor mode, the which-key overlay stays open until the command completes (driven byvim-keypress/vim-command-doneevents, not a fixed timer). Fixed by checking for partial matches when the timeout fires — if the current key buffer has pending completions in the registry, the timeout restarts instead of resetting. The popup now stays alive until the user completes or abandons the sequence. (#97)- Plugin:
src/workspace/global-key-handler.ts(startTimeout— partial-match check beforeresetSequence)
- Plugin:
-
gtalways navigates to first tab instead of next tab in non-editor views — pressinggtwithout a count prefix in non-editor views (graph, canvas, reading view) always jumped to the first tab instead of cycling to the next tab. Root cause:dispatch()in the global key handler usedthis.count || 1, making count 0 (no count typed) indistinguishable from count 1 (user typed1gt). Thegthandler’sif (count > 0)always triggeredgotoNthTab(app, 1). Fixed by passingthis.countdirectly tobuiltinhandlers, letting each handler decide its own default. Thegthandler already had the correct branching (count > 0→ nth tab, else → next tab). Other handlers (j/kscroll, hint actions) applycount || 1locally. (#97)- Plugin:
src/workspace/global-key-handler.ts(dispatch— rawthis.countfor builtin,this.count || 1for obcommand repeat),src/workspace/global-defaults.ts(localcount || 1in scroll/hint handlers)
- Plugin:
-
Ngt(count + gt) ignored count in editor views — pressing2gtor3gtin an editor view always went to the next tab instead of the Nth tab. Root cause: the editor-modegtwas mapped toworkspace:next-tabviacreateCommandAction, which ignoresactionArgs.repeatentirely. The count-awaregotoTabaction was only mapped tog<C-t>. Fixed by replacing thegtmapping with a newgtActionthat usesactionArgs.repeatIsExplicitto distinguish “no count typed” (next tab) from “count N typed” (go to tab N). (#97)- Plugin:
src/workspace/navigation.ts(gtAction—repeatIsExplicitcheck,gotoNthTabfor explicit count,workspace:next-tabfor no count)
- Plugin:
-
gotoNthTabcounted sidebar leaves in tab numbering —Ngtandg<C-t>counted all workspace leaves (including sidebar panes) when determining the Nth tab.3gtcould navigate to a sidebar pane instead of the 3rd editor tab. Fixed by filtering leaves withleaf.getRoot() === app.workspace.rootSplitto only count main editor area leaves, matching the existing pattern insrc/lua/loader.ts. (#97)- Plugin:
src/workspace/global-defaults.ts(gotoNthTab—rootSplitfilter),src/workspace/navigation.ts(createGotoTabAction—rootSplitfilter)
- Plugin:
-
Oil editor degraded when opened from non-editor context — opening Oil from an empty pane, settings view, graph view, or any non-markdown context produced a broken editor: keybindings (
g?,<CR>,q) didn’t work, which-key popup didn’t appear, and the cursor could move through concealed icon ranges character by character. Two root causes: (1) Inembeddable-editor.ts, thebuiltinVimOnclosure variable capturedisVimEnabled(app)which returnstruewhen the bundled fork is active — making the guard!builtinVimOn && isBundledVimActive()always false and the explicit vim extension push dead code. The embedded editor relied entirely on Obsidian’sregisterEditorExtension()injection to receive vim, which could fail on leaves that had never hosted a MarkdownView. Fixed by removing the dead guard and adding a post-constructionensureVimExtension()safety net that checks for vim presence viagetCM()and appends it viaStateEffect.appendConfigonly if absent. (2) Inmanager.ts,openOil()calledgetLeaf(false)which reuses the current leaf — when that leaf was a non-editor view (empty pane, settings), it lacked initialized editor infrastructure. Fixed by priming the leaf with a temporary markdown view state (setViewState({ type: 'markdown' })) before switching to the Oil view type when no MarkdownView is active.- Plugin:
src/editors/embeddable-editor.ts(removedbuiltinVimOnclosure, removed dead vim push frombuildLocalExtensions, addedensureVimExtension()withgetCMcheck +StateEffect.appendConfigfallback, replacedisVimEnabledimport withisBuiltinVimEnabled+getCM),src/oil/manager.ts(openOil— leaf priming withsetViewState({ type: 'markdown' })when no active MarkdownView)
- Plugin:
-
Cannot open files/folders from Oil explorer at vault root — after the v0.90.0 fix, pressing
<CR>on any file or folder in the Oil explorer did nothing. Root cause:discoverAndMergeHidden()calledcache.loadDirectory()three times during a single refresh cycle, causing buffer entry IDs to become out of sync with the cache. Entry lookup by ID returnedundefined, soopenEntryAtCursor()silently aborted. Fixed by passing the expected buffer content from the initial render as a parameter todiscoverAndMergeHidden(), eliminating the redundantrenderDirectoryToBuffer()call that triggered the thirdcache.loadDirectory(). The cache is now updated exactly once per merge. Confirmed by spike unit test demonstrating ID desync (buffer IDs [1,2] vs cache IDs [6,7]). (#93)- Plugin:
src/oil/manager.ts(discoverAndMergeHidden— acceptsexpectedContentparameter, removed redundantrenderDirectoryToBuffercall),src/oil/oil-view.ts(callers pass rendered content)
- Plugin:
-
Oil explorer title bar does not update when navigating directories — after navigating from one directory to another, the tab header continued to show the original directory name. Root cause:
setDirectory()andrefreshContent()updatedthis.dirPathbut never signaled Obsidian to re-readgetDisplayText(). Fixed by addingnotifyHeaderChanged()which callsleaf.updateHeader()(Obsidian internal) after dirPath changes, insetDirectory(),refreshContent(), andsetState(). (#93)- Plugin:
src/oil/oil-view.ts(notifyHeaderChangedprivate method, called fromsetDirectory,refreshContent,setState)
- Plugin:
-
Hidden files toggle (
g.) has no effect — pressingg.in Oil to toggle hidden files did nothing. Root cause:this.settings.oilShowHiddenFiles ?? this.showHiddenused the nullish coalescing operator (??), butoilShowHiddenFilesis typed asboolean(defaultfalse), so??never fell through to the runtime togglethis.showHidden. Fixed by replacing the boolean field with ashowHiddenOverride: boolean | null(null = use setting) and agetEffectiveShowHidden()helper that prioritizes the override when set. (#93)- Plugin:
src/oil/manager.ts(showHiddenOverridefield,getEffectiveShowHidden()helper,toggleHidden()rewritten)
- Plugin:
-
<CR>in Oil opens file in new tab instead of replacing Oil view — pressing Enter on a file in Oil opened it in a new tab, leaving the Oil view in the original tab. In oil.nvim,<CR>(select) opens the file in the same window, replacing the oil buffer. Root cause:navigateWithJump()usedopenLinkText()which cannot replace a custom view type. Fixed by usingleaf.openFile()directly on the Oil leaf vianavigateWithJumpFile(), matching the pattern used bycloseOil(). (#93)- Plugin:
src/oil/manager.ts(openEntryAtCursorrewritten to useopenFileInLeaf, newopenFileInLeafprivate method),src/oil/keybindings.ts(oilOpenEntrydelegates tomanager.openEntryAtCursor())
- Plugin:
Added
- Oil
<C-t>open in new tab — new:oilopentabex command mapped to<C-t>, matching oil.nvim’s default. Opens the file under cursor in a new tab while keeping the Oil view in the current tab.- Plugin:
src/oil/manager.ts(openEntryAtCursorInNewTab),src/oil/keybindings.ts(mapping + action)
- Plugin:
- Oil
<C-s>/<C-h>split open — new:oilopensvand:oilopenshex commands mapped to<C-s>(vertical split) and<C-h>(horizontal split), matching oil.nvim’s defaults. Opens the file under cursor in a split pane alongside the Oil view.- Plugin:
src/oil/manager.ts(openEntryAtCursorInSplit),src/oil/keybindings.ts(mappings + actions)
- Plugin:
- Oil
<C-c>close —<C-c>now maps to:oilclose, matching oil.nvim’s default close binding.qremains as an additional close key.- Plugin:
src/oil/keybindings.ts(mapping)
- Plugin:
- Oil
gxopen in default app — new:oilopenexternalex command mapped togx, matching oil.nvim’s default. Opens the file under cursor in the system’s default application viaapp.openWithDefaultApp().- Plugin:
src/oil/manager.ts(openEntryExternalAtCursor),src/oil/keybindings.ts(mapping + action)
- Plugin:
Tests
- 13 unit tests in
test/unit/global-key-handler.test.ts: dispatch count for builtin actions (count=0, count=1, count=3, count reset after dispatch), dispatch count for obcommand actions (once without count, N times with count), gt tab navigation issue #97 (gt without count → next tab, 3gt → nth tab, 1gt → nth tab), sequence timeout with partial matches (keeps alive, dispatches after restart, resets on no match, no lingering after exact match) - 4 unit tests in
test/unit/global-defaults.test.ts: gotoNthTab via gt mapping (skips sidebar leaves, first root tab for count=1, no-op when count exceeds tabs, workspace:next-tab for count=0) - 11 e2e tests in
test/specs/global-nav.e2e.ts(issue #97): editor-mode Ngt (gt without count → next tab not first, 1gt → first, 2gt → second, 3gt → third, 9gt → stays), non-editor-mode Ngt (gt → next, 1gt → first, 2gt → second, 3gt → third, 9gt → stays), sequence timeout updated (partial match keeps sequence alive) - 12 unit tests in
test/unit/oil-cache-sync.test.ts: cache ID synchronization after render (5 tests),getEffectiveShowHiddenoverride logic (5 tests),renderDirectoryat vault root (2 tests) - 11 e2e tests in
test/specs/oil-poc.e2e.ts: vault root folder navigation (2 tests), title bar update on directory change (2 tests), hidden files toggle (1 test), same-leaf file open (1 test),<C-t>keymap registration (1 test), vertical and horizontal split open (2 tests),gxmethod registration (1 test), Obsidian reload for split cleanup (1 test) Modalclass added totest/unit/__mocks__/obsidian.tsto unblock unit tests importingmanager.ts
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked Oil non-editor context degradation as fixed; updated vim state per-editor note withensureVimExtension()safety netCONTRIBUTING.md: Updatedembeddable-editor.tsdescription (ensureVimExtension safety net) andmanager.tsdescription (leaf priming)AGENTS.md: Updated dual-vim architecture section with embedded editor vim injection and safety netdocs/features/oil-explorer.md: Added non-editor context opening noteKNOWN_LIMITATIONS.md: Added which-key popup timeout fix and gt/Ngt tab navigation fixesdocs/features/workspace-navigation.md: Added Ngt count support description and which-key timeout fix notedocs/reference/keybindings.md: Already hadNgtrow — no change neededCONTRIBUTING.md: Updatedglobal-key-handler.tsandglobal-defaults.tsdescriptionsKNOWN_LIMITATIONS.md: Marked Oil cache desync, title bar, and hidden toggle as fixed; added<CR>same-leaf fix; added new keymaps (<C-t>,<C-s>,<C-h>,<C-c>,gx)docs/features/oil-explorer.md: Updated Oil ex commands table with new keymapsdocs/features/ex-commands.md: Updated Oil ex commands table with new keymapsdocs/reference/keybindings.md: Updated Oil keybindings table with new keymapsCONTRIBUTING.md: Updated Oil keybindings descriptionREADME.md: Updated Oil feature description with oil.nvim-matching keybindings
[0.90.0] - 2026-07-30
Fixed
- Note freezes in Reading Mode after closing Oil explorer — closing the Oil explorer view (via
q,:q,:wq, or Luavim.ob.oil.close()) reopened the previous file in Obsidian’s default mode (often Reading/Preview) instead of the mode the user was in when they opened Oil. Root cause:openOil()capturedpreviousFile(path only) but not the editor’s view mode. Fixed by capturingpreviousViewMode(theMarkdownViewstate: source mode, live preview, or reading mode) when opening Oil and restoring it vialeaf.openFile(file, { state: previousViewMode })on close. All 4 close paths (keybindingsq, ex commands:q/:wq, and Lua APIvim.ob.oil.close()) are unified into a singlecloseOil()method onOilManager. (#93)- Plugin:
src/oil/oil-view.ts(previousViewModefield,getState/setStateextended,getPreviousViewModegetter),src/oil/manager.ts(openOilcaptures mode viaMarkdownView.getState(), newcloseOil()shared method with mode restoration),src/oil/keybindings.ts(oilClosedelegates tomanager.closeOil()),src/workspace/commands.ts(closeOilViewdelegates tooilManager.closeOil()),src/main.ts(Lua APIoilClosedelegates tooilMgr.closeOil())
- Plugin:
- Cursor focus lost when switching back to Oil tab — after opening a file from Oil and then switching back to the Oil tab via
gTor Obsidian’s tab navigation, the cursor focus was missing. Keystrokes were not captured by the Oil editor until the user clicked with the mouse. Root cause: Oil’s editor focus was set only once inonOpen()and never re-applied when switching back. Fixed by adding afocusEditor()method toOilViewand calling it fromOilKeybindingManager.onActiveLeafChange()when switching into an Oil view. (#93)- Plugin:
src/oil/oil-view.ts(focusEditor()public method),src/oil/keybindings.ts(onActiveLeafChangecallsview.focusEditor()when switching to Oil)
- Plugin:
:Oil .opens current file’s directory instead of vault root — running:Oil .opened the directory containing the current active file rather than the vault root. In oil.nvim,.means current working directory, which maps to the vault root in Obsidian. Root cause: the conditionif (!dirPath || dirPath === '.' || dirPath === '/')treated.identically to an empty argument. Fixed by separating.and/into their own branch that resolves to vault root (""), while the empty-argument case continues to resolve to the current file’s parent directory. Both the ex command handler (commands.ts) and global ex command handler (global-ex-command.ts) are updated. (#93)- Plugin:
src/workspace/commands.ts(:Oilex command path resolution),src/ui/global-ex-command.ts(global ex command path resolution)
- Plugin:
- Hidden files (dotfiles) not shown in Oil explorer — hidden files and folders (e.g.,
.gitignore,.git/) were not visible in Oil even with “Show hidden files” enabled. Root cause:app.vault.getFiles()andapp.vault.getAllFolders()only return Obsidian-indexed files, and Obsidian does not index dotfiles. Fixed by adding a two-pass rendering approach: the initial sync render uses the Vault API (unchanged), then an async second pass discovers hidden entries viaapp.vault.adapter.list()(which returns all filesystem entries including dotfiles) and merges them into the listing. A race condition guard prevents overwriting user edits during the async merge. Hidden files are currently view-only — CRUD operations on dotfiles may fail because they lackTFile/TFolderobjects in the Vault index. (#93)- Plugin:
src/oil/render.ts(exportedgetParentPath/isInConfigDir, newdiscoverHiddenEntries()function),src/oil/manager.ts(newdiscoverAndMergeHidden()method with race condition guard),src/oil/oil-view.ts(setEditorContent()method, async trigger inonOpen()andrefreshContent())
- Plugin:
- Inconsistent behavior when deleting surroundings with doubled symmetric delimiters —
ds$on$$example$$did nothing instead of deleting the innermost$pair to produce$example$. Same failure fords"on""hi"",cs$on$$example$$, and other symmetric (same open/close) surround characters when doubled. Root cause:findSurroundingQuotes()in the codemirror-vim fork paired all quote positions sequentially at even/odd indices (i += 2). For$$example$$with positions[0, 1, 9, 10], this created pairs(0,1)and(9,10)— the two adjacent$$on each side — leaving the cursor between them with no match. Fixed by replacing the sequential pairing with cursor-expansion: search backward from cursor for the nearest quote character (open), then forward for the next one (close). This correctly handles both doubled delimiters ($$example$$→ finds inner pair(1, 9)) and adjacent pairs ("hello" "world"→ finds pair around cursor). (#96)- Fork:
~/Repos/codemirror-vim/src/vim.js(findSurroundingQuotes— cursor-expansion algorithm replacing sequentiali += 2pairing) - Fork:
~/Repos/codemirror-vim/DIFFERENCES.md(added “Symmetric surround quote matching” section)
- Fork:
- Snippet ex commands do not work after vimrc/Lua config reload —
:snippet <name>and:snippetsex commands silently stopped working after anyreloadFeatures()cycle (triggered by vimrc loading, Lua config loading, or settings changes). Root cause:registerSnippetCommands()was called only inonload(), butreloadFeatures()callsunregisterAll()which replaces all registered ex commands with no-ops — and snippet commands were never re-registered. The Picker-based snippet insertion was unaffected because it uses a separatepickerRegistrynot managed byVimRegistration. Fixed by addingregisterSnippetCommands()toreloadFeatures(), matching the pattern used by all other feature registrations. (#95)- Plugin:
src/main.ts(reloadFeatures— addedregisterSnippetCommandscall gated byenableSnippets)
- Plugin:
- Which-key shows EasyMotion commands incorrectly with space leader — EasyMotion commands (prefixed with
<leader><leader>) appeared at the wrong level in the which-key popup when using space as the leader key. Two root causes: (1)LeaderRegistry.addBinding()stripped the leader prefix using the raw leader key (" "), butonKeyPressLeaderOnly()compared against normalized keys ("<Space>"fromvim-keypressevents). The stored binding keys (" f") never matched the normalized drill-down prefix ("<Space>"). Similarly,addGroupLabel()stored the group label key in raw format, causinggetRelativeGroupLabels()lookups to miss. Fixed by normalizing bothlhsandprefixvianormalizeVimKey()at storage time inaddBinding()andaddGroupLabel(). (2) In grouped mode,buildNextKeyEntries()calledisSpecialKey()to filter out non-typeable keys like<CR>,<Left>, etc. — but<Space>was also treated as special, causing all EasyMotion bindings (whose first key after leader-stripping is<Space>) to be silently dropped from the grouping. Fixed by exempting<Space>from the special key check. (#94)- Plugin:
src/ui/which-key.ts(LeaderRegistry.addBinding— normalizelhsand leader before stripping;LeaderRegistry.addGroupLabel— normalizeprefixbefore storing;isSpecialKey— exempt<Space>from special key filtering)
- Plugin:
Tests
- 6 fork tests in
~/Repos/codemirror-vim/test/vim_test.js:ds_doubled_dollar_deletes_inner,ds_doubled_quote_deletes_inner,cs_doubled_dollar_changes_inner,ds_single_dollar_pair,ds_adjacent_dollar_pairs,ds_dollar_cursor_on_delimiter - 5 e2e tests in
test/specs/surround.e2e.ts(doubled symmetric delimiters — #96):ds$on$$example$$in Live Preview,ds"on""hi"",cs$on$$example$$,ds$on single$hello$,ds$on adjacent$hello$ $world$ - 28 unit tests in
test/unit/which-key.test.ts:LeaderRegistrynormalization (raw space leader, pre-normalized leader, format consistency, non-leader rejection, bare-leader rejection, deduplication, backslash leader, comma leader), group label normalization (raw vs normalized prefix, cross-format consistency),clearBuiltinBindingswith normalized keys, double-leader drill-down (issue #94 scenario — EasyMotion bindings filterable by<Space>prefix, single-leader bindings excluded),isSpecialKey(<Space>exempt, other angle-bracket keys special, plain keys not special) - 2 e2e tests unskipped in
test/specs/snippets/snippet-variables.e2e.ts::snippetcommand expands by name,:snippetsopens picker - 1 e2e test in
test/specs/settings-reload.e2e.ts: snippet ex commands survivereloadFeatures()(regression test for #95) - 17 unit tests in
test/unit/oil-render.test.ts:getParentPath(4 tests),isInConfigDir(4 tests),discoverHiddenEntries(9 tests — dotfiles, dot-folders, index exclusion, config dir exclusion, non-dotfile exclusion, adapter.list failure graceful fallback, nested paths, mixed entries, empty results) - 5 e2e tests in
test/specs/oil-poc.e2e.ts(Oil explorer #93)::Oil .opens vault root,:Oil /opens vault root, closing oil restores source mode, closing oil restores live preview mode,closeOil()restores previous file
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added surround doubled symmetric delimiter fixAGENTS.md: Updated fork test count (1882)DIFFERENCES.md(fork): Added “Symmetric surround quote matching” sectiondocs/features/surround.md: Added doubled delimiter behavior noteKNOWN_LIMITATIONS.md: Added hidden files view-only limitation to Oil section; marked Reading Mode freeze, focus loss, and:Oil .path resolution as fixedCONTRIBUTING.md: Updated Oil codebase structure descriptions (oil-view.ts,manager.ts,render.ts)docs/features/oil-explorer.md: Updated with mode restoration on close, focus restoration on tab switch,:Oil ./:Oil /path semantics, hidden files via adapter API, view-only dotfile limitationdocs/features/ex-commands.md: Updated:Oilargument descriptiondocs/reference/keybindings.md: Updated:Oildescription with.//path supportKNOWN_LIMITATIONS.md: Marked ex command snippet expansion as fixed; added which-key EasyMotion double-leader fix to which-key overlay sectiondocs/features/snippets.md: Updated ex command trigger description noting reload survivaldocs/configuration/which-key.md: Added note about double-leader prefix grouping for EasyMotion
[0.89.0] - 2026-07-30
Fixed
- Insert-mode surround dot-repeat —
.afteri<C-G>s{char}text<Esc>now replays the full surround + typed text. Previously, dot-repeat replayed only the typed text without delimiters. The fork stores_surroundInsertCharand_surroundInsertNewlineonlastInsertModeChanges. During replay,replaySurroundAwareInsert(insiderepeatLastEdit) strips the delimiter entry fromchanges[0], insertspair.open, replays typed text viarepeatInsert, then insertspair.close. Wrapped incm.operation()for undo atomicity. Counted dot-repeat (2.) repeats the text inside one set of delimiters. This exceeds both vim-surround and nvim-surround, where insert-mode surround dot-repeat is broken (nvim-surround #301). (#82)- Fork:
~/Repos/codemirror-vim/src/vim.js(createInsertModeChanges,recordLastEdit,surroundInsert,surroundInsertNewline,replaySurroundAwareInsertinrepeatLastEdit,onCursorActivity),~/Repos/codemirror-vim/src/types.ts(_surroundInsertChar,_surroundInsertNewlinefields)
- Fork:
Tests
- 9 fork tests in
~/Repos/codemirror-vim/test/vim_test.js:dot_insert_surround_unspaced,dot_insert_surround_spaced,dot_insert_surround_quotes,dot_insert_surround_empty,dot_insert_surround_counted,dot_insert_surround_no_cross_session_leak,dot_insert_surround_no_leak_after_o,dot_insert_surround_before_text_lost,dot_insert_surround_alias_b
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked insert-mode surround dot-repeat as fixed; separated macro recording limitation into own sectionREADME.md: Updated surround feature description with insert-mode dot-repeatAGENTS.md: Updated codemirror-vim fork description with insert-mode surround dot-repeat and test count (1870)DIFFERENCES.md(fork): Updated insert-mode surround section with dot-repeat implementation detailsdocs/features/surround.md: Updated insert mode and dot-repeat sections with insert-mode dot-repeat behavior
[0.88.0] - 2026-07-30
Added
- Yank-ring dot-repeat — pressing
.after paste cycling (p+<C-p>/<C-n>) now repeats the final cycled text instead of the original paste. On cycling exit, the final cycled content is written to the original paste register. The fork’srepeatLastEditre-reads the register at replay time. Follows yanky.nvim’supdate_register_on_cyclesemantics. System clipboard registers ("+/"*) are excluded.- Plugin:
src/vim/yank-ring.ts(originalPasteRegistertracking,getPasteRegisterName(), register write incancel(),setVim())
- Plugin:
undefineExfork API — the codemirror-vim fork now exposesVim.undefineEx(name)to remove ex commands registered viadefineEx. Cleans both theexCommandsfunction map andcommandMap_prefix lookup. Returnstrueif the command existed,falseotherwise.- Fork:
~/Repos/codemirror-vim/src/vim.js(undefineExonvimApi)
- Fork:
- Exmap unregistration on vimrc soft-reload — removing an
exmapdefinition from the vimrc file now unregisters the old handler on save. Exmap names are tracked per vimrc load invimrcExmapNamesand cleaned viaundefineExbefore re-applying on soft-reload. Plugin-defined and fork built-in ex commands are unaffected.- Plugin:
src/main.ts(vimrcExmapNamesSet, cleanup insoftReloadVimrc),src/vimrc/loader.ts(exmapNamesinApplyResultandVimrcLoadResult),src/types/vim-api.d.ts(undefineExtype,setTexton registers,lastEditInputStateonVimState)
- Plugin:
Tests
- 3 fork tests in
~/Repos/codemirror-vim/test/vim_test.js:ex_undefineEx(define → undefine → verify removed),ex_undefineEx_nonexistent(returns false),ex_undefineEx_short_name(short name prefix cleaned) - 3 e2e tests in
test/specs/yank-ring.e2e.ts: dot-repeat after single cycle pastes cycled text, dot-repeat without cycling pastes original (regression), single cycle then dot-repeat pastes cycled text - 4 e2e tests in
test/specs/vimrc-exmap-reload.e2e.ts:vimrcExmapNamesfield exists,undefineExavailable on Vim API, returns false for nonexistent, defineEx + undefineEx round-trip with built-in survival
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Removed:earlier/:laterand:signfrom N/A table (contradicted implemented features); clarified flash dot-repeat as working correctly; updated exmap soft-reload to reflect unregistration support; updated yank-ring dot-repeat as fixedREADME.md: Updated yank-ring feature description with dot-repeatAGENTS.md: Updated codemirror-vim fork description withundefineExAPI and test countdocs/features/quality-of-life.md: Updated yank-ring section with dot-repeat behaviordocs/configuration/vimrc.md: Updated soft-reload section — exmap removal now works
[0.87.0] - 2026-07-29
Added
- Hotkey conflict detection wizard — on plugin load, detects when Obsidian’s default hotkeys (Ctrl+W, Ctrl+D, Ctrl+F, Ctrl+B) conflict with workspace navigation keys. Shows a one-time Notice per plugin version with a “Check hotkey conflicts” button in Settings → Vim Motions → Navigation that lists each active conflict with step-by-step unbinding instructions. Skipped on mobile and when workspace nav is disabled.
- Plugin:
src/workspace/hotkey-conflicts.ts(new — conflict detection viahotkeys.json,VimInfoModaldisplay),src/settings.ts(conflictNoticeDismissedVersionsetting, button in both declarative and imperative settings paths),src/main.ts(detection ononLayoutReady)
- Plugin:
- Per-view
CursorMoved/TextYankPost/CursorHold/CmdlineEnter/CmdlineLeaveautocmd events — extends the per-view autocmd pattern (already implemented for mode events viaAutocmdModeWatcher) to 5 additional events.CursorMovedfires independently per view with position-change detection (only fires when cursor actually moved).TextYankPostfires from any view including popovers.CursorHoldfires per-view with configurable delay. Built-in vim mode retains active-leaf-only behavior.- Plugin:
src/vim/autocmd-event-watcher.ts(new —AutocmdEventWatcherViewPlugin),src/lua/autocmd.ts(useEventViewPluginflag, per-view handler methods, gated legacy bindings),src/main.ts(extension registration, callback wiring, hold delay sync)
- Plugin:
- Visual-mode paste cycling — yank-ring paste cycling now works after visual-mode paste (
viw+p+<C-p>to cycle). Detects visual paste via anchor/cursor position comparison at snapshot time. Computes paste range via doc-length arithmetic. Visual block paste is excluded. Normal-mode paste cycling is unaffected.- Plugin:
src/vim/yank-ring.ts(snapshot()helper,posMin(), visual paste detection inonCommandDone,prevAnchor/prevDocLength/prevSelectionLength/prevVisualLine/prevVisualBlocktracking)
- Plugin:
- Console warning for unknown
setoptions — unknownsetoptions in vimrc now produce aconsole.warnon first encounter per vimrc load. Options recognized by either the plugin (KNOWN_SET_OPTIONS) or CM Vim built-in options are not warned about. Deduplication prevents repeated warnings for the same option.- Plugin:
src/vimrc/loader.ts(KNOWN_CM_VIM_OPTIONSset,warnedSetOptionsdeduplication,clearSetOptionWarnings())
- Plugin:
Changed
- Flash count prefix now honored with labels —
3f{char}with 2+ matches now jumps directly to the 3rd match without showing the label overlay. When the count exceeds available matches, the last match is used (Neovim parity).f{char}without a count prefix still shows labels for 2+ matches. Works in operator-pending mode (d3f{char}) and witht/Ttill motions.- Plugin:
src/flash/char-mode.ts(count prefix check before label overlay)
- Plugin:
- Flash dot-repeat clarified — dot-repeat after
df{char}{label}already works correctly. The fork stores the resolved position via_asyncMotionTargetandrepeatLastEditreplays the operator to the same relative offset. The label UI does not re-appear (correct vim behavior). KNOWN_LIMITATIONS.md entry clarified.
Fixed
- Cursor focus lost when pressing Tab to navigate cells in Embedded table widget — pressing
Tabin insert mode inside an embedded table cell editor froze the editor. The cursor disappeared, vim mode got stuck in Insert mode, andEscapestopped working. Root cause:exitCellEdit()scheduled a 50msrefreshAfterOp()timer that was non-cancellable and had no state guard. WhenTabcalledexitCellEdit()→enterCellEdit()synchronously, the deferred refresh fired while the new cell editor was active — removing its key handlers, potentially rebuilding the widget DOM (orphaning the editor), and leaving the controller in an inconsistent state (cell-editstate withtable-navhandlers). Fixed with four layers of defense: (1)refreshAfterOp()now stores and deduplicates the timer ID in arefreshTimermember, cancelled inexitTable(),enterCellEdit(), anddestroy(). (2)doRefreshAfterOp()guards against firing incell-editorinactivestate. (3)exitCellEdit()accepts{ skipRefresh: true }— the Tab handler skips bothsetActiveEditTableRange(null)andrefreshAfterOp()to prevent widget DOM rebuilds during cell-to-cell transitions. (4)enterCellEdit()cancels any pending refresh timer as belt-and-suspenders protection. Additionally,Tabat the last cell of the last row (orShift-Tabat the first cell) now returns to table-nav mode instead of silently re-entering the same cell. (#92)- Plugin:
src/vim/table-nav-controller.ts(refreshTimermember,refreshAfterOptimer storage/dedup,doRefreshAfterOpstate guard,exitCellEditskipRefreshparam,handleCellEditKeyrewrite with widget re-query and boundary handling,enterCellEdittimer cancel andpendingDcleanup)
- Plugin:
- Visual mode highlighting in embedded table cell editors — entering charwise visual mode (
v) in an embedded table cell editor now shows selection highlighting. The cell editor’s CM6 instance doesn’t receive.cm-focused, which previously caused the browser to hide::selectionhighlights. Fixed by adding aCSSStyleSheetondocument.adoptedStyleSheetsthat forces::selectionvisibility in.cm-vimVisual:not(.cm-vimVisualLine)scoped to.vim-table-cell-editor. Linewise visual mode (V) already worked via the fork’s focus-independentlinewiseVisualHighlightViewPlugin. (#19)- Plugin:
src/vim/table-cell-editor.ts(visualSelectionSheetviaadoptedStyleSheets)
- Plugin:
- Undo tree memory eviction on file close — in-memory undo trees (
undoTreeMap) are now evicted when all editors for a file are closed, preventing unbounded memory growth in long sessions. Dirty trees are persisted before eviction whenundoFileis enabled. Persisted data on disk is not deleted — reopening a file restores from persistence or starts fresh.- Plugin:
src/main.ts(undo tree eviction inactive-leaf-changehandler)
- Plugin:
- Undo tree stale-tree notification — when
undoFileis enabled and a file was modified outside Obsidian between sessions, an Obsidian Notice is now shown when the persisted undo tree’sdocLengthdoesn’t match the current file size. Detection fires at most once per file per session. Legacy persisted trees withoutdocLengthgracefully skip the check.- Plugin:
src/main.ts(activateUndoTreeForFile—docLengthcomparison + Notice),src/vim/undo-tree.ts(docLengthfield onSerializedUndoTree)
- Plugin:
vim.v.insertmodenow populated — returns'i'for insert mode,'r'for replace mode (R),'v'for virtual replace mode (gR), and''in normal/visual modes. Available in keymap function callbacks viagetInsertModeChar(). Autocmd callbacks default to''(no adapter context available).- Plugin:
src/lua/api.ts(getInsertModeCharhelper,insertmodeadded to 5setVimVContextcallsites)
- Plugin:
Tests
- 2 unit tests in
test/unit/undo-tree.test.ts:docLengthround-trip preservation, legacy data withoutdocLengthgraceful deserialization - 9 unit tests in
test/unit/lua/vim-v.test.ts:insertmode'r'/'v'context values, 7getInsertModeChartests (null, normal, insert, replace, virtual replace, priority, missing state) - 2 unit tests in
test/unit/textarea-vim.test.ts:clearSetOptionWarningsexport and idempotency - 5 e2e tests in
test/specs/flash-char-mode.e2e.ts:3fadirect jump,5faclamp to last match,2fadirect jump,d3faoperator-pending,1fasingle match - 8 unit tests in
test/unit/hotkey-conflicts.test.ts: conflict array structure, detection logic (empty, full, partial, custom binding, unrelated keys) - 10 unit tests in
test/unit/vim/autocmd-event-watcher.test.ts: callback wiring (set/clear/extension), CursorMoved detection (fires on move, skips unchanged, fires on each distinct move), CursorHold timer (fires after delay, resets on new move, custom delay), TextYankPost - 1 e2e test in
test/specs/yank-ring.e2e.ts: normal-mode paste cycling regression after visual-paste changes
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Reorganized — moved 20 fixed top-level sections to new “Resolved Issues” section at bottom; updated flash count prefix, undo tree eviction, undo tree stale notification, visual mode cell editor,vim.v.insertmode, exmap soft-reload, unknown set option, flash dot-repeat, hotkey conflicts, per-view autocmd events, visual paste cycling, and SettingDefinitionList investigation entriesdocs/features/flash.md: Added count prefix behavior and dot-repeat notedocs/features/undo-tree.md: Updated memory management and stale-tree notificationdocs/features/tables.md: Updated visual mode highlighting fix in cell editorsdocs/features/quality-of-life.md: Updated yank-ring with visual-mode paste cyclingdocs/configuration/lua-config.md: Updatedvim.v.insertmodefrom deferred to active, updated per-view autocmd event listdocs/configuration/vimrc.md: Added unknown set option warning behavior, updated exmap soft-reloaddocs/configuration/settings.md: Added hotkey conflict detection button to workspace navigation settingsdocs/getting-started/recommended-setup.md: Added hotkey conflict wizard noteREADME.md: Updated flash motions, workspace navigation, and Lua configuration feature descriptionsAGENTS.md: Updated autocmd event list with per-view CursorMoved/TextYankPost/CursorHold/CmdlineEnter/CmdlineLeave
[0.86.0] - 2026-07-28
Fixed
- Which-key displays inaccurate count of group subcommands in “all” mode — when
whichKeyModewas set to “All partial keys” and the user pressed the leader key, the which-key popup showed wildly inflated(+N)group counts (e.g.,(+418)instead of(+21)). The “All partial keys” code path (showCompletions()) queriedvim.getCompletions()from the CM vim engine, which returns the entiredefaultKeymaparray — including built-in defaults, plugin-internal keymaps, and user-defined keymaps — instead of using only theleaderBindingsregistry (which contains only user-visible leader keymaps). The “Leader key only” mode (showLeaderBindings()) was unaffected because it already usedleaderBindingsdirectly. Fixed by adding anisLeaderScopebranch inshowCompletions()that mirrorsshowLeaderBindings()— building entries fromthis.leaderBindingsfiltered by the current prefix, with correct label/icon/color resolution and leader-style title formatting. Non-leader completions (g,z,d, etc.) continue usingvim.getCompletions()as before. (#91)- Plugin:
src/ui/which-key.ts(showCompletions—isLeaderScopebranch, deferredgetCompletionsto non-leaderelsebranch)
- Plugin:
- Several
vim.optand vimrcsetoptions produce “unknown vim.opt option” warning — 12 plugin settings were documented but never registered in the Luavim.optproxy (KNOWN_SET_OPTIONS) or the vimrc:setpathway (vim.defineOption). Setting them viavim.opt.yankring = trueorset yankringin vimrc logged a console warning and had no effect. Fixed by adding all 12 options to both registries. Options now work identically across Settings UI, vimrc, and Lua. (#90)- Added to both
KNOWN_SET_OPTIONSandvim.defineOption:yankring,yankhighlightmode,yankhighlightduration,undotree,undofile,undotreemaxnodes,foldawarenavigation,foldpersistence,harpoon,dial - Added to
KNOWN_SET_OPTIONSonly (already hadvim.defineOption):jumplist,jumplistsize - Plugin:
src/vimrc/loader.ts(12 newKNOWN_SET_OPTIONSentries +setJumpListEnabled/setJumpListSizeimports),src/vim/options.ts(10 newvim.defineOptioncalls + 2 new exported setters)
- Added to both
Tests
- 1 e2e test in
test/specs/lua-space-leader.e2e.ts: which-key group count in “all” mode with space leader stays bounded to actual leader bindings (not inflated by engine-internal keymaps)
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added which-key inflated group count fix; added vim.opt/vimrc option parity fixdocs/configuration/lua-config.md: Added 8 missing options to vim.opt table (harpoon,dial,jumplist,foldawarenavigation,foldpersistence,jumplistsize,yankhighlightduration,yankhighlightmode)docs/configuration/vimrc.md: Added 6 missing options to vimrc tables (harpoon,dial,foldawarenavigation,foldpersistence,yankhighlightmode,yankhighlightduration)
[0.85.0] - 2026-07-28
Fixed
- Scroll jumps to cursor when interacting with Meta Bind or other plugin fields in the properties panel — the
propertiesFoldObserverinfold-sync.tswatched.metadata-containerfor anyclassattribute mutation and unconditionally dispatchedEditorView.scrollIntoView(selection.main.head). Plugins like Meta Bind that render interactive inputs in the properties area trigger class mutations that are not fold toggles, causing the editor to scroll back to the last vim cursor position. Fixed by addingattributeOldValue: trueto theMutationObserverconfig and comparing the old vs newis-collapsedclass presence — the observer now only firesscrollCursorIntoView()when the fold state actually changes. No-op mutations (identical class string) and non-fold mutations (any class other thanis-collapsed) are ignored. (#89)- Plugin:
src/vim/fold-sync.ts(propertiesFoldObserver—is-collapsedfilter,attributeOldValue: true)
- Plugin:
Tests
- 4 e2e tests in
test/specs/properties-fold-scroll.e2e.ts: non-fold class mutation preserves scroll position, no-op class re-assignment preserves scroll position, fold toggle triggers scroll, unfold toggle triggers scroll - Spike test
test/specs/spikes/spike-metabind-scroll-issue89.e2e.ts: 8 diagnostic tests confirming root cause (class mutation scroll jump, observer attribution, split-view behavior, class mutation audit)
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added properties fold observer scroll fixCONTRIBUTING.md: Updatedfold-sync.tsdescription withis-collapsedfilterdocs/features/workspace-navigation.md: Updated fold scroll behavior note
[0.84.0] - 2026-07-25
Fixed
- Hint mode labels missing on wikilinks and markdown links when cursor is on the same line — in Live Preview, wikilinks on the cursor’s line render as
.cm-hmd-internal-linkspans (not.cm-underline), and markdown links render as.cm-link/.cm-urlspans. These were not inTARGET_SELECTOR, so no hint labels appeared. In Source mode, wikilinks always render as.cm-hmd-internal-linkand were similarly missed. Fixed by adding.cm-hmd-internal-link,.cm-link, and.cm-urltoOBSIDIAN_SELECTORSand extendingclassifyTarget()to resolve links from these elements via the existingresolveCmUnderlineHref()pipeline. Deduplication filters prevent multiple hints per link: aliased wikilink sub-spans, nested.cm-underlineinside.cm-hmd-internal-link, formatting bracket spans, and markdown link URL spans when a text span exists. (#85)- Plugin:
src/ui/hint-mode.ts(added selectors, extendedclassifyTarget, deduplication filters increateHintAction)
- Plugin:
- Hint mode link resolution fails in Obsidian runtime —
getEditorViewFromElement()used the DOM.cmView.viewproperty to access the CM6 EditorView, but this property is not accessible in Obsidian’s runtime environment (only works in the WDIO test context). All resolved links returnedhref: undefined, causing hint labels to appear but do nothing when activated. Fixed by falling back to theMarkdownView.editor.cmpath (the same accessor used by the rest of the codebase viagetEditorView()insrc/util/editor.ts). (#85)- Plugin:
src/ui/hint-mode.ts(getEditorViewFromElementfallback viaapp.workspace.getActiveViewOfType(MarkdownView))
- Plugin:
- Hint mode does not open external URLs from editor links — when
resolveCmUnderlineHref()resolved an external URL (e.g.,https://example.com),hintActivate()fell through to the generic click handler because theisInternalLinkcheck excluded URLs starting withhttp://orhttps://. The generic click handler dispatches pointer/click events on<span>elements, which have no click handler and produce no effect. Fixed by adding an explicitwindow.open(linkHref)path for external URLs. (#85)- Plugin:
src/ui/hint-mode.ts(hintActivateexternal URL branch)
- Plugin:
Tests
- 9 new e2e tests in
test/specs/hint-mode-links.e2e.ts: Source mode navigation (plain, aliased, inline wikilinks), cursor-on-line Live Preview navigation, multiple wikilinks on same line, aliased wikilink deduplication,yfyank on wikilink,Fopen-in-new-tab on wikilink, embed wikilink hint visibility - Mode-switching helpers (
ensureLivePreview,ensureSourceMode,isLivePreview,isSourceMode) extracted totest/helpers.ts - Spike test
test/specs/spikes/spike-hint-wikilink-issue85.e2e.ts: 17 diagnostic tests probing DOM element discovery,posAtDOMmapping accuracy,findLinkAtCursorresolution, and end-to-end hint activation across Live Preview, Source mode, and Reading view
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated hint mode target classification with.cm-hmd-internal-link,.cm-link,.cm-urlselectors, deduplication filter descriptions, EditorView fallback, and external URL handlingCONTRIBUTING.md: Updatedhint-mode.tsdescription with cursor-on-line and Source mode link resolution, EditorView MarkdownView fallback, external URL handlingdocs/features/hint-mode.md: Updated link handling section with Source mode support, cursor-on-line behavior, and external URL opening
[0.83.0] - 2026-07-25
Fixed
- Autocmd mode events only fire in the active editor leaf —
InsertEnter,InsertLeave, andModeChangedautocmd events now fire per-view across all editors (split panes, popover hover-preview editors, canvas card text inputs) when using the bundled vim fork. Previously, these events only fired for the active workspace leaf because theAutocmdManagerbound to a single adapter viaonActiveLeafChange(). Non-leaf editors (popovers, canvas cards) never triggeredactive-leaf-change, so autocmd callbacks for mode events never executed in these contexts. Fixed by addingAutocmdModeWatcher, a CM6ViewPluginthat hooksvim-mode-changeper-EditorView and fires mode events throughAutocmdManager.fire(). The ViewPlugin is registered viaregisterEditorExtension()and automatically applies to all editors. The single-adapter mode-change binding inbindAdapter()andactivate()is gated by auseViewPluginflag — when the ViewPlugin is active (bundled vim mode), the legacy binding is skipped. Built-in vim mode retains the existing active-leaf-only behavior. Other adapter-dependent events (TextYankPost,CursorMoved,CursorHold,CmdlineEnter,CmdlineLeave) remain active-leaf-only for v1. (#88)- Plugin:
src/vim/autocmd-mode-watcher.ts(new —AutocmdModeWatcherViewPlugin,setAutocmdModeCallbacks/clearAutocmdModeCallbacks),src/lua/autocmd.ts(useViewPluginflag,setUseViewPlugin(),handleModeChangeFromView(), guardedonModeChangeinactivate()andbindAdapter()),src/main.ts(extension registration, callback wiring inloadLuaConfigInternal, cleanup inonunload)
- Plugin:
- Hint mode labels missing on wikilinks and markdown links when cursor is on the same line — in Live Preview, wikilinks on the cursor’s line render as
.cm-hmd-internal-linkspans (not.cm-underline), and markdown links render as.cm-link/.cm-urlspans. These were not inTARGET_SELECTOR, so no hint labels appeared. In Source mode, wikilinks always render as.cm-hmd-internal-linkand were similarly missed. Fixed by adding.cm-hmd-internal-link,.cm-link, and.cm-urltoOBSIDIAN_SELECTORSand extendingclassifyTarget()to resolve links from these elements via the existingresolveCmUnderlineHref()pipeline. Deduplication filters prevent multiple hints per link: aliased wikilink sub-spans, nested.cm-underlineinside.cm-hmd-internal-link, formatting bracket spans, and markdown link URL spans when a text span exists. (#85)- Plugin:
src/ui/hint-mode.ts(added selectors, extendedclassifyTarget, deduplication filters increateHintAction)
- Plugin:
Tests
- 3 new unit tests in
test/unit/lua/autocmd.test.ts:handleModeChangeFromViewfires events,bindAdapterskips mode-change whenuseViewPluginis true,activateskipsonModeChangewhenuseViewPluginis true - 7 unit tests in
test/unit/vim/autocmd-mode-watcher.test.ts: callback set/clear/overwrite, extension creation, mode payload forwarding, cleanup after clear - 4 e2e tests in
test/specs/lua-autocmd-perview.e2e.ts: InsertEnter fires exactly once in active leaf (no double-firing), InsertEnter fires in non-active split viaVim.handleKey, ModeChanged fires in non-active split with correct pattern, InsertLeave fires in non-active split - Spike tests:
test/specs/spikes/spike-autocmd-multiview.e2e.ts(13 tests — multi-view event discovery),test/specs/spikes/spike-autocmd-popover-timing.e2e.ts(12 tests — popover/timing analysis) - 9 new e2e tests in
test/specs/hint-mode-links.e2e.ts: Source mode navigation (plain, aliased, inline wikilinks), cursor-on-line Live Preview navigation, multiple wikilinks on same line, aliased wikilink deduplication,yfyank on wikilink,Fopen-in-new-tab on wikilink, embed wikilink hint visibility - Mode-switching helpers (
ensureLivePreview,ensureSourceMode,isLivePreview,isSourceMode) extracted totest/helpers.ts - Spike test
test/specs/spikes/spike-hint-wikilink-issue85.e2e.ts: 17 diagnostic tests probing DOM element discovery,posAtDOMmapping accuracy,findLinkAtCursorresolution, and end-to-end hint activation across Live Preview, Source mode, and Reading view
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added per-view mode events section documenting which events fire per-view, which remain active-leaf-only,getModeState()semantics, andvim.obsidian.mode()behavior; updated hint mode target classification with.cm-hmd-internal-link,.cm-link,.cm-urlselectors and deduplication filter descriptionsdocs/configuration/lua-config.md: Added per-view callout to autocommands section, markedInsertEnter/InsertLeave/ModeChangedas “(per-view)” in events tabledocs/features/hint-mode.md: Updated internal link handling section with Source mode support and cursor-on-line behaviorCONTRIBUTING.md: Addedautocmd-mode-watcher.tsto codebase structure; updatedhint-mode.tsdescription with cursor-on-line and Source mode link resolutionAGENTS.md: Updated Lua API description noting per-view autocmd mode eventsREADME.md: Updated Lua configuration feature description with per-view mode events
[0.82.0] - 2026-07-24
Fixed
- Animated cursor does not animate for count-prefixed and multi-key motions — movements like
4j(count-prefixed) andg$(multi-key) caused the cursor to teleport instead of animating. TheresolveVimMode()method in the animated cursor controller usedvim.status(the chord display string) to detect operator-pending mode. Sincevim.statusis set on every keystroke (e.g.,"4"when typing a count digit,"g"when typing a prefix key), any multi-keystroke motion triggered a false mode change to operator-pending — which has a different cursor shape (underline vs block). Each shape change calledsnap(), bypassing the animation entirely. Fixed by removingvim.statusfrom the operator-pending detection — onlyinputState.operator(set when an actual operator liked/c/yis registered) now gates the operator-pending mode. (#86)- Plugin:
src/vim/animated-cursor/controller.ts(resolveVimMode— removedvim.statuscheck)
- Plugin:
- Hint mode does not navigate wikilinks or markdown links in Live Preview — typing the hint label for a wikilink (
[[Target]]) or markdown link ([text](Target)) in the editor did nothing. The.cm-underlinespans rendered by Live Preview are<span>elements withouthrefordata-hrefattributes —classifyTargetcorrectly identified them as links but extractedhref: undefined, causinghintActivateto fall through to the generic click handler (which does nothing useful on CM6 spans). Fixed by addingresolveCmUnderlineHref()which uses the CM6EditorView.posAtDOM()API to convert the DOM element to a document offset, then calls the existingfindLinkAtCursor()regex fromgoto-definition.tsto extract the link target from the raw markdown text. Works for wikilinks (including aliased and heading links), markdown links (internal and external), and bare URLs. Reading view and frontmatter property links were unaffected (they use<a>elements with properhref/data-hrefattributes). (#85)- Plugin:
src/ui/hint-mode.ts(getEditorViewFromElement,resolveCmUnderlineHref, updatedclassifyTargetlink branch)
- Plugin:
- Input method not restored after manual IME switch during insert mode — when a user manually switched input methods while in insert mode (e.g., from Vietnamese to English via OS keyboard shortcut), pressing
Escthenireset the IME to the original input method instead of preserving the manually chosen one. Thesave()method inImSwitchercached the stalelastKnownImvalue (set by the plugin’s lastset()call) before querying the OS for the actual current IME. The async OS query updatedlastKnownImbut never wrote back tosavedImByLeaf, sorestore()always read the stale value. Fixed by makingsave()async — it now queries the OS for the real IME state first, then caches the result in bothlastKnownImandsavedImByLeaf.onInsertLeave()awaits the save before switching to the normal-mode default IME. Falls back tolastKnownImwhen the OS query fails (e.g., binary timeout). (#83)- Plugin:
src/im/im-switcher.ts(save()async with OS query,onInsertLeave()awaits save,debouncedSwitch/pendingSwitchaccept async callbacks),src/lua/api.ts(imSavetype updated),src/lua/loader.ts(fire-and-forget async save),src/lua/obsidian-api.ts(void floating promise)
- Plugin:
Tests
- 10 e2e tests in
test/specs/hint-mode-links.e2e.ts: wikilink/markdown-link/bare-URL href resolution from.cm-underlinespans, wikilink navigation (plain and aliased), markdown link navigation, inline wikilink navigation, reading view regression, frontmatter property link regression, external link safety - Updated 10 unit tests in
test/unit/im-switcher.test.ts:save()tests now verify OS query behavior (mockexecuteImGetreturn value instead of manually settinglastKnownIm), async settle viavi.advanceTimersByTimeAsync(0), new test for fallback when OS query returns null
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added animated cursor multi-key motion fix; added hint mode link navigation fix to hint mode actions sectionCONTRIBUTING.md: Updatedhint-mode.tsdescription with link resolution viaposAtDOMdocs/features/hint-mode.md: Updated internal link handling section with Live Preview resolution detailsKNOWN_LIMITATIONS.md: Updated input method switching section with manual IME switch fixCONTRIBUTING.md: Updatedim-switcher.tsdescriptionAGENTS.md: No changes needed (existing description already covers per-view IM switching)
[0.81.0] - 2026-07-23
Fixed
- EasyMotion capital letter search not working — EasyMotion character search motions (
<leader><leader>s,<leader><leader>f, etc.) failed when typing a capital letter (Shift+key) as the search character. ThewaitForKey()handler resolved on theShiftkeydown event (before the actual character key arrived), causing the motion to silently abort. Fixed by adding a modifier-key guard matching the existing pattern inwaitForLabel()—e.key.length !== 1keys are now suppressed and ignored, keeping the handler alive for the real character. (#84)- Plugin:
src/easymotion/keypress.ts(waitForKeymodifier-key guard)
- Plugin:
Tests
- 6 unit tests in
test/unit/easymotion-keypress.test.ts:waitForKeyresolves single character keys and Escape, ignores Shift/Control/Alt/Meta modifier-only keys - 1 e2e test in
test/specs/easymotion-comprehensive.e2e.ts: EasyMotion bidirectional char search with capital letter (Z) input
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added capital letter fix to EasyMotion operator-pending sectionCONTRIBUTING.md: Updatedkeypress.tsdescription with modifier-key guarddocs/features/easymotion.md: Added note about capital letter support in find motions
[0.80.0] - 2026-07-23
Fixed
- IME change detection limited to primary editor leaf — IME composition tracking and input method switching now work across all editor views (split panes, Page Preview popovers, Canvas card editors). Previously, composition events and mode-change detection were wired to a single element/adapter obtained from
getActiveViewOfType(MarkdownView), so non-primary editors never received IME handling. Fixed with two new CM6 ViewPlugins registered viaregisterEditorExtension():CompositionTrackertrackscompositionstart/compositionendper-EditorView, andImModeWatcherbindsadapter.on('vim-mode-change')per-EditorView to detect insert mode transitions. The autocmd-based IM switch registrations (InsertEnter/InsertLeave/CmdlineLeave) are replaced by the per-view mechanism; Lua autocmd callbacks continue to fire for the primary leaf via AutocmdManager (unchanged contract). (#83)- Plugin:
src/im/composition-tracker.ts(new),src/im/im-mode-watcher.ts(new),src/im/im-switcher.ts(refactored — removed single-element tracking, addedcleanupView()),src/main.ts(registered extensions, removed autocmd-based IM registrations)
- Plugin:
Tests
- 15 unit tests in
test/unit/composition-tracker.test.ts: per-view composing state, multi-tracker isolation, destroy cleanup,onAllCompositionsEndcallback lifecycle, unsubscribe - 14 unit tests in
test/unit/im-mode-watcher.test.ts: lazy adapter binding, mode change detection (insert/leave/replace), adapter re-binding, cleanup, multiple views with unique IDs - 4 e2e tests in
test/specs/ime-composition-multiview.e2e.ts: composition tracking on active/non-active editors, independent per-view tracking, insert mode detection on non-active editor
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated input method switching section with multi-view fixREADME.md: Updated input method switching feature description with multi-view supportCONTRIBUTING.md: Addedcomposition-tracker.tsandim-mode-watcher.tsto codebase structureAGENTS.md: Updated Lua API description noting per-view IM switching
[0.79.0] - 2026-07-22
Added
vim.vnamespace — Neovim-compatible predefined variables — read-only metatable proxy exposingvim.v.count,vim.v.count1,vim.v.register,vim.v.operator(Tier 1),vim.v.searchforward(read/write),vim.v.insertmode,vim.v.numbermax/numbermin/numbersize,vim.v.true/false/null(Tier 2), andvim.v.foldstart/foldend/foldlevel/folddashes,vim.v.lnum/relnum/virtnum,vim.v.char,vim.v.hlsearch,vim.v.event(Tier 3 — context-dependent). Context is set fromactionArgsbefore each keymap callback invocation and cleared after.vim.v.eventis populated during autocmd dispatch with the event data table.vim.v.hlsearchqueries the fork’s search overlay state viagetSearchState(cm).getOverlay().- Plugin:
src/lua/api.ts(VimVContext,setVimVContext,clearVimVContext, vim.v metatable, autocmd vim.v.event wiring),src/lua/loader.ts(getVimApi,getSearchForward,setSearchForward,getHlSearchcallbacks),src/types/vim-api.d.ts(ActionArgsextended,feedKeysandgetOverlayadded toVimApi),src/lua/engine.ts(EXPR_INSTRUCTION_LIMIT)
- Plugin:
{ expr = true }keymap support —vim.keymap.setnow accepts{ expr = true }for function callbacks. The callback must return a string that is fed as keystrokes via the fork’s newfeedKeysAPI. Sync-only — async APIs cannot be used in expr callbacks. String expr (Vimscript evaluation) is not supported with a helpful error guiding users to the function form. Recursion guard (200 depth, matching Neovim) prevents infinite expr → feedKeys → expr loops.- Fork:
~/Repos/codemirror-vim/src/vim.js(feedKeysmethod on VimApi — delegates todoKeyToKeywith noremap flag and recursion protection) - Plugin:
src/lua/api.ts(expr callback path withlua_pcall(state, 0, 1, 0), return value capture,feedKeysinvocation)
- Fork:
Tests
- 55 unit tests in
test/unit/lua/vim-v.test.ts: Tier 1 defaults and context (count, count1, register, operator), read-only enforcement, Tier 2 constants (numbermax/min/size, true/false/null), searchforward (callback read/write), insertmode, Tier 3 fold/statuscolumn/event/char variables, hlsearch callback (read from getHlSearch, fallback to context), vim.v.event in autocmd context (multi-field, nested data), expr mapping registration, unknown keys - 8 e2e tests in
test/specs/lua-vim-v.e2e.ts:vim.v.countandvim.v.count1with and without typed counts,vim.v.eventpopulated during InsertEnter/InsertLeave autocmds,vim.v.hlsearchreturns 1 after/search and 0 after:nohlsearch - 7 e2e tests in
test/specs/lua-expr-mapping.e2e.ts: expr returns executed keys, expr with count, nil/empty return, error handling, string expr error, special keys
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added expr mapping limitations (string expr, async expr, operator-pending, count forwarding) and vim.v limitations (async callback reliability, outside-callback behavior)docs/configuration/lua-config.md: Addedvim.vsection with all variable tables, expr mapping examples, and updatedvim.keymap.setoptions table withexprdocumentationREADME.md: Updated Lua configuration feature description withvim.vand expr mappingsCONTRIBUTING.md: Updatedapi.tsdescription with vim.v namespace and expr mapping supportAGENTS.md: Updated codemirror-vim fork description withfeedKeysAPI, updated Lua API list withvim.v(20 variables including event/hlsearch)
[0.78.0] - 2026-07-22
Fixed
- Fold gutter click does not unfold (continued) — the initial fix in 0.76.0 (correcting zero-width ranges in the plugin’s own fold-column and statuscolumn gutters) was insufficient because those gutters are off by default — the reporter was clicking Obsidian’s native fold gutter, which the plugin doesn’t control. CM6’s
foldStaterequires an exact{from, to}match to remove a fold; a mismatched range is silently ignored. Fixed by addingunfoldNormalizerExtenderinfold-sync.ts— atransactionExtenderthat detects mismatchedunfoldEffectranges and appends a corrective effect with the actual stored fold range. Works for all fold sources: Obsidian’s native gutter, the plugin’s custom gutters, and vim commands. (#80)- Plugin:
src/vim/fold-sync.ts(unfoldNormalizerExtender)
- Plugin:
- Insert-mode surround cursor position and undo —
<C-G>s{char}now inserts both the opening and closing delimiters immediately (matching vim-surround behavior) instead of deferring the close delimiter toexitInsertMode. Fixes: (1) cursor now lands on the last typed character afterEsc(was on the closing delimiter), (2) undo is improved (was 3 steps: close, text, open — now 2 steps: text, delimiters), (3) dot-repeat degrades cleanly (replays only typed text, not garbled()hello). ThemaybeResetmechanism clears delimiter text from the insert-mode change stream solastInsertModeChanges.changescontains only user-typed text. Known limitation: dot-repeat replays only the typed text, not the surrounding delimiters. Macro recording of insert-mode surround keys is also not supported (pre-existing fork limitation). (#82)- Fork:
~/Repos/codemirror-vim/src/vim.js(surroundInsert,surroundInsertNewlinerefactored;exitInsertModedeferred-close block removed),~/Repos/codemirror-vim/src/types.ts(surroundInsertCloseproperty removed)
- Fork:
Tests
- 17 e2e tests in
test/specs/fold-unfold-normalizer.e2e.ts: unfold normalizer for heading folds (exact-match, zero-width, wrong-to, line-boundary, vim zc/zo round-trip, zM/zR round-trip, no-op on non-folded line, multi-fold targeting), frontmatter folds in source mode (exact-match, zero-width, line-start mismatch, line-boundary, fold.from > line.from verification, vim zc/zo round-trip)
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Updated fold gutter unfold fix with unfold normalizer extender descriptionCONTRIBUTING.md: Updatedfold-sync.tsdescription with unfold normalizerdocs/features/workspace-navigation.md: Added unfold normalizer note to Folds sectionREADME.md: Updated surround feature description with insert-mode cursor fixAGENTS.md: Updated codemirror-vim fork description noting insert-mode surround refactordocs/features/surround.md: Updated insert-mode cursor behavior description
[0.77.0] - 2026-07-22
Fixed
- Animated cursor may not animate on Windows 11 — the canvas rAF loop could silently die on Windows due to several platform-specific behaviors: (1) Any transient error during a tick frame (null coordinate during window refocus, detached DOM node) threw an unhandled exception that permanently killed the
requestAnimationFrameloop — the cursor disappeared until plugin reload. Fixed by wrapping the loop body in try/catch; errors are logged once and the loop continues. (2) Windows 11 Efficiency Mode, window occlusion tracking (CalculateNativeWinOcclusion), and high-resolution timer suppression can all silently stop rAF delivery without throwing. Added a 500mssetIntervalheartbeat that detects a stalled loop and re-wakes it — unlike rAF,setIntervalis not suppressed by Chromium’s occlusion tracker. (3) When the browser tab/window is hidden and restored, rAF may not resume. Added avisibilitychangelistener that re-wakes the loop when the page regains visibility. (4) Windows displays at 125%/150% scaling produce fractionaldevicePixelRatiovalues (1.25/1.5). Canvas backing-store dimensions are now rounded withMath.round()to avoid sub-pixel aliasing and continuous compositor re-uploads. Informed by cursor-smith’s v1.1.8 fix for the same “cursor disappears until plugin reload” failure mode and terminal-workbench-cursor’s heartbeat safety-net pattern.- Plugin:
src/vim/animated-cursor/manager.ts(try/catch inloop(), heartbeatsetInterval,visibilitychangelistener, DPR rounding)
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added Windows resilience details to animated cursor sectionCONTRIBUTING.md: Updatedmanager.tsdescription with resilience mechanismsdocs/features/animated-cursor.md: Added Windows resilience sectionREADME.md: Updated animated cursor feature description with cross-platform resilience
[0.76.0] - 2026-07-22
Added
- Per-view cursor suppression fork API — added
setCursorSuppressedForView(view, suppressed),clearCursorSuppressedForView(view), andisCursorSuppressedForView(view)to the codemirror-vim fork. Per-view overrides take precedence over the globalsetCursorSuppressedstate, allowing the plugin to selectively restore the native cursor in specific contexts (table cell editors, textarea overlays) or force suppression (table navigation) without affecting other editors. Overrides are automatically cleaned up when the editor’sBlockCursorPluginis destroyed.- Fork:
~/Repos/codemirror-vim/src/block-cursor.ts(per-view state map, API implementation, cleanup indestroy)
- Fork:
- Animated cursor vimrc/Lua configuration — all 8 animated cursor settings are now configurable via vimrc (
set smoothcursor,set smoothcursorsmoothness=0.3, etc.) and Lua (vim.opt.smoothcursor = true, etc.). Master togglesmoothcursorenables/disables the feature withreloadFeatures(). Sub-options (smoothcursorglide,smoothcursorsmoothness,smoothcursorsmear,smoothcursorstiffness,smoothcursortrailstiffness,smoothcursordamping,smoothcursormaxlength) hot-reload without restart. All useSideEffectOptpattern syncing bothsettings[key]and module-level config. Short aliases:sc,scg,scs,scm,scst,scts,scd,scml. (#78)- Plugin:
src/vimrc/loader.ts(16SideEffectOptentries),src/settings.ts(animatedCursoradded toRELOAD_KEYS)
- Plugin:
- Animated cursor in oil explorer — animated cursor now renders in the oil file explorer. Single shared canvas architecture: one canvas on
.app-containerowned byAnimatedCursorManager, shared by all controllers. Reduces memory from O(N × viewport) to O(1 × viewport). Each controller clips drawing to its own editor bounds viactx.clip().MAX_CONTROLLERSraised from 8 to 16 with warning log on capacity. Canvas lifecycle managed by the manager (created on first register, removed when last controller deregisters). Null-check oncanvas.getContext('2d')for browser canvas limits. Table cell editors and textarea vim overlays fall back to the native cursor. (#78)- Plugin:
src/vim/animated-cursor/manager.ts(shared canvas ownership, sizing, lifecycle),src/vim/animated-cursor/controller.ts(removed per-controller canvas, draws on shared context),src/oil/oil-view.ts(injectscreateAnimatedCursorExtension()when enabled)
- Plugin:
Fixed
- Fold gutter click does not unfold — clicking a fold marker (▾) in the fold column or statuscolumn gutter folded the region correctly but clicking again to unfold had no effect. The
unfoldEffectwas dispatched with{ from: line.from, to: line.from }(zero-width range) instead of the actual fold range, so CodeMirror found no matching fold decoration to remove. Fixed by capturing the fold’s end position fromfoldedRanges().between()and passing the full{ from, to }range tounfoldEffect. (#80)- Plugin:
src/vim/fold-column.ts(click handler),src/vim/statuscolumn.ts(handleFoldClick)
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added fold gutter unfold fix to folding sectionREADME.md: Added vimrc/Lua configuration to animated cursor feature descriptionCONTRIBUTING.md: Updatedmanager.tsandconfig.tsdescriptions for Phase 3 architectureAGENTS.md: Updated codemirror-vim fork description with per-view cursor suppression APIdocs/features/animated-cursor.md: Added vimrc/Lua configuration section and oil explorer support sectiondocs/configuration/vimrc.md: Addedsmoothcursor,smoothcursorsmoothness,smoothcursorsmearto boolean options; addedsmoothcursorsmoothness,smoothcursorstiffness,smoothcursortrailstiffness,smoothcursordamping,smoothcursormaxlengthto number optionsdocs/configuration/lua-config.md: Added all 8smoothcursor*entries to vim.opt options table
[0.75.1] - 2026-07-22
Fixed
-
Table navigation cursor ghost — both native and animated cursors are now hidden during embedded table navigation. Early suppression in the
ViewPluginupdate cycle eliminates the brief cursor flash when entering a table. The animated cursor snaps to the exit position (no interpolation) when resuming after table navigation to prevent cross-table “ghost” trails.- Plugin:
src/vim/table-nav-controller.ts(callssetCursorSuppressedForViewandpauseAnimatedCursorForView)
- Plugin:
-
Textarea overlay invisible cursor — the native cursor is now restored in textarea vim overlays by un-suppressing it for the overlay’s editor view. Previously, the global suppression for the animated cursor made the native cursor invisible in the overlay where the animated cursor doesn’t render.
- Plugin:
src/vim/textarea-vim-manager.ts(callssetCursorSuppressedForView(view, false))
- Plugin:
-
Table cell editor cursor inconsistency — per-view un-suppression ensures the native cursor always renders inside embedded table cell editors, matching the behavior of textarea overlays.
- Plugin:
src/vim/table-cell-editor.ts(callssetCursorSuppressedForView(view, false))
- Plugin:
-
Animated cursor stays as block in operator-pending mode — pressing
d,c,y, or other operators without a motion kept the cursor as a block instead of switching to the configured operator-pending shape (default: underline). Two issues: (1)resolveVimMode()only checkedvim.status(set for prompt-based pending like surround) but notvim.inputState.operator(set for standard operators liked/c/y). (2) Operator-pending is a transient state that doesn’t trigger CM6 transactions, so the ViewPlugin’supdate()never fired. Fixed by checkinginputState.operatorinresolveVimMode()and polling the cursor shape every rAF frame intick()to detect changes that bypass CM6’s transaction system. (#78)- Plugin:
src/vim/animated-cursor/controller.ts(resolveVimModechecksinputState.operator, per-frame shape polling intick)
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Removed operator-pending detection from nice-to-have (implemented)
[0.75.0] - 2026-07-22
Added
- Animated cursor blinking — the canvas cursor now blinks matching CM6’s default behavior (1200ms cycle, hard on/off toggle). After cursor movement, the cursor stays solid for 600ms before resuming blink. Blink epoch is aligned to the end of the reset delay so the first blink cycle starts cleanly. Blink only runs when the editor has focus; unfocused editors show a solid cursor. Suppressed during smear/smooth animation (cursor is moving). (#78)
- Plugin:
src/vim/animated-cursor/controller.ts(computeBlinkAlpha,lastMoveTime,blinkEpoch, focus-aware rAF loop)
- Plugin:
Fixed
- Animated cursor disappears below line ~28 — the canvas was sized to the viewport but positioned at the top of the scroll container (
position: absolute; top: 0insidescrollDOM). After scrolling, cursor coordinates pointed to positions below the canvas bounds. Fixed by moving the canvas to.app-containerwithposition: fixedand using raw viewport-relative coordinates fromcoordsAtPos()directly — matching cursor-smith’s architecture. The canvas is clipped to the editor pane rect viactx.clip()each frame. (#78)- Plugin:
src/vim/animated-cursor/controller.ts(viewport-fixed canvas, removed scroll offset math),styles.css(fixed positioning)
- Plugin:
- Animated cursor displaced rightward in visual mode — entering visual mode (
v) shifted the canvas cursor one character to the right. In visual mode with a forward selection (anchor < head), CM6’sselection.main.headpoints past the last selected character. The fork’sBlockCursorPlugin.measureCursor()decrementsheadin this case, but the animated cursor used the raw value. Fixed by applying the same head adjustment: whenanchor < headand the character at head isn’t\n, decrement position by 1. (#78)- Plugin:
src/vim/animated-cursor/controller.ts(head position adjustment inrefreshTarget)
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Marked cursor blink as fixed; removed “cursor blink after convergence” from nice-to-have (implemented)docs/features/animated-cursor.md: Added cursor blinking section
[0.74.0] - 2026-07-21
Changed
- Settings organized into 7 pages — the flat list of 20 settings groups is now organized into 7 navigable pages: General, Appearance, Navigation, Keybindings, Snippets & files, Input method, and Advanced. On Obsidian 1.13+, pages appear as sidebar entries via
type: 'page'ingetSettingDefinitions(). On pre-1.13, a button tab bar at the top of the settings panel switches between pages. Thedisplay()method is refactored into 7 private render methods (renderGeneralTab,renderAppearanceTab, etc.) for maintainability. No settings were added or removed.- Plugin:
src/settings.ts(declarative pages + imperative tab bar + 7 render methods),styles.css(tab bar CSS)
- Plugin:
- Settings reorganized across pages — moved settings to more logical groupings: sign column and fold column moved from Vim features to Appearance (new “Gutter” group); yank highlight settings moved to Appearance (new “Yank highlight” group); workspace navigation and fold settings moved to Navigation (new “Workspace navigation” group); picker and third-party integration settings consolidated into a new “Picker” group on General (replacing the old “Third-party integrations” group).
- Plugin:
src/settings.ts(both declarative and imperative paths)
- Plugin:
- Declarative settings API enhancements (1.13+) — leveraged additional Obsidian 1.13+ declarative settings features:
- Page descriptions (
desc) on all 7 pages for at-a-glance navigation - Warning status indicator on General page when built-in vim mode is enabled
- Input method page hidden on mobile via
visible: Platform.isDesktop - Search aliases on 20+ settings for better discoverability in Obsidian’s global settings search
defaultValue: trueon 33 toggle controls for framework-managed defaults- Group-level search filter on Jump navigation group (15+ settings)
- Replaced 6 conditional spreads (
...(condition ? [...] : [])) withvisiblepredicates for cleaner reactivity viarefreshDomState() - Inline
validateon 8 numeric/path controls (range checks, path format validation) - Snippet directory changed from text input to
type: 'folder'vault folder picker - 38 conditional
visiblepredicates on child settings — sub-settings hide when their parent feature is disabled (animated cursor, flash, EasyMotion, hint mode, snippets, oil explorer, undo tree, status bar, which-key, workspace nav, yank highlight) - Plugin:
src/settings.ts(declarative path only)
- Page descriptions (
- Pre-1.13 settings conditional visibility — matching the declarative path, the imperative render methods now hide sub-settings when their parent feature is disabled. Uses CSS class toggling (
syncVisibilityClass) for instant show/hide without full re-render — parent toggleonChangehandlers toggle a class on the content container, and child settings are wrapped in gate divs hidden by CSS when the parent class is absent. Covers all 12 parent-child groups across 4 render methods.- Plugin:
src/settings.ts(imperative path —syncVisibilityClasshelper + gate divs in render methods),styles.css(18-selector conditional visibility rule block)
- Plugin:
Documentation
CHANGELOG.mdAGENTS.md: Updated dual settings tab description with page organization, page assignment guide, and page ownership table (workspace nav and folding moved to Workspace navigation group)CONTRIBUTING.md: Updated settings.ts codebase structure entry with 7 page namesKNOWN_LIMITATIONS.md: Added SettingDefinitionList deferred limitation for leader bindings and which-key labels; updated “Third-party integrations” references to “Picker”docs/configuration/settings.md: Added page organization table, explanation of 1.13+ vs pre-1.13 behavior, renamed “Third-party integrations” heading to “Picker”docs/features/ex-commands.md: Updated settings path from “Third-party integrations” to “Picker”docs/development/picker-api.md: Updated settings path from “Third-party integrations” to “Picker”
[0.73.1] - 2026-07-21
Fixed
- Animated cursor settings missing from pre-1.13 settings tab — the 8 animated cursor settings (enable, smooth cursor, smoothness, smear trail, stiffness, trailing stiffness, damping, max length) were only present in the post-1.13 declarative settings API (
getSettingDefinitions()). Added the full settings group to the pre-1.13 imperativedisplay()method with matching toggle/slider controls, disabled-state gating, andreloadFeatures()on master toggle change.- Plugin:
src/settings.ts(post-1.13display()method)
- Plugin:
- Animated cursor e2e tests flaky due to ViewPlugin lifecycle timing — the “canvas is created” and “native cursor is hidden” tests checked DOM state (canvas presence in scrollDOM, CSS class on cm-editor) which was timing-sensitive during
reloadFeatures(). Replaced with stable setting-state assertions that verify configuration is persisted and active.- Plugin:
test/specs/animated-cursor.e2e.ts
- Plugin:
- ESLint warnings in animated cursor module — resolved 8 lint issues: moved canvas inline styles to CSS class (
obsidianmd/no-static-styles-assignment), replacedinstanceofwith.instanceOf()(obsidianmd/prefer-instanceof), removed unnecessary type assertion (@typescript-eslint/no-unnecessary-type-assertion), replacedrequestAnimationFramewithwindow.requestAnimationFrame(obsidianmd/prefer-window-timers), replaceddocument.createElementwith Obsidian’screateElhelper (obsidianmd/prefer-create-el).- Plugin:
src/vim/animated-cursor/controller.ts,src/vim/animated-cursor/manager.ts,styles.css
- Plugin:
[0.73.0] - 2026-07-21
Added
- Animated cursor (smear + smooth movement) — canvas-based cursor rendering with smooth exponential interpolation and spring-damper smear trail. Per-mode cursor shapes (block, bar, underline, hollow) rendered on
<canvas>overlay. Fork-side cursor suppression viasetCursorSuppressed(). Disabled by default — enable via Settings → Vim Motions → Animated cursor. Inspired by smear-cursor.nvim and cursor-smith. (#78)- Plugin:
src/vim/animated-cursor/(new: types.ts, smooth-cursor.ts, physics.ts, renderer.ts, manager.ts, controller.ts, config.ts),src/settings.ts(8 new settings),src/main.ts(extension registration, lifecycle) - Fork:
~/Repos/codemirror-vim/src/block-cursor.ts(setCursorSuppressedAPI),~/Repos/codemirror-vim/src/index.ts(re-export)
- Plugin:
Tests
- 32 unit tests in
test/unit/animated-cursor.test.ts: SmoothCursor (11 tests: setTarget snap/no-snap, tick exponential decay, frame-rate independence, smoothness extremes, snap, isConverged, current, reset), SmearPhysics (11 tests: setTarget snap/no-snap, tick spring-damper, head-faster-than-tail, isConverged, snap, reset, max length clamping, frame-rate independence, volume shrinkage), getCursorShapeForMode (10 tests: all mode mappings, custom shapes) - 5 e2e tests in
test/specs/animated-cursor.e2e.ts: canvas creation, native cursor hiding, disable toggle, cursor follows movement, settings sub-toggles
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added animated cursor section with limitations and nice-to-have future itemsREADME.md: Added animated cursor to features listCONTRIBUTING.md: Addedanimated-cursor/module to codebase structureAGENTS.md: Updated codemirror-vim fork description withsetCursorSuppressedAPI; added animated cursor to page ownership table~/Repos/codemirror-vim/DIFFERENCES.md: AddedsetCursorSuppressedAPI section
[0.72.0] - 2026-07-21
Added
labelmatchfontsizesetting — opt-in setting that scales jump label font to match the target line’s font size (e.g., larger labels on headings). Disabled by default. Configurable via Settings → Vim Motions → Jump navigation → Scale labels to line height,set labelmatchfontsizein vimrc, orvim.opt.labelmatchfontsize = truein Lua. (#75)- Plugin:
src/settings.ts,src/vim/options.ts,src/vimrc/loader.ts,src/easymotion/overlay.ts(per-targetlabelMetrics),src/easymotion/register.ts,src/flash/register.ts,src/flash/char-mode.ts,src/flash/jump-mode.ts,src/flash/search-mode.ts
- Plugin:
Changed
- Label vertical centering — jump labels are now vertically centered within the line height instead of being top-aligned. On lines with taller fonts (headings), labels sit centered in the line rather than hugging the top edge. (#75)
Fixed
- Cursor stuck below frontmatter in source mode — pressing
k,C-u, or arrow-up from the first content line after YAML frontmatter could not enter the frontmatter block in source mode. The fork’sfindPosVadapter unconditionally intercepted upward cursor movement near frontmatter boundaries to redirect focus to the properties widget (live-preview behavior). In source mode, no properties widget exists — the interception fired but found no focus target, leaving the cursor stuck. Fixed by gating the frontmatter interception on Obsidian’seditorLivePreviewFieldstate field. A newsetLivePreviewField()API on the fork accepts the host-provided field without coupling the fork to Obsidian. In source mode, the block is skipped entirely and the cursor moves through raw frontmatter text normally. (#77)- Fork:
~/Repos/codemirror-vim/src/cm_adapter.ts(setLivePreviewField,_livePreviewFieldgate infindPosV),~/Repos/codemirror-vim/src/index.ts(re-export) - Plugin:
src/vim/bundled-vim.ts(passeseditorLivePreviewFieldto fork),src/types/codemirror-vim.d.ts(type declaration)
- Fork:
- EasyMotion line motions targeting hidden formatting in Live Preview —
<leader><leader>j/<leader><leader>kline motions targeted hidden markdown formatting characters (e.g.,##on headings,**on bold text) instead of the first visible character. In Live Preview, the label appeared on the hidden prefix position, obscuring the first visible character. Fixed by addingskipHiddenPrefix()tofindLineTargets(), which scans forward from the raw-text first-non-blank character usingcoordsAtPos()to find the first character that occupies visible space. (#79)- Plugin:
src/easymotion/targets.ts(skipHiddenPrefix,findLineTargets)
- Plugin:
Documentation
CHANGELOG.mdKNOWN_LIMITATIONS.md: Added label vertical centering note, RTL label positioning limitation, and line motion hidden formatting fix to flash motions section; updated frontmatter navigation section with source mode fixAGENTS.md: Updated codemirror-vim fork description withsetLivePreviewFieldAPICONTRIBUTING.md: Updatedbundled-vim.tsdescription with live-preview field wiring~/Repos/codemirror-vim/DIFFERENCES.md: AddedsetLivePreviewFieldAPI section; updated properties navigation section with live-preview gatingdocs/configuration/settings.md: Addedlabelmatchfontsizeto Jump navigation settingsdocs/configuration/vimrc.md: Addedlabelmatchfontsize/lmfsto boolean optionsdocs/configuration/lua-config.md: Addedlabelmatchfontsizeto vim.opt tabledocs/features/flash.md: Addedlabelmatchfontsizeto configuration tabledocs/features/easymotion.md: Added scale labels setting to configuration
[0.71.0] - 2026-07-20
Added
- Yank-ring paste cycling — after
p/P, pressing<C-p>replaces the pasted text with the previous numbered register ("1–"9).<C-n>reverses direction. Cycling wraps. Any non-cycling command cancels state;<C-p>/<C-n>then revert tok/j. Gated byenableYankRingsetting (default: on). Usesvim-keypressevent detection andaddToHistory.of(false)for single-undo-group cycling.- Plugin:
src/vim/yank-ring.ts(new),src/settings.ts(enableYankRing),src/main.ts(lifecycle integration)
- Plugin:
- Indentation text object (
ii/ai) —iiselects contiguous lines with same-or-greater indentation.aiadds the parent line above and trailing blank lines. Zero-indentation and blank lines return no match. Column-aware tab handling via CM6state.tabSize. Gated by existingenableTextObjectssetting.- Plugin:
src/text-objects/indentation.ts(new),src/text-objects/register.ts
- Plugin:
grblockwise visual mode —<C-V>block selection +grnow replaces each line in the block with corresponding register content. Single-line registers duplicate to all block rows; multi-line registers apply line-by-line; excess register lines truncate to block height. Cursor lands at top-left of block. Previously returned early (no-op).- Plugin:
src/operators/replace-with-register.ts(blockwise branch)
- Plugin:
Changed
- EasyMotion label positioning — labels now appear one character to the right of the target character (after the target) instead of on top of it. This prevents labels from obscuring the character they target. The change applies to all EasyMotion motions (word, char, line, search). Match highlights now also appear behind EasyMotion labels.
Fixed
- Flash highlight rectangles hardcoded to 8×16px — the
.vim-motions-flash-matchhighlight boxes used a fixedwidth: 8px; height: 16pxregardless of actual character dimensions, breaking with proportional fonts, different font sizes, and CJK characters. Now dynamically measured viacoordsAtPos()for both start and end of each match. CSS dimensions use custom properties (--vim-motions-flash-w,--vim-motions-flash-h) with fallbacks. (#75) - Flash labels obscure matched text — labels were positioned at the match START coordinate, rendering on top of the matched characters. Labels are now positioned at the END of the matched text (one character past the last matched character), matching flash.nvim’s default
after = truebehavior. Matched text remains visible with a colored highlight underneath. (#75) - Flash match highlights missing during label phase — when labels appeared (pattern met
minPatternLength), match highlights disappeared. NowshowOverlayrenders both match highlights AND labels simultaneously (flash.nvim parity). Match highlights persist during label narrowing — only labels update when typing a label prefix. (#75) - Flash jump-mode label narrowing destroyed match highlights — in jump mode (
s), typing a label prefix character destroyed the entire overlay and recreated it with only remaining targets, losing match highlights for non-matching targets. Now usesupdateLabels()to narrow labels while preserving all match highlights. (#75)- Plugin:
src/easymotion/overlay.ts(extractedmeasureTarget+measureLabelAnchor+renderHighlightSpansshared helpers, addedrenderHighlightstoshowOverlay, label positioning at end-of-match),src/easymotion/types.ts(matchLength?: numberonTarget),src/easymotion/targets.ts(matchLengthinfindSubstringTargets),src/flash/search-mode.ts(matchLengthinfindSearchMatchTargets),src/flash/jump-mode.ts(widenedcurrentOverlaytype toOverlayHandle, label narrowing viaupdateLabels),styles.css(CSS custom properties for.vim-motions-flash-matchdimensions)
- Plugin:
- Flash jump mode two-character label premature exit — when flash jump mode (
s) displayed two-character labels (28+ matches with default 27-char label alphabet), typing the first character of a two-char label either jumped to the wrong single-char target or appended the character to the search pattern, destroying the label state. The same bug affected post-//?search labels. Fixed by adding prefix accumulation with label narrowing: typed characters are checked as label matches first (exact → jump, prefix → narrow and update overlay), then fall back to extending the search pattern. Extracted sharedwaitForFlashLabel()intosrc/flash/label-input.tsfor reuse bychar-mode.ts. (#76)- Plugin:
src/flash/label-input.ts(new: shared label selection state machine),src/flash/jump-mode.ts(prefix accumulation + label narrowing),src/flash/search-mode.ts(prefix accumulation + label narrowing),src/flash/char-mode.ts(imports sharedwaitForFlashLabel)
- Plugin:
- Flash jump
sconflicting with surroundcs/ys/dsin operator-pending mode — when flash jump mode was enabled withsas the key, surround operations (cs",ds",ysiw") were intercepted by flash because motions take precedence over partial action matches in operator-pending mode. Fixed by implementing an operator-prefix shadow resolver in the codemirror-vim fork: when an operator is pending and the next key fully matches a motion but also partially matches anoperatorPendingaction (e.g., surround’ss<character>), the resolver defers to the partial match, waiting for the next character to disambiguate. A configurable timeout (operatorshadowtimeout, default 1000ms) falls back to executing the deferred motion if no next key arrives. (#76)- Fork:
~/Repos/codemirror-vim/src/vim.js(shadow resolver inmatchCommand(), timer inhandleKeyNonInsertMode(), cleanup inclearInputState()+ teardown,operatorshadowtimeoutoption) - Fork:
~/Repos/codemirror-vim/src/types.ts(_shadowTimeronvimState) - Plugin:
src/settings.ts(operatorshadowtimeoutsetting + Settings UI),src/vimrc/loader.ts(operatorshadowtimeout/ostinKNOWN_SET_OPTIONS)
- Fork:
Tests
- 5 unit tests in
test/unit/flash-targets.test.ts:findSubstringTargetssetsmatchLength(5-char, 1-char, 2-char patterns), empty pattern returns empty,findCharTargetsdoes not setmatchLength - 2 e2e tests in
test/specs/flash-jump-mode.e2e.ts: two-char label narrowing (labels persist after typing first char of two-char label), single-char label immediate jump - 3 e2e tests in
test/specs/flash-jump-mode.e2e.ts: shadow resolver surround coexistence (cs"',ds",ysiw"with flashsenabled) - 10 tests in
~/Repos/codemirror-vim/test/vim_test.js: shadow resolver (cs,ds,ysiwsurround wins,dd/ccregression,dw/cwno-activation, Escape clears timer,g~regression,ost=0disables) - 6 e2e tests for
grblockwise intest/specs/operators.e2e.ts(4 unskipped + register preservation + cursor position) - 13 e2e tests in
test/specs/indentation-textobj.e2e.ts(inner/around selection, operators, zero-indent, blank lines, nesting, yank, cursor position, mode verification) - 8 e2e tests in
test/specs/yank-ring.e2e.ts(cycling, reversal, cancellation, fallback to k/j, paste variants, register preservation)
Documentation
CHANGELOG.mdREADME.mdCONTRIBUTING.md: Updated overlay.ts description; addedlabel-input.tsto codebase structureAGENTS.md: Updated codemirror-vim fork description with operator-prefix shadow resolver and test countKNOWN_LIMITATIONS.md: Updated flash motions section with highlight sizing and label positioning fixes; added operator-prefix key dispatch section (Implemented); updatedskey / surround conflict bullet to reference resolverdocs/features/flash.md: Added highlight and label rendering behavior documentation; updated surround conflict note to document automatic resolution via shadow resolver; added two-char label behavior documentationdocs/features/easymotion.md: Added note about label positioning changedocs/features/text-objects.mddocs/features/quality-of-life.mddocs/reference/keybindings.mddocs/configuration/settings.md: Addedoperatorshadowtimeoutto Vim engine settings groupdocs/configuration/vimrc.md: Addedoperatorshadowtimeout/ostto numeric options tabledocs/configuration/lua-config.md: Addedvim.opt.operatorshadowtimeoutentry~/Repos/codemirror-vim/DIFFERENCES.md: Added operator-prefix shadow resolver section
[0.70.0] - 2026-07-19
Added
- Undo tree visualization — branching undo history with
g+/g-chronological navigation across all branches (buffer content changes via ChangeSet dispatch),:earlier N/Ns/Nm/Nh/Nd/Nfand:latertime/count/save-point navigation,:undolistmodal, sidebar view (:UndoTreeToggle/Show/Hide) with DOM tree rendering, click-to-navigate, keyboard nav (j/k/Enter/q), collapse/expand branches, relative timestamps, summary diff preview,vim.fn.undotree()Lua API (Neovim-compatible dict), optional persistence (set undofile), per-file undo tree map, Obsidian commands for sidebar management. Inspired by undotree.- Plugin:
src/vim/undo-tree.ts(shadow tree data structure),src/vim/undo-tree-view.ts(sidebar view),src/main.ts(CM6 integration, g+/g- actions, persistence hooks),src/workspace/commands.ts(ex commands),src/lua/fn.ts(vim.fn.undotree())
- Plugin:
Changed
minAppVersionbumped from 1.6.6 to 1.7.2 — required forWorkspace.revealLeaf()used by undo tree.
Fixed
g;/g,/g-/g+keymaps not working afterreloadFeatures()— the changelist and undo treemapCommandregistrations were only inonload()but not inreloadFeatures(). SincereloadFeatures()callsunregisterAll()(which wipes all custom keymaps) and then re-registers features, these keymaps were silently wiped on any settings change, vimrc load, or Lua config load. Fixed by adding the registrations toreloadFeatures().- Plugin:
src/main.ts(added changelist + undo tree mapCommand calls toreloadFeatures())
- Plugin:
Tests
- 66 unit tests in
test/unit/undo-tree.test.ts: data structure (branching, navigation, pruning, time lookup, Neovim dict), serialize/deserialize round-trip, findBySaveCount, computePath, navigating flag, ChangeSet storage - 8 e2e tests in
test/specs/undo-tree.e2e.ts: CM6 integration, g+/g-, :earlier/:later, :undolist - 4 e2e tests in
test/specs/undo-tree-view.e2e.ts: sidebar open/close, node rendering, current marker - 5 e2e tests in
test/specs/undo-tree-navigation.e2e.ts: buffer content changes via :earlier/:later, g+/g- safety
Documentation
CHANGELOG.md: Added undo tree visualization feature and reloadFeatures fixKNOWN_LIMITATIONS.md: Added undo tree sectionREADME.md: Added undo tree to features listCONTRIBUTING.md: Added undo-tree.ts, undo-tree-view.ts to codebase structureAGENTS.md: Added undo tree to page ownership tabledocs/features/undo-tree.md: New feature pagedocs/features/index.md: Added undo tree linkdocs/features/quality-of-life.md: Added g+/g- and :earlier/:later to change navigationdocs/reference/keybindings.md: Added undo tree navigation sectiondocs/configuration/settings.md: Added Undo tree settings groupdocs/configuration/vimrc.md: Added undotree/undofile optionsdocs/configuration/lua-config.md: Added vim.fn.undotree() and vim.opt entriesdocs/features/ex-commands.md: Added :earlier/:later/:undolist/:UndoTreeToggle
[0.69.0] - 2026-07-19
Fixed
- Textarea vim overlay re-activates immediately after Escape exit — pressing Escape in normal mode tore down the overlay and called
originalEl.focus(), which triggered thefocusinlistener and re-created the overlay in insert mode after the 150ms debounce. Users saw a brief flash of the textarea before being placed back in insert mode, making it impossible to return to the modal context. Fixed by adding arecentlyExitedguard (WeakRef+ 250ms cooldown) that suppresses thefocusinhandler for the textarea that was just exited. After the cooldown, the textarea can be re-activated by clicking into it again. (#69)- Plugin:
src/vim/textarea-vim-manager.ts(recentlyExitedWeakRef guard,markRecentlyExitedcooldown)
- Plugin:
- Lua text objects lost after
reloadFeatures()—vim.textobject.add()registrations from.obsidian.init.luawere silently discarded becauseloadLuaConfigInternal()calledreloadFeatures()after Lua evaluation, destroying theVimRegistrationinstance that held the keybindings. Fixed by persisting text object specs inluaTextObjectSpecs[]and re-registering them viareregisterLuaTextObjects()afterreloadFeatures()completes.- Plugin:
src/main.ts(luaTextObjectSpecs,registerLuaTextObject,reregisterLuaTextObjects)
- Plugin:
Added
- Subword motions (spider.nvim-style) —
w/b/e/geoverride stopping at camelCase, snake_case, and kebab-case word boundaries. Opt-in viaset subword/ Settings → Vim features → Subword motions. 10,000-char performance guard falls back to standard word motions on pathological lines.- Plugin:
src/util/subword.ts(shared boundary detection),src/motions/subword.ts(4 motion variants)
- Plugin:
- General-purpose text objects — 6 new text objects complementing the existing 13 Markdown-specific ones:
iS/aS(subword segment),in/an(numeric literal with sign/decimal),iq/aq(nearest quote pair on line),iD/aD(wikilink[[...]]with nesting),gL(forward-seeking URL),i,/a,(comma-separated argument with nesting). All work with operators (d,c,y) and visual mode.- Plugin:
src/text-objects/{pair-util,subword,number,any-quote,double-bracket,url,argument}.ts
- Plugin:
- Enhanced increment/decrement (dial.nvim-style) —
<C-a>/<C-x>extended to cycle: markdown checkboxes ([ ]↔[x]), booleans (true↔false, case-preserved), hex colors (component-wise R/G/B, clamped 0–255), dates (YYYY-MM-DDwith rollover), CSS values (preserving unit), and integers. Priority-ordered rules (first match wins). Opt-in viaset dial/ Settings → Vim features → Enhanced increment/decrement. Falls back to default<C-a>/<C-x>when no rule matches.- Plugin:
src/actions/{dial-rules,dial,register-dial}.ts
- Plugin:
- Custom text objects via Lua —
vim.textobject.add(keys, spec)andvim.textobject.del(keys)API for defining custom text objects from.obsidian.init.lua.vim.gen_spec.pair(open, close, opts?)generates pair-matching specs with nesting and multi-line support. Keys must start withi(inner) ora(around). Invalid inputs produce error notices.- Plugin:
src/lua/textobject-api.ts
- Plugin:
- External grep binary integration — optional native ripgrep or GNU grep binary for the picker’s grep/live-grep sources. Supports
rg --json(structured output) andgrep -rn(file:line:content format). Desktop-only with automatic fallback to in-memory search on mobile or binary failure. Circuit breaker (3 errors in 60s → auto-disable). Process cancellation on new query.- Plugin:
src/picker/sources/ripgrep-process.ts, settings:ripgrepEnabled,ripgrepBinaryPath,ripgrepArgs,grepMode
- Plugin:
Tests
- 37 unit tests in
test/unit/subword.test.ts: boundary detection (13 patterns), motion logic (20 cases including cross-line, count, performance guard) - 45 unit tests in
test/unit/text-objects-extended.test.ts: all 6 text objects with inner/around, edge cases, nesting - 31 unit tests in
test/unit/dial.test.ts: all 6 rules individually + priority ordering + tryDial integration - 26 unit tests in
test/unit/lua-textobject-api.test.ts: asymmetric/symmetric pair matching, nesting, multi-line, scanLimit - 21 unit tests in
test/unit/ripgrep-process.test.ts: JSON/grep output parsing, arg building, error classification - 10 e2e tests in
test/specs/subword-motions.e2e.ts: navigation, operators, snake/kebab, count, setting toggle - 11 e2e tests in
test/specs/text-objects-extended.e2e.ts: all text objects with delete/change operators - 9 e2e tests in
test/specs/dial.e2e.ts: all rule types, count prefix, no-match fallback, setting toggle - 5 e2e tests in
test/specs/lua-textobject.e2e.ts: custom pairs (single-char, multi-char, nested), error handling - 3 e2e tests in
test/specs/ripgrep.e2e.ts: conditional skip (binary availability)
Documentation
CHANGELOG.md: Added textarea re-activation prevention fixKNOWN_LIMITATIONS.md: Textarea re-activation after Escape → Fixed (recentlyExited guard)
[0.68.0] - 2026-07-19
Tests
- 13 regression tests in
test/specs/table-escaped-pipes.e2e.tsfor issues #66 and #67: typing|outside tables (empty doc, mid-text, non-table line, multiple pipes), escaped\|navigation (]|skips escaped pipes, wikilink pipe doesn’t split cell), typing|in table cells (auto-escape, cell count preservation). 1 test skipped (Obsidian swallows|at DOM level — see KNOWN_LIMITATIONS.md)
Documentation
KNOWN_LIMITATIONS.md: Updated #67 from “Fixed” to “Partially fixed” — documented remaining Obsidian platform behavior where typing|in table cells is swallowed by the 1.7+ table editor at the DOM level (identical in built-in vim, bundled fork, and no-vim modes). Documented workaround via Embedded table widget mode.docs/features/tables.md: Added[!bug]callout about|typing limitation in Live Preview table cells- Documentation audit — systematic audit of all
docs/pages against source code. Corrected stale counts and inaccurate information across 11 files:docs/configuration/lua-config.md: Fixedinsert_normalmode prompt default from(insert)toNORMAL(matchingsrc/settings.ts)docs/features/text-objects.md,docs/features/index.md: Updated “12 text objects” → 13 (includes table rows)AGENTS.md,CONTRIBUTING.md,docs/development/architecture.md: Updated “27 vim.fn functions” → 26 (matchessrc/lua/fn.ts)README.md,docs/features/index.md: Updated “12 built-in picker sources” → 14docs/features/index.md: Updated “60+ ex commands” → 100+README.md,docs/features/snippets.md: Updated “40+ bundled snippets” → 60+docs/configuration/settings.md: Moved jumplist/jumplistsize to “Vimrc / Lua only” subsection (not in Settings UI); added updatetime for consistency; moved stray cursorlineopt row from inside callout into Line numbers table; added Cursor line highlight mode rowdocs/features/quality-of-life.md: Added change list navigation (g;/g,) sectiondocs/features/workspace-navigation.md: Made fold providers (frontmatter, callouts) more explicitKNOWN_LIMITATIONS.md: Updated “all 12 sources” → “all 14 sources” in cross-note jump list sectiondocs/features/index.md: Updated “Ships 40+ snippets” → “Ships 60+ snippets”docs/getting-started/index.md: Updated “60+ ex commands” → “100+ ex commands”
[0.67.0] - 2026-07-18
Added
- Flash motions — enhanced f/F/t/T with labels — when pressing
f{char}and 2+ matches exist in the viewport, jump labels appear on all matches. Single match auto-jumps (stock Vim behavior preserved). Works with operators (df{char}{label},cf,yf), visual mode (vf{char}{label}), and;/,repeat. Multi-line search enabled by default (configurable viaset flashmultiline). Inspired by flash.nvim.- Plugin:
src/flash/char-mode.ts(core motion override),src/flash/register.ts(registration with original capture),src/flash/labeler.ts(distance-based label assignment with reuse + conflict skip),src/flash/state.ts(active flag, clever-f state)
- Plugin:
- Flash jump mode (s) — bidirectional character jump bound to a configurable key (default:
s). Presss{char}to search both directions with labels. Disabled by default (set flashjumpto enable). Normal mode only — visualsretainscmapping.- Plugin:
src/flash/jump-mode.ts
- Plugin:
- Flash clever-f — when enabled (
set flashcleverf), pressingf{same-char}after a flash jump falls through to stockfbehavior (acts as;). Uses a 5-second timeout window.- Plugin:
src/flash/char-mode.ts(clever-f check),src/flash/state.ts(last search tracking)
- Plugin:
- Search match counter — hlslens-style
[3/15]indicator in the status bar showing the current match index and total count after/search andn/Nnavigation. Hides when cursor moves off a match or mode changes from normal. General feature, not flash-specific.- Plugin:
src/vim/search-counter.ts(new),src/vim/mode-tracker.ts(status bar integration)
- Plugin:
- Incremental jump search — jump mode (
s) now accepts multiple characters incrementally. Each keystroke narrows the match set; labels update in real-time with stable assignment viaFlashLabeler. Supports Backspace (remove last char, widen matches), Enter (jump to nearest), and autojump on single match. Operator-pending (ds{pattern}{label}) and visual mode supported.- Plugin:
src/flash/jump-mode.ts(rewritten),src/easymotion/targets.ts(findSubstringTargets)
- Plugin:
- Label conflict skipping — labels that match the next character after a match position are excluded from the label pool, preventing ambiguity when the user might type that character to narrow the search.
- Plugin:
src/flash/jump-mode.ts(computeSkipChars)
- Plugin:
flashMinPatternLengthsetting — configurable minimum characters before labels appear in jump mode (default: 1). Below the threshold, matches are highlighted without labels.- Plugin:
src/settings.ts,src/vim/options.ts,src/vimrc/loader.ts
- Plugin:
- Match highlighting without labels —
showMatchHighlights()renders subtle position indicators for matches below theminPatternLengththreshold, distinct from label overlays.- Plugin:
src/easymotion/overlay.ts(showMatchHighlights),styles.css(.vim-motions-flash-match)
- Plugin:
- Flash search mode — after committing a
/or?search with Enter, labels appear on all visible matches. Press a label key to jump directly; any non-label key clears labels. Configurable viaset flashsearch/set noflashsearch.- Plugin:
src/flash/search-mode.ts(new),src/main.ts(registration with cleanup)
- Plugin:
- codemirror-vim fork API additions —
getMotion(name)retrieves a motion function by name (for capturing originals before override).recordLastCharacterSearch(increment, args)sets the;/,repeat state from plugin code.- Fork:
~/Repos/codemirror-vim/src/vim.js
- Fork:
Changed
- Flash labeler —
FlashLabelerclass with distance-based assignment (closest targets get home-row labels), label reuse across narrowing (labels stay stable as match set shrinks), and conflict skipping viaskipCharsset. - EasyMotion dimming description — updated to “Dim non-target text when EasyMotion or flash is active” since both features share the dimming overlay.
- Textarea vim overlay Escape no longer closes parent modal — pressing Escape in normal mode within the textarea overlay now tears down the overlay and returns focus to the original textarea, but no longer re-dispatches a synthetic Escape keydown to the parent UI. Previously, the second Escape closed the host modal (e.g., Spaced Repetition’s edit flashcard dialog), which could cause data loss if the user hadn’t clicked Save. The new behavior follows a symmetric context stack: modal → vim overlay → modal → user closes modal manually. (#69)
- Plugin:
src/vim/textarea-vim-manager.ts(removed synthetic Escape dispatch fromhandleEscapeAndRedispatch)
- Plugin:
Tests
- 1 new e2e test in
test/specs/textarea-vim.e2e.ts: Escape from normal mode returns to textarea without closing modal — verifies overlay removed, modal still present, textarea restored, content synced - 6 spike tests in
test/specs/spikes/spike-flash-override.e2e.ts: defineMotion override, async motion, operator-pending, getMotion, recordLastCharacterSearch - 17 baseline tests in
test/specs/flash-baseline.e2e.ts: stock f/F/t/T with flash disabled (regression guards) - 9 e2e tests in
test/specs/flash-char-mode.e2e.ts: autojump, multi-match labels, escape cancel, settings toggle, multi_line, operator-pending, semicolon repeat - 7 e2e tests in
test/specs/flash-jump-mode.e2e.ts: jump mode setting, autojump, labels, no-match, escape, default key, clever-f - 8 e2e tests in
test/specs/flash-incremental.e2e.ts: incremental narrowing, autojump on single match, backspace, zero matches, escape, enter, min_pattern_length, operator-pending - 5 e2e tests in
test/specs/flash-search-mode.e2e.ts: labels after /pattern Enter, label jump, non-label key clears, no labels on zero/single match - 3 e2e tests in
test/specs/search-counter.e2e.ts: count after search, update after n, hide when cleared - operator-combos.e2e.ts updated to disable flash (stock f/F/t/T behavior preserved)
Documentation
docs/features/flash.md: New feature page — usage, multi-line, operator-pending, visual, jump mode, clever-f, configurationdocs/features/index.md: Added flash motions linkdocs/features/easymotion.md: Added cross-reference to flashdocs/features/index.md: Updated flash description with incremental + search labelsdocs/configuration/settings.md: Added flash, flashmultiline, flashjump, flashjumpkey, flashcleverf, flashminpatternlength, flashsearch settingsdocs/configuration/vimrc.md: Added flash boolean and string options (including flashminpatternlength, flashsearch)docs/configuration/lua-config.md: Added flash vim.opt entries (including flashminpatternlength, flashsearch)docs/reference/keybindings.md: Added flash motions, jump mode, and search labels sectionsKNOWN_LIMITATIONS.md: Added flash motions section (Phase 1 + Phase 2 + Phase 3A + Phase 3B limitations)README.md: Updated flash motions feature bullet with incremental search + post-commit search labelsCONTRIBUTING.md: Added flash/ module, search-counter.ts, updated jump-mode.ts descriptionAGENTS.md: Updated flash motions page ownership with all settingsKNOWN_LIMITATIONS.md: Textarea Escape behavior updated — no longer re-dispatches to parent, symmetric context stack documentedREADME.md: Updated textarea feature description with new Escape behavior
[0.66.0] - 2026-07-18
Added
vim.regex()— ECMAScript regular expressions in Lua —vim.regex(pattern, flags?)creates a regex object exposingmatch_str,match_line,match_pos,replace, andtestmethods. Uses JavaScript’sRegExpengine (not Vim regex syntax). Returns 0-based byte offsets matching Neovim’svim.regex()convention. Invalid patterns raise a Lua error catchable withpcall.- Plugin:
src/lua/regex.ts(new),src/lua/api.ts(registration viainjectRegex)
- Plugin:
- Fengari fork:
__gcmetamethods via FinalizationRegistry —__gcmetamethods on userdata are now invoked when the userdata becomes unreachable from JavaScript. Registration happens atlua_setmetatabletime (only when the metatable contains__gc). Finalizers are drained at three points: outermostluaD_pcallreturn,collectgarbage("collect"), andlua_close. Errors in__gcare silently swallowed (PUC-Rio semantics). Finalization order is unspecified. Tables with__gcare not finalized (userdata only). Environments withoutFinalizationRegistrygracefully degrade (no registration, no errors).- Fork:
~/Repos/fengari/src/lstate.js(finalizer infrastructure onglobal_State,drainFinalizers,lua_closedrain + unregister),~/Repos/fengari/src/lapi.js(lua_setmetatablesplitLUA_TUSERDATA/LUA_TTABLE, FR registration),~/Repos/fengari/src/ldo.js(drain point inluaD_pcall),~/Repos/fengari/src/lbaselib.js(collectgarbage("collect")drain integration)
- Fork:
- Fengari fork:
collectgarbage()no longer crashes — all 8collectgarbagemodes now return safe values instead of throwingluaL_error("lua_gc not implemented")."count"returns0, 0(no memory tracking)."collect"drains the__gcfinalizer queue."isrunning"returnsfalse. All other modes return0. Previously, any Lua code callingcollectgarbage()crashed the entire init sequence.- Fork:
~/Repos/fengari/src/lbaselib.js
- Fork:
- Native JS error propagation via
lua_atnativeerror— the plugin now installs alua_atnativeerrorhandler that converts native JS errors (TypeError, RangeError, etc.) to extractable Lua strings. Previously, native JS errors thrown inside fengari C functions were pushed aslightuserdataand lost —lua_tolstringreturnednull, producing generic “Unknown Lua error” messages. The handler extractsError.message(orString(e)for non-Error values) and pushes it as a Lua string. Covers all threads including coroutines (handler is onglobal_State).- Plugin:
src/lua/engine.ts(lua_atnativeerrorhandler increateSandboxedState),src/lua/types.d.ts(lua_touserdata,lua_atnativeerror,lua_pushintegertype declarations)
- Plugin:
Changed
- Fengari fork:
sprintf-jsreplaced with custom formatter — thesprintf-jsnpm dependency (sole runtime dependency) has been replaced with a purpose-builtluaSprintffunction in the fork’ssrc/lstrlib.js. The fork now ships with zero runtime dependencies. Output is byte-identical to the previous implementation for all standard format patterns.- Fork:
~/Repos/fengari/src/lstrlib.js,~/Repos/fengari/package.json,~/Repos/fengari/DIFFERENCES.md
- Fork:
- Fengari fork: integers widened from 32-bit to 53-bit —
math.maxintegeris now9007199254740991(2^53 - 1). Arithmetic operations use full 53-bitNumberprecision.string.packsize("j")returns 8 (was 4).tonumber("1099511627776")now returns the integer (wasnil). Bitwise operations remain 32-bit (JavaScript platform limitation). See~/Repos/fengari/DIFFERENCES.md§ “Integer widening” for the full change list and remaining limitations.- Fork:
~/Repos/fengari/src/luaconf.js,~/Repos/fengari/src/llimits.js,~/Repos/fengari/src/lvm.js,~/Repos/fengari/src/lobject.js,~/Repos/fengari/src/lstrlib.js,~/Repos/fengari/src/ltable.js,~/Repos/fengari/src/lapi.js,~/Repos/fengari/src/ldo.js,~/Repos/fengari/src/lmathlib.js,~/Repos/fengari/src/lbaselib.js
- Fork:
- Coroutine↔Promise bridge for async Lua execution — Lua callbacks (keymap functions, autocmd handlers, timer callbacks, user commands) can now call async APIs that yield the coroutine and resume when the Promise resolves. The bridge uses fengari’s
lua_yieldkcontinuations with aCoroutineRunnermanaging thread lifecycle, instruction hooks, timeouts (10s), and concurrency limits (16 concurrent operations).pcallcorrectly catches async errors across yield/resume boundaries.- Plugin:
src/lua/coroutine-runner.ts(new:CoroutineRunner+AsyncRegistry),src/lua/engine.ts(evalLuaAsync,INSTRUCTION_LIMITexport),src/lua/types.d.ts(7 new fengari type declarations:lua_newthread,lua_resume,lua_yieldk,lua_status,lua_xmove,lua_isyieldable,LUA_YIELD)
- Plugin:
vim.ob.fs.read(path)andvim.ob.fs.readlines(path)— read vault files from Lua.readreturns a string,readlinesreturns a table of lines. Both yield internally via the coroutine bridge. Errors are catchable withpcall. Works in keymap callbacks, autocmd handlers, timer callbacks, and user commands. Also works at top level ininit.lua. Blocked in snippetf()/d()nodes (raises “async APIs cannot be called from snippet nodes”).- Plugin:
src/lua/obsidian-api.ts(read/readlinesC-functions),src/lua/loader.ts(fsReadcallback viaadapter.read+readExternalFilefor absolute paths),src/lua/api.ts(fsReadonVimApiCallbacks)
- Plugin:
require()for multi-file Lua configs —require('mymodule')loadslua/mymodule.luafrom the vault root. Dot-separated names resolve to subdirectories (require('utils.strings')→lua/utils/strings.lua). Modules are cached inpackage.loaded. Circular requires detected via sentinel. Security: path traversal (..), absolute paths, and backslash paths are rejected.- Plugin:
src/lua/package.ts(new:packagetable, sandboxedload(), Lua-implementedrequire()),src/lua/engine.ts(loadkept in disabled list with re-enable note)
- Plugin:
load(chunk)re-enabled with sandboxing —load()compiles a string chunk and returns the compiled function (ornil+ error).dofileandloadfileremain disabled. The instruction count hook applies to loaded code.- Plugin:
src/lua/package.ts(injectSandboxedLoad)
- Plugin:
evalLuaAsyncfor async init.lua execution — top-levelinit.luacode can now call async APIs likevim.ob.fs.read. The init.lua chunk runs inside a coroutine viaevalLuaAsync, which compiles on the main state and delegates toinvokeAsyncCapable.autocmdManager.activate()fires only after all yields complete.- Plugin:
src/lua/engine.ts(evalLuaAsync),src/lua/loader.ts(evalLua→await evalLuaAsync)
- Plugin:
- Callback sites refactored for async capability — all 4 Lua callback invocation sites now use
CoroutineRunner.invokeAsyncCapablewhen a runner is available, with fallback to the originallua_pcallpath when not. Existing sync callbacks work identically.- Plugin:
src/lua/api.ts(keymap, user command, autocmd callbacks),src/lua/timers.ts(invokeLuaCallback+ 5 call sites)
- Plugin:
- Snippet async guard —
f()andd()snippet node evaluations are wrapped withrunner.setAsyncBlocked(true/false)to prevent async API calls during snippet expansion.- Plugin:
src/snippets/dynamic-bridge.ts(guards inrecomputeIfNeededandexpandDynamicSnippet)
- Plugin:
Tests
- 11 tests in
~/Repos/fengari/test/collectgarbage.test.js: all 8 modes return safe values, pcall succeeds, invalid mode errors - 7 tests in
~/Repos/fengari/test/atnativeerror.test.js: TypeError/RangeError extraction, string/number throws, pure Lua error unaffected, handler covers coroutine threads, without-handler baseline - 10 tests in
~/Repos/fengari/test/gc-finalizers.test.js: userdata__gcdrain, no-overhead without__gc, tables not registered, error swallowing, metatable nil/change unregister, recursive drain guard,lua_closedrain, post-close guard, no-FR graceful degradation - 8 tests in
~/Repos/fengari/test/53bit-integers.test.js: sprintf replacement (format specifiers, flags, hex float), 53-bit integer constants/boundaries, wide arithmetic, string parsing/formatting, table keying, pack/unpack with SZINT=8, 32-bit bitwise verification, wide for-loop - 9 unit tests in
test/unit/lua/regex.test.ts: constructor validation,match_str(offsets + nil),match_linealias,match_posfrom offset,replacewith captures + global flag,testboolean, flags (case-insensitive), invalid pattern error, missing pattern error - 8 spike tests in
~/Repos/fengari/test/coroutine-promise-bridge.test.js:lua_yieldkcontinuations,lua_isyieldable,pcallacross yield, instruction hooks, error propagation, sequential yields, Lua-level vs C-level coroutines - 11 unit tests in
test/unit/lua/coroutine-runner.test.ts: sync path, yield/resume, rejected Promise, pcall error catch, instruction limit, timeout, concurrency limit, destroyAll, sequential async, snippet guard, thread-targeted hooks - 7 unit tests in
test/unit/lua/eval-lua-async.test.ts: sync code, syntax errors, top-level async, sequential async, pcall at top level, side effects across yield, instruction limit - 6 unit tests in
test/unit/lua/fs-read.test.ts: file read, pcall error catch, empty file, readlines, sequential reads, snippet guard - 10 unit tests in
test/unit/lua/package-require.test.ts: module loading, caching, subdirectory resolution, circular require, missing module, path traversal, syntax error, runtime error, load() compilation, load() error - 22 e2e tests in
test/specs/lua-require.e2e.ts: functional behavior (7), error handling (4), sandbox security (11)
Documentation
CHANGELOG.md: Added fengari fork improvements (sprintf, 53-bit integers,__gc,collectgarbage,atnativeerror),vim.regex(), coroutine bridge, async Lua APIs, require(), load(), evalLuaAsync entriesKNOWN_LIMITATIONS.md: 32-bit integer limitation → Implemented (widened to 53-bit), hrtime overflow claim corrected; JS RegExp item 5 → Implemented; sprintf item 7 → Implemented (zero deps); vault file reading → Implemented; coroutine bridge item 1 → Implemented (Phases 1–3); require() item 2 → Implemented; load() item 6 → Implemented;__gcitem 4 → Implemented (userdata via FinalizationRegistry); error message quality item 8 → Implemented (atnativeerror handler);collectgarbageitem 10 → Implemented (safe no-ops); fengari improvement opportunities priority table updated (9/10 implemented, only weak tables remaining)README.md: Updated Lua configuration feature bullet withvim.regex(), async file reading, multi-file configs, and__gcuserdata finalizationCONTRIBUTING.md: Addedregex.ts,coroutine-runner.tsandpackage.tsto codebase structureAGENTS.md: Updated fengari fork section — sprintf-js removed (zero deps), 53-bit integers,vim.regex(), async bridge, require(), load(),__gcvia FinalizationRegistry,collectgarbagesafe no-ops, native error propagation viaatnativeerrordocs/configuration/lua-config.md: Addedvim.regex()API reference section,vim.ob.fs.read/readlinesto fs table,require()andload()sections,collectgarbagebehavior, updated unsupported APIs list~/Repos/fengari/DIFFERENCES.md: Updated behavioral differences table (collectgarbage,__gc), updated inherited limitations (collectgarbage and__gcaddressed)~/Repos/fengari/DIFFERENCES.md: Added “Integer widening” section, sprintf replacement documentation, updated behavioral differences table, updated files modified list
[0.65.0] - 2026-07-17
Fixed
- Block cursor displays wrong character after editor refocus in Live Preview — when the editor lost and regained focus (e.g., opening/closing DevTools), Obsidian’s Live Preview re-expanded hidden markdown formatting (like
##in headings) after focus returned. The block cursor’srequestMeasureran in the same frame as the decoration change, before the browser reflowed the new DOM — causingcoordsAtPos()to read stale layout coordinates and the cursor to display the wrong character. Fixed in the codemirror-vim fork by addingfocusChangedto the block cursor’s redraw trigger and scheduling a deferredrequestAnimationFramere-measure on focus gain, ensuring the cursor reads post-reflow coordinates. (#71)- Fork:
~/Repos/codemirror-vim/src/block-cursor.ts(focusChangedtrigger, deferredrequestAnimationFramere-measure),~/Repos/codemirror-vim/src/index.ts(focus event handler oncontentDOM)
- Fork:
Added
- Replace-with-register operator (
gr{motion}) — implements thegroperator from vim-ReplaceWithRegister.gr{motion}replaces the text covered by {motion} with register contents, discarding the replaced text (register preserved). Supportsgrr(linewise),"xgr{motion}(named registers),{Visual}gr(visual charwise and linewise),[count]grr, and dot-repeat. Blockwise visual mode is a documented no-op for v1. (#72)- Plugin:
src/operators/replace-with-register.ts(new: operator implementation),src/operators/register.ts(new:registerReplaceWithRegister()export),src/main.ts(independent gating viaenableReplaceWithRegister),src/workspace/navigation.ts(conditionalgrn/grr/gra→<leader>rn/<leader>rb/<leader>rarelocation),src/settings.ts(enableReplaceWithRegistersetting in both UI versions),src/vimrc/loader.ts(replacewithregister/rwroptions),src/types/vim-api.d.ts(getRegister()type)
- Plugin:
enableReplaceWithRegistersetting — boolean toggle (default:true) gating thegroperator independently fromenableHardWrap. When enabled,grn/grr/graworkspace bindings are relocated to<leader>rn/<leader>rb/<leader>raunder a “Notes” which-key group. When disabled, legacygrn/grr/grabindings are restored. Configurable via Settings UI (both pre-1.13 and post-1.13),:set replacewithregister/:set rwrin vimrc, orvim.opt.replacewithregisterin Lua.
Tests
- 22 e2e tests in
test/specs/operators.e2e.tsfor replace-with-register:grr(single, multi-line, count),griw,gr$,grl,gri',gr}, named registers ("agriw,"a3grr), visualgr(charwise and linewiseV), register type coercion (linewise↔charwise), cursor positioning, dot-repeat (griw,grr,3grr+.), multi-line register expansion, text object at line boundary. 4 skipped blockwise visual mode tests documenting expected behavior for future implementation.
Documentation
CHANGELOG.md: Added replace-with-register operator entry; added block cursor refocus fix entryKNOWN_LIMITATIONS.md: Updatedgrreplace-with-register parity section — removed[count]grrand dot-repeat gaps (confirmed working), updated test coverage line; added block cursor refocus → FixedREADME.md: Added replace-with-register to features listCONTRIBUTING.md: Addedreplace-with-register.tsto codebase structure, updated workspace navigation descriptionAGENTS.md: Updated workspace navigation descriptiondocs/reference/keybindings.md: Added replace-with-register section, updated workspace nav with<leader>r*bindingsdocs/features/workspace-navigation.md: Updated migration note with new<leader>r*defaultsdocs/features/ex-commands.md: Updated default-key column for relocated commandsdocs/configuration/settings.md: AddedenableReplaceWithRegisterto Vim features groupdocs/configuration/vimrc.md: Addedreplacewithregister/rwrto boolean optionsdocs/configuration/lua-config.md: Addedreplacewithregisterto vim.opt table
[0.64.0] - 2026-07-16
Fixed
- Embedded table cell editor cursor shapes — cell editors in embedded table widget mode now display correct cursor shapes (block for normal, bar for insert) matching the user’s configured
cursorShapessettings. Previously, insert mode showed no cursor and normal mode showed a hollow block due to two issues: (1)cursorShapeswas not passed tocreateEmbeddableEditor()intable-cell-editor.ts, and (2) the cell editor’s CM6 instance does not receive.cm-focusedfrom Obsidian, causing the fork’s unfocused-cursor rule to apply. Fixed with a module-level setter (setCellEditorCursorShapes) wired in bothonload()andreloadFeatures(), apendingCursorShapesstash inembeddable-editor.tsto handle the super-before-assignment timing issue, and a dynamicCSSStyleSheet(viadocument.adoptedStyleSheets) that generates cursor CSS from the user’s configured shapes. (#19)- Plugin:
src/vim/table-cell-editor.ts(setter, dynamic stylesheet),src/editors/embeddable-editor.ts(pendingCursorShapesstash),src/main.ts(setter calls + cleanup)
- Plugin:
- Embedded table cell editor font size and line height mismatch — the cell editor text appeared larger than surrounding rendered table text in some themes, and cell height increased when entering edit mode. Added
font-size: inherit,font-family: inherit,line-height: inheritto.vim-table-cell-editor .cm-editor,font-size: inheritto.cm-content, andpadding: 0+line-height: var(--table-line-height, var(--line-height-tight))to.cm-line. (#19)- Plugin:
styles.css(table cell editor CSS)
- Plugin:
- Table cell editor destroying wikilinks and formatting — editing a cell containing
[[wikilink]],**bold**, or other markdown syntax stripped the syntax on write-back. Two issues: (1) the cell editor read its initial value fromwrapper.textContent(the rendered DOM), which returns plain text without markdown syntax —[[note-a]]becamenote-a. Now reads raw markdown from the document source viagetCellDocumentRange(). (2) On cell editor close, the cell content was restored as plaintextContentwithout re-rendering markdown. Now usesMarkdownRenderer.render()to restore proper inline formatting (wikilinks, bold, italic, code) in the cell wrapper after the editor is destroyed. (#19)- Plugin:
src/vim/table-cell-editor.ts(acceptsrawMarkdownparameter,rerenderCellContent()on close),src/vim/table-nav-controller.ts(passes raw text from document)
- Plugin:
- Textarea vim overlay content not synced on rapid teardown — clicking “Save” via hint mode (
fkey) on a modal while a debounced content sync was pending could close the modal before the CM6 overlay flushed its content to the hidden<textarea>. The host plugin (e.g., Spaced Repetition) read the textarea’s stale value and saved incomplete content. Root cause:teardownActive()cancelled the pending 100ms sync timer and destroyed the editor without flushing. ThehandleBlur()andhandleEscapeAndRedispatch()paths already calledsyncNow()before teardown, but the MutationObserver path (modal removed from DOM) went straight toteardownActive(). Fixed by adding asyncNow()call inteardownActive()immediately after cancelling the timer but before destroying the editor. (#69)- Plugin:
src/vim/textarea-vim-manager.ts(syncNowflush inteardownActive)
- Plugin:
Documentation
CHANGELOG.md: Added entries for embedded table cell editor cursor shapes, font size/line height, wikilink color fixes, and textarea sync-on-teardown fixKNOWN_LIMITATIONS.md: Added Live Preview rendering, cursor shapes, visual mode highlighting, and wikilink color loss limitations under “Table cell vim modality”; textarea content sync race condition → Fixeddocs/features/tables.md: Added Live Preview callout for cell editors
[0.63.0] - 2026-07-16
Added
- Cross-note vim jump list —
<C-o>and<C-i>now navigate backward/forward through a cross-note jump history. Jumps are recorded when navigating between notes viagd/gD, picker file selection (all 12 sources), harpoon, oil, hint mode,:e/:find/:tabnew/:buffer/:bfirst/:blast, structural buffer cycling (]b/[b), and Luavim.cmd("e ..."). Within-buffer jumps (G, gg, /, ?) continue to use the fork’s built-in jump list. Standalone EasyMotion jumps (not operator-pending) are also recorded. The jump list persists across sessions, handles file rename/delete, and supports count prefixes (3<C-o>). New:jumpsex command displays the list. Newjumplist(boolean, default true) andjumplistsize(number, default 200) vim options.- Plugin:
src/vim/jumplist.ts(new:JumpListclass),src/workspace/navigate.ts(new:navigateWithJump/navigateWithJumpFile/navigateWithJumpSetActivewrappers),src/workspace/global-defaults.ts(createJumpListWalkOverride),src/vim/jumplist-bridge.ts(new: CM6 ViewPlugin for fork bridge),src/vim/options.ts(jumplist/jumplistsize),src/easymotion/register.ts(jump recording),src/main.ts(lifecycle, persistence, rename/delete handlers) - 43 navigation call sites migrated across 23 files (goto-definition, picker sources, oil, harpoon, hint mode, global-ex-command, Lua API, buffer cycling, workspace commands, vault search)
- Plugin:
- Table cell vim modality (embedded mode) — cell editors in embedded table widget mode now support a two-Escape pattern: first Escape exits insert → normal mode within the cell editor, second Escape exits the cell editor back to table-nav mode. Entry mode semantics:
i(insert at start),a(append at end),c(clear + insert),s(substitute). Vim registers are shared between cell editors and the main editor. Status bar reflects cell editor vim mode when active.- Plugin:
src/editors/embeddable-editor.ts(mode-aware Escape keymap),src/vim/table-nav-controller.ts(entry mode dispatch viahandleKey),src/vim/mode-tracker.ts(cell editor mode sync)
- Plugin:
ir/artable row text objects —irselects inner row content (between first and last|, excluding pipes),arselects the entire row including pipes. Works in raw markdown mode. Follows the same pattern asi|/a|cell text objects.- Plugin:
src/text-objects/table-row.ts(new),src/text-objects/register.ts
- Plugin:
Fixed
- Hint mode
Fon file explorer and other generic targets opens in current tab instead of new tab — pressingFin hint mode on a file in the left sidebar file explorer (.nav-file-title) opened it in the current tab, identical tof. ThehintOpenNew()function only passedopenInNewPane=trueforlinkandpanetarget types; all other targets (generic,button,input) fell through toopenInNewPane=false, bypassing the existing Ctrl+Meta click path that Obsidian interprets as “open in new tab”. SimplifiedhintOpenNew()to always passopenInNewPane=true— the Ctrl+Meta click dispatch at line 344 already handles all non-link, non-pane targets correctly. (#70)- Plugin:
src/ui/hint-mode.ts(hintOpenNewsimplified to unconditionalopenInNewPane=true)
- Plugin:
jumpListWalkaction override lost afterreloadFeatures()— thedefineActionOverride('jumpListWalk', ...)applied duringonload()was wiped byreloadFeatures()(called during vimrc loading) becauseunregisterAll()restored the original action and the override was not re-registered. Fixed by adding the override toreloadFeatures()alongside the existingnewLineAndEnterInsertModeoverride.- Plugin:
src/main.ts(addedjumpListWalkoverride toreloadFeatures())
- Plugin:
- First character swallowed when entering table cell editor — pressing
iin table-nav mode opened the cell editor and immediately dispatchedhandleKey(adapter, 'i'), but the vim extension on the cell editor’s CM6 instance hadn’t finished initializing. The dispatchediwas either a no-op (vim not ready) or treated as typed text. Fixed by deferring thehandleKeydispatch viasetTimeout(fn, 0).- Plugin:
src/vim/table-nav-controller.ts(deferredhandleKeyinenterCellEdit)
- Plugin:
Tests
- 27 unit tests in
test/unit/jumplist.test.ts:JumpListclass — record, deduplication, jumpOlder/jumpNewer with count, handleRename, handleDelete with index adjustment, serialize/deserialize, max size eviction, forward history truncation, onRecord callback - 10 e2e tests in
test/specs/jump-list.e2e.ts: within-buffer G/gg/count<C-o>, cross-notegd→<C-o>→<C-i>, jump list data structure verification,:jumpsmodal, jumplist setting toggle, deleted file resilience - 4 e2e tests in
test/specs/table-cell-vim-mode.e2e.ts:dir/dar/yirtable row text objects,irno-op on non-table content - 9 skipped e2e tests for embedded table cell editing (two-Escape, entry modes, register sharing) — test-environment limitation where CM6 table widget rendering does not activate through
registerEditorExtensionin WDIO; features verified manually
Documentation
CHANGELOG.md: Added entries for cross-note jump list, table cell vim modality, ir/ar text objects, jumpListWalk override fix, cell editor first-character fix, hint modeFfile explorer fixKNOWN_LIMITATIONS.md: Cross-note jump list → Implemented (with cross-window limitation noted); table cell vim modality → documented two-Escape pattern and entry modes; ir/ar text objects → documented; hint modeF→ updated behavior table (all targets now open in new tab)README.md: Added cross-note jump list to features listCONTRIBUTING.md: Added jumplist.ts, navigate.ts, table-row.ts, jumplist-bridge.ts to codebase structuredocs/reference/keybindings.md: Added<C-o>/<C-i>jump list,:jumps,ir/artext objectsdocs/features/hint-mode.md: UpdatedFaction description to include file explorer and generic targetsdocs/features/tables.md: Added vim modality in cell editors section, table row text objectsdocs/configuration/settings.md: Addedjumplistandjumplistsizesettingsdocs/configuration/vimrc.md: Addedjumplist/jumplistsizeoptions
[0.62.0] - 2026-07-15
Fixed
- Textarea vim overlay height collapses to near-zero after 0.60.1 — the 0.60.1 fix for unbounded textarea growth replaced
minHeightwith fixedheight+maxHeightcopied from the original textarea’s computed size. When the original textarea used dynamic height (e.g.,height: autoor content-dependent sizing), the captured height could be very small, trapping the CM6 overlay at a tiny fixed size with content hidden. Fixed by using adaptive height calculation:minHeight = max(cssHeight, scrollHeight, 100px)ensures a reasonable minimum, andmaxHeight = max(effectiveHeight, 50vh)caps growth at half the viewport with scrollbar overflow. The wrapper’s CSSoverflowchanged fromhiddentoautoso content exceedingmaxHeightscrolls instead of being clipped. (#69)- Plugin:
src/vim/textarea-vim-manager.ts(adaptive height calculation withMIN_HEIGHT_PXfloor),styles.css(overflow: autoon.vim-motions-textarea-overlay)
- Plugin:
- Which-key “all” mode intercepting multi-key Oil bindings — in “All partial keys” mode, the popup delay timer (default 500ms) caused the which-key overlay to appear between the
gand second keystroke (?,.,s,f), disrupting Oil’sg?help modal and otherg-prefixed bindings. Fixed by bypassing the popup delay timer when the active view is an OilView — the overlay shows immediately, allowing multi-key bindings to complete without interference. Operator-pending hints (d,c,y) still work normally in Oil.- Plugin:
src/ui/which-key.ts(onKeyPressGeneralOil context check)
- Plugin:
ci*marked as permanent Live Preview limitation — investigation found thatci*(change inside bold) works correctly in Live Preview for multi-character content. On the active line, Obsidian usesDecoration.mark(visible text nodes), notDecoration.replace— the cursor is not displaced by collapsed decorations. The original limitation was overstated based on early testing with a transaction filter that has since been removed.- Plugin:
test/specs/text-objects.e2e.ts(unskippedci*test, now passing)
- Plugin:
Tests
- 4 new e2e tests in
test/specs/oil-which-key.e2e.ts:g?opens Oil help modal with which-key “all” mode,g.not intercepted, no stale overlay afterg?, leader-mode control - 1 unskipped e2e test in
test/specs/text-objects.e2e.ts:ci*on multi-character bold content
Documentation
CHANGELOG.md: Added entries for which-key + Oil fix,ci*limitation resolution, and textarea height fixKNOWN_LIMITATIONS.md: Which-key + Oil non-editor context → Fixed; which-key “all” mode Oil interception → Fixed;ci*Live Preview → resolved (was overstated); textarea overlay height collapse → Fixeddocs/configuration/which-key.md: Updated Oil explorer context section — removed non-editor and “all” mode warningsdocs/features/text-objects.md: Removedci*limitation note if present
[0.61.0] - 2026-07-15
Fixed
- Hint mode
Fon pane targets opens in same tab instead of new tab — pressingFin hint mode on a pane target (.workspace-leaf-content) behaved identically tof(focus the pane) instead of opening the pane’s content in a new tab. ThehintActivate()function ignored theopenInNewPaneparameter fortargetType === 'pane', always callingsetActiveLeaf(). Now callsworkspace.duplicateLeaf(leaf, 'tab')whenopenInNewPaneis true. Link targets were unaffected —openLinkText()already used the parameter correctly. (#70)- Plugin:
src/ui/hint-mode.ts(hintActivatepane branch)
- Plugin:
j/kand other standard-gate keys not working in Bases views — Obsidian Bases views (.basefiles) use the view type"bases", which was missing from the defaultGLOBAL_NAV_VIEW_TYPESset. TheisPluginLeafActive()check treated Bases as a plugin view and blocked standard-gate keys (j/kscroll,H/Ltab switch, count-prefix digits). (#70)- Plugin:
src/workspace/global-key-handler.ts(added'bases'toGLOBAL_NAV_VIEW_TYPES),src/settings.ts(updated default list in description)
- Plugin:
Tests
- 1 new e2e test in
test/specs/hint-mode.e2e.ts:Fon pane target callsduplicateLeaf(spy-based verification) - 1 new e2e test in
test/specs/global-nav.e2e.ts:Hfrom bases view switches to previous tab (creates.basefile, verifies standard-gate key interception)
Documentation
CHANGELOG.md: Added entries for hint modeFpane fix and Bases view type fixKNOWN_LIMITATIONS.md: Updated hint mode target classification (paneF→duplicateLeaf); updated workspace navigation view type list to includebasesdocs/features/hint-mode.md: Updated pane target behavior descriptiondocs/features/workspace-navigation.md: Addedbasesto default view typesdocs/configuration/settings.md: Updated workspace navigation view types default listdocs/configuration/vimrc.md: Updated default view types in descriptiondocs/configuration/lua-config.md: Updated default view types in examples
[0.60.1] - 2026-07-15
Fixed
- Textarea vim setting not visible in legacy settings UI — the “Vim keybindings in text areas” toggle was only added to the new
getSettingDefinitions()API (Obsidian 1.13+). Users on Obsidian <1.13 could not find or enable the setting. Added the toggle to the legacydisplay()method in the “Vim features” section. (#69)- Plugin:
src/settings.ts(addedenableVimTextareastoggle to legacy settings UI)
- Plugin:
- Textarea vim overlay grows with content instead of fixed size — the CM6 editor overlay expanded vertically as content grew, unlike the original textarea which had a fixed height with scrollbar. Changed the wrapper to use
height+maxHeight(copied from the original textarea’s computed size) and addedoverflow: autoto the CM6 scroller, so content scrolls within the original textarea’s dimensions. (#69)- Plugin:
src/vim/textarea-vim-manager.ts(height+maxHeightinstead ofminHeight),styles.css(overflow: autoon.cm-scroller)
- Plugin:
- Textarea vim overlay text larger than original — the CM6 editor used Obsidian’s default editor font size instead of inheriting from the original textarea. Added
font-size: inheritto.vim-motions-textarea-overlay .cm-editorso the overlay matches the original element’s font size. (#69)- Plugin:
styles.css(font-size: inheriton.cm-editor)
- Plugin:
- Removed
!importantfrom textarea hidden styles — the.vim-motions-textarea-hiddenCSS class used!importanton all four properties, which is not allowed by the project’s CSS conventions.- Plugin:
styles.css
- Plugin:
[0.60.0] - 2026-07-15
Added
- Vim keybindings in text areas — focused
<textarea>elements (e.g., flashcard edit modals from Spaced Repetition) are replaced with a vim-enabled CodeMirror 6 editor overlay. Starts in insert mode for transparent typing; press Escape for normal mode with full vim support. Second Escape tears down the overlay and returns focus to the original textarea (modal stays open). Content syncs back to the hidden textarea continuously (100ms debounce) with syntheticinput/changeevents. Desktop only, disabled by default. (#69)- Plugin:
src/vim/textarea-vim-manager.ts(new),src/editors/embeddable-editor.ts(skipActiveEditoroption),src/main.ts(registration),src/settings.ts(enableVimTextareas),src/vim/options.ts(vimtextareas/vta),src/vimrc/loader.ts(KNOWN_SET_OPTIONS),styles.css(overlay + hidden styles)
- Plugin:
Documentation
KNOWN_LIMITATIONS.md: Added “Vim keybindings in text areas” section with scope, limitations (no input/contenteditable/iframe support, framework re-render conflicts, programmatic value detection, popout windows, maxlength enforcement)docs/configuration/settings.md: Added “Vim keybindings in text areas” setting to Vim features tabledocs/configuration/vimrc.md: Addedvimtextareas/vtato boolean options tabledocs/configuration/lua-config.md: Addedvimtextareastovim.opttable
[0.59.0] - 2026-07-15
Fixed
- Absolute line number highlight not updating on cursor movement — when only absolute line numbers were enabled (
set numberwithoutset relativenumber), thevim-motions-line-num-currenthighlight (bold current line number) did not follow the cursor. ThelineMarkerChangecallback in both the standalone line number gutter and the unifiedstatuscolumngutter only checkedupdate.docChangedin absolute mode, ignoringupdate.selectionSet(cursor movement). Relative and hybrid modes were unaffected because they already includedupdate.selectionSet. The highlight only updated incidentally when entering special content (MathJax, images) that triggereddocChangedorviewportChanged. (#68)- Plugin:
src/vim/line-number-gutter.ts(lineMarkerChangeabsolute branch),src/vim/statuscolumn.ts(lineMarkerChange!hasRelativebranch)
- Plugin:
- Typing
|moves cursor to the left of|— typing|anywhere in insert mode triggered the table auto-format inputHandler, which intercepted the keystroke and repositioned the cursor even on non-table lines (any line matching/^\s*\|/). The mid-edit interception caused cursor jumps, making it impossible to type|normally in an empty document or at the start of a line. (#66) - Tables do not handle escaped
|characters correctly — in raw/cursor-aware table modes, escaped pipes (\|) inside cells were treated as cell boundaries during mid-edit auto-formatting, causing the cursor to jump and live preview to render extra cells. The auto-format inputHandler ranrealignTableLines()after every|keystroke, which repositioned the cursor based on the realigned table structure — even when the|was escaped. Wikilinks ([[page|alias]]) inside raw table cells also triggered incorrect cell boundary detection. (#67)- Root cause for both:
table-auto-format.tsintercepted every|keystroke in insert mode and ran column realignment mid-edit. - Fix: Replaced mid-edit
|interception with format-on-exit — tables are only realigned when the cursor leaves the table range. A CM6ViewPlugintracks cursor entry/exit from table ranges and dispatchesrealignTableLines()viaqueueMicrotaskwhen the cursor exits a dirty (edited) table. UsesAnnotation.define<boolean>()as a re-entrancy guard. The||→ separator row auto-generation is preserved as a standalone inputHandler. - Plugin:
src/vim/table-format-on-exit.ts(new: format-on-exit ViewPlugin + separator handler),src/vim/table-utils.ts(addedrealignTableLines(),parseAlignments(),buildSepCell(),findTableBounds(), canonicalAlignmenttype),src/vim/table-auto-format.ts(deleted),src/vim/table-cursor-fix.ts(deleted),src/vim/table-operations.ts(refactored to use sharedrealignTableLines()),src/motions/tables.ts(refactored to use sharedrealignTableLines(), removed 5 duplicate helpers),src/vim/table-render-widget.ts(uses canonicalAlignmenttype),src/vim/table-nav-controller.ts(addedtableRealign()indoRefreshAfterOp()for embedded cell edit alignment),src/vim/table-cell-editor.ts(removedcreateTableAutoFormatExtensiondependency),src/motions/register.ts(removedtableAwareMoveUpoverride onk),src/main.ts(replaced auto-format/cursor-fix registration with format-on-exit)
- Root cause for both:
Changed
- Neovim-style modal styling for
GlobalExCommandModalandVimInfoModal— the ex command modal (:in non-editor views) and the info modal (:marks,:buffers,:registers, Oilg?) now use Neovim-inspired styling: transparent container, accent border (--color-accent), floating title label positioned on the top border, monospace font, and hidden Obsidian chrome (close button, modal header).GlobalExCommandModaluses a prompt-modal pattern with three styled sections (input, results, instructions) and two-column suggestion rows showing:command+ description. All 40+ ex commands now havedescriptionfields.VimInfoModaluses an info-modal pattern with an accent-bordered inner wrapper. Both patterns use--modal-background,--font-monospace, and--color-accentCSS variables for full theme compatibility.- Plugin:
src/ui/global-ex-command.ts(descriptionfield onGlobalExEntryandExSuggestion, prompt-modal container styling, two-columnrenderSuggestion),src/ui/vim-info-modal.ts(info-modal container styling, floating title, inner wrapper),styles.css(new prompt-modal and info-modal CSS sections)
- Plugin:
- Neovim-style modal styling extended to remaining modals —
OutlineModal(:outline/gO),SearchResultsModal(:vimgrep),ContextActionsModal(gra), andOilConfirmModal(Oil destructive commit) now use the same Neovim-inspired styling asGlobalExCommandModalandVimInfoModal.OutlineModalshows heading text + line number,SearchResultsModalshows filename + line preview,ContextActionsModalshows command name + command ID.OilConfirmModaluses the info-modal pattern with accent-bordered buttons. Removed 3 unused CSS classes (vim-motions-search-file,vim-motions-search-preview,vim-motions-outline-item).- Plugin:
src/ui/outline-modal.ts,src/workspace/vault-search.ts,src/ui/context-actions.ts,src/oil/manager.ts,styles.css
- Plugin:
Documentation
KNOWN_LIMITATIONS.md: Absolute line number highlight not updating on cursor movement → FixedKNOWN_LIMITATIONS.md: Table auto-formatting → updated to describe format-on-exit behavior; typing|cursor jump (#66) → Fixed; escaped\|handling (#67) → Fixeddocs/features/tables.md: Auto-formatting section rewritten to describe format-on-exit behavior
[0.58.0] - 2026-07-15
Fixed
- Table escaped pipes — cells containing escaped pipes (
\|) no longer corrupt cell boundaries during navigation, text object operations, or embedded cell editing write-back. All pipe-boundary detection across 7 files now uses shared escape-aware utilities (findUnescapedPipes()/splitCellsEscapeAware()) intable-utils.ts. Escaped pipes (\|) are treated as cell content;\\|(escaped backslash + real pipe) is correctly treated as a boundary via backslash-parity checking.- Plugin:
src/vim/table-utils.ts(new:findUnescapedPipes(),splitCellsEscapeAware(),countPrecedingBackslashes()),src/vim/table-render-widget.ts,src/vim/table-operations.ts,src/vim/table-auto-format.ts,src/vim/table-nav-controller.ts,src/text-objects/table-cell.ts,src/motions/tables.ts(all updated to use shared utilities)
- Plugin:
- Vimrc file I/O timing —
readVimrcFile()now usesstat()as a readiness probe beforeread(), distinguishing genuinely empty files (stat.size === 0, no retry) from timing-empty reads (stat.size > 0, retry with extended backoff 50/100/200/400ms).fileExists()usesstat()instead of a fullread(). On retry exhaustion, a user-facing Notice is shown. ThevimrcLoadingflag is now wrapped intry/finallyso a failedloadVimrc()call no longer permanently blocks future retry attempts. The samestat()+retry pattern is applied toreadLuaFile()in the Lua config loader.- Plugin:
src/vimrc/loader.ts(readVimrcFile,fileExists),src/lua/loader.ts(readLuaFile),src/main.ts(try/finallyaround vimrc loading)
- Plugin:
- Oil which-key labels not appearing —
getCommandLabels()inOilKeybindingManagerreturned empty when oil bindings were not yet applied (theif (!this.applied) return []guard prevented labels from being registered duringrebuildWhichKey()). Labels are now always returned — they are static metadata fromOIL_MAPPINGS, valid regardless of whether the vim mappings are currently active.- Plugin:
src/oil/keybindings.ts(removedappliedguard fromgetCommandLabels())
- Plugin:
- Live grep UI blocking on large vaults — the live grep picker source now uses chunked async iteration (50 files per chunk) with event loop yields between chunks, preventing UI freezes during searches on vaults with many files. Functionally identical results.
- Plugin:
src/picker/sources/live-grep.ts(chunked iteration withwindow.setTimeout(0)yields)
- Plugin:
Changed
- Oil
g?help usesVimInfoModal—g?in Oil now opens a modal dialog (Key/Action table) instead of a custom DOM overlay. This follows the same pattern used by:marks,:buffers, and:registerswhen the picker is disabled. Dismissible via Escape.- Plugin:
src/oil/keybindings.ts(showOilHelprewritten to useVimInfoModal),styles.css(removed.vim-motions-oil-help*CSS)
- Plugin:
Tests
- 5 e2e tests in
test/specs/table-escaped-pipes.e2e.ts(new):di|with\|content,\\|as real pipe boundary,]|navigation skipping\|,yi|yanking cell with\|,:tablerealignpreserving\|cell content
Documentation
KNOWN_LIMITATIONS.md: Table escaped pipes → Fixed;g?oil help command → Fixed (was listed as “planned but not yet implemented”); oil which-key integration → Fixed; surroundysdot-repeat with tag/function → clarified as working at runtime (test infrastructure limitation only); vimrc timing section updated with improved retry mechanismKNOWN_LIMITATIONS.md: Added two new Oil limitations — which-key/g?not working in non-editor context (no prior editor leaf), and which-key “all” mode intercepting multi-key Oil bindings (g?,g.,gs,gf)
[0.57.0] - 2026-07-14
Fixed
- Table cell edits not rendered immediately in embedded mode — editing a table cell and pressing Escape or Tab wrote the change to the document but did not visually update the rendered table widget until the user navigated away or switched cells. The
tableRenderFieldStateField’supdate()method has a “D12 guard” (activeEditTableRange) that, when set, maps old decorations instead of rebuilding them ondocChanged. This guard was correctly cleared for table operations (o,dd,J,K, etc.) viaexecuteTableOp()and for full table exit viaexitTable(), butexitCellEdit()— the path taken when closing a cell editor — never cleared it. The dispatch fromcloseCellEditor()triggereddocChangedwhile the guard was still active, causingprev.map(tr.changes)(position-only shift) instead ofbuildDecorations()(full HTML rebuild). Fixed by addingsetActiveEditTableRange(null)beforecloseCellEditor()and replacing the immediatehighlightCell()withrefreshAfterOp()— the same pattern used by all other table mutation paths. (#61)- Plugin:
src/vim/table-nav-controller.ts(exitCellEdit())
- Plugin:
- Visual-line mode highlight missing on replaced widget blocks — in visual-line mode (
V), the fork’slinewiseVisualHighlightViewPlugin usesDecoration.line()to highlight selected lines, but replaced widget blocks (MathJax$$, note embeds![[note]], plugin table widgets) have no.cm-lineelements in the DOM — CM6 silently drops line decorations for replaced ranges. Fixed by adding a plugin-sideLinewiseWidgetHighlightViewPlugin that scanscontentDOMdirect children for non-.cm-linewidget elements and togglescm-vim-linewise-widget-selectionon widgets whose document range overlaps the visual-line selection. Usesview.posAtDOM()for position mapping. Generic — works for all replaced widget types, not just MathJax. (#57)- Plugin:
src/vim/linewise-widget-highlight.ts(new),styles.css(.cm-vim-linewise-widget-selectionrule),src/main.ts(extension registration)
- Plugin:
[0.56.0] - 2026-07-14
Added
- Surround
csf(change surrounding function name) —csfchanges the function name around the cursor. Prompts for the new function name via afunc:status bar prompt; press Enter to apply or Escape to cancel. Dot-repeat (.) replays with the saved name. Uses the samefindSurroundingFunctionasdsf(single-line only). Handles nested calls and method chains.- Fork:
~/Repos/codemirror-vim/src/vim.js(target === 'f'case in change operator,pendingInputprompt,funcResultfallback inhandleSurroundSubState)
- Fork:
- Oil which-key integration — the which-key popup now shows Oil-specific keybindings (
-,Enter,~,g.,gs,y.,gf,g?,q,Ctrl-l) with descriptions when an Oil view is active. Descriptions are single-sourced from theOIL_MAPPINGSarray and fed into the which-keycommandLabelsmap viagetCommandLabels().- Plugin:
src/oil/keybindings.ts(descfield onOilMapping,getCommandLabels()method),src/main.ts(oil labels injected intorebuildWhichKey())
- Plugin:
- Oil
g?help overlay — pressg?in Oil to toggle a help overlay listing all Oil keybindings with descriptions. Entries are derived fromOIL_MAPPINGSto prevent drift. Dismissible viag?(toggle) or Escape.- Plugin:
src/oil/keybindings.ts(g?mapping,showOilHelpmethod),styles.css(oil help overlay styles)
- Plugin:
- IM platform presets — a settings dropdown auto-fills binary path, arguments, and default IM for common tools: macism (macOS), im-select (Windows), fcitx5-remote (Linux), ibus (Linux). Values are editable after selection.
- Plugin:
src/settings.ts(imPresetsetting,IM_PRESETSdata, preset dropdown in both legacy and searchable settings UI)
- Plugin:
:IMToggle/:IMStatusex commands —:IMToggleenables/disables IM switching and saves the setting.:IMStatusqueries the current IM identifier and displays it via a Notice.- Plugin:
src/main.ts(registerImExCommands())
- Plugin:
- IM session persistence — per-editor IM state is persisted to plugin settings via
saveData()(30-second interval + immediate save on unload). On plugin load, the persisted state is restored so the firstInsertEnteruses the correct IM instead of the default.- Plugin:
src/im/im-switcher.ts(loadPersistedState(),getPersistedState()),src/settings.ts(persistedImStatefield),src/main.ts(load/save wiring)
- Plugin:
- Special marks in picker — the
:markspicker now shows special marks (',.,<,>) under a “Special marks” group between buffer and global marks.- Plugin:
src/picker/sources/mark-providers.ts(SpecialMarkProvider),src/main.ts(registered as third provider)
- Plugin:
:grepregex support —:grepnow uses JavaScriptRegExpfor pattern matching instead of Obsidian’sprepareSimpleSearch. Invalid regex patterns gracefully fall back to substring matching. This matches Neovim’s:grepbehavior where the pattern is a regex.- Plugin:
src/picker/sources/grep.ts(createMatcher()withRegExp+ fallback),src/picker/sources/live-grep.ts(same pattern)
- Plugin:
Changed
loadInitLua()parameter refactor — the function signature was refactored from 11 positional parameters to(app, vim, options?)with aLoadInitLuaOptionsinterface. All callers updated.- Plugin:
src/lua/loader.ts(LoadInitLuaOptionsinterface, destructured options),src/main.ts(caller updated)
- Plugin:
Fixed
- Vimrc loading reliability —
readVimrcFilenow retries with exponential backoff (50ms, 100ms, 200ms) when the vault adapter returns empty content during earlyactive-leaf-changeevents. The arbitrary 100ms safety-net timeout for map re-application has been removed. This addresses the intermittent issue wherenmap L $orset textwidthcommands were silently dropped on plugin load.- Plugin:
src/vimrc/loader.ts(readVimrcFileretry logic),src/main.ts(removed 100ms setTimeout)
- Plugin:
- Vimrc soft-reload — the vimrc file is now watched via
vault.on('modify'). When modified, maps and settings are re-applied without a plugin reload. Previous vimrc-sourced maps are unmapped before re-application to prevent accumulation.exmapdefinitions from the initial load persist (documented as known limitation).- Plugin:
src/main.ts(softReloadVimrc(),vimrcMapKeystracking,vault.on('modify')handler)
- Plugin:
- BufEnter initial fire destroying buffer-local keymaps — function-callback keymaps registered in
BufEnterautocmd handlers during the initial syntheticBufEnterwere destroyed by the subsequentreloadFeatures()→vim.resetKeymap()call. Fixed by deferring the initialBufEnterfire until afterreloadFeatures()andapplyLuaMaps()complete. TheAutocmdManagernow stores the initial file path inpendingInitialBufEnterPathand exposesfireInitialBufEnter()to fire it at the correct lifecycle point.- Plugin:
src/lua/autocmd.ts(pendingInitialBufEnterPath,fireInitialBufEnter()),src/main.ts(call after reload)
- Plugin:
- Blockquote fenced code block detection —
findFenceLinesnow matches fences prefixed with blockquote markers (> ```). The regex was updated from/^```/to/^(?:>\s*)*```/with blockquote depth matching to ensure open/close fences are at the same nesting level. This fixes multi-line text objects and smart list continuation incorrectly matching delimiters inside blockquote code blocks.- Plugin:
src/text-objects/code-block.ts(FENCE_OPEN/FENCE_CLOSEregexes,blockquoteDepth())
- Plugin:
- EasyMotion operator-pending dot-repeat —
d<leader><leader>w{label}followed by.now replays the delete to the same relative position. The fork stores the resolved async motion position as a relative offset inlastEditInputState._asyncMotionTargetafterapplyOperatorsucceeds. During dot-repeat,repeatLastEditdetects this stored target and applies the operator directly instead of re-executing the async motion overlay.- Fork:
~/Repos/codemirror-vim/src/vim.js(_asyncMotionTargetstorage in async.then(),repeatCommand()offset replay)
- Fork:
- Surround
ysdot-repeat with text object motions —ysiwb(surround inner word with parentheses) then.on a different word now correctly replays the surround. The fork stores the text object motion characters (_ysTextObjectMotionand_ysTextObjectChar) inlastEditInputStatevia theonRepeatcallback. During dot-repeat,repeatLastEditre-evaluates the text object at the current cursor viatextObjectManipulation()and appliesaddSurroundToRange(). Works for all simple delimiters (ysiwb,ysiw",ysaw',ysiw]). Tag (ysiw<em>) and function (ysiwflen) dot-repeat is verified at the fork level (1806/0) but cannot be tested via WDIO due to</>key dispatch conflicts with vim’s angle-bracket notation parser — these tests are skipped in the plugin e2e suite with a reference to the fork tests.- Fork:
~/Repos/codemirror-vim/src/vim.js(_ysTextObjectMotion/_ysTextObjectCharinonRepeat,repeatCommand()text object replay),~/Repos/codemirror-vim/src/types.ts(3 newInputStateInterfacefields),~/Repos/codemirror-vim/DIFFERENCES.md
- Fork:
- Settings parity between pre-1.13 and post-1.13 Settings UI — all plugin settings are now exposed in both the legacy
PluginSettingTab.display()method (Obsidian <1.13) and the newgetSettingDefinitions()API (Obsidian 1.13+). Previously, 22 settings were missing from the legacy UI and 9 from the new UI:- Added to legacy settings (pre-1.13): Input method section (7 settings: enable, binary path, obtain/switch args, normal mode IM, restore behavior, default insert IM), Fuzzy picker for buffers, Picker leader mappings, Picker matching engine, Third-party integrations (Omnisearch, Obsidian Tasks, Dataview), Show config load notifications, Which-key popup delay, 7 mode prompts (visual line, visual block, select, virtual replace, command, search, insert-normal)
- Added to new settings (1.13+): Snippets group (4 settings: enable, bundled, directory, trigger mode), File explorer group (4 settings: oil explorer, show hidden files, confirm delete threshold, default sort order), Workspace navigation view types
- Plugin:
src/settings.ts(display()andgetSettingDefinitions())
- Lua runtime callbacks now have infinite loop protection — all 5 runtime
lua_pcallsites (function keymaps, user commands, autocmd handlers, timer callbacks, snippet f()/d() nodes) are now wrapped withwithInstructionGuard, which setslua_sethookwithLUA_MASKCOUNTbefore each call and clears it after. Instruction limit: 500,000 for callbacks, 100,000 for snippet nodes. On timeout, a throttled Notice is shown (5-second cooldown). Obsidian remains responsive.- Plugin:
src/lua/types.d.ts(type fix),src/lua/engine.ts(withInstructionGuard,showLuaErrorNotice),src/lua/api.ts(3 sites),src/lua/timers.ts(1 site),src/snippets/dynamic-bridge.ts(2 sites)
- Plugin:
- Global marks updated on file rename/delete — renaming a file now updates all global marks (
A–Z) pointing to it. Deleting a file removes the marks. Follows the samevault.on('rename')/vault.on('delete')pattern as harpoon and fold persistence.- Plugin:
src/vim/mark-store.ts(renamePath(),removeByPath()),src/main.ts(4 lines in event handlers)
- Plugin:
- Surround
csbBysaBbchain now works —yswith text object motions (aB,iw,a", etc.) aftercsor standalone now correctly applies the surround. Theys_motionhandler directly evaluates text object motions instead of dispatching throughhandleKey→evalInput, whereclearInputStatewould lose theselectedCharacter. 74/74 nvim-surround golden comparison tests now pass (up from 73/74).- Fork:
~/Repos/codemirror-vim/src/vim.js(ys_motionhandler,operatorArgson surround state),~/Repos/codemirror-vim/src/types.ts(operatorArgsfield)
- Fork:
- Surround dot-repeat cross-type leak prevention — the surround dot-repeat guard was tightened to only use saved replacements when
_surroundTypematches the current operation type (cs,ys,yss), preventing stale state from a priorcsoperation from silently consuming a subsequentyscommand.- Fork:
~/Repos/codemirror-vim/src/vim.js(tightenedsavedReplacementguards with_surroundTypematch)
- Fork:
- Vimrc loading decoupled from CM adapter — vimrc parsing no longer requires a CM adapter.
loadVimrcnow uses a two-phase approach:readAndParseVimrcFilereads and parses the file without needing an editor (reusingparseVimrcfromparser.ts), thenapplyVimrcCommandsapplies all 14VimrcCommandtypes with explicit handling —exmapis applied eagerly (no CM needed), unknown commands deferred topendingExCommandsand applied when a CM adapter becomes available. The 5×50ms retry loop has been removed. The 100ms safety-net map reapplication is kept for CM Vim keymap init timing.- Plugin:
src/vimrc/loader.ts(readAndParseVimrcFile,applyVimrcCommands,applyPendingExCommands, refactoredloadVimrc),src/main.ts(retry loop removed,pendingVimrcExCommandsfield, deferred application inactive-leaf-changehandler)
- Plugin:
Documentation
KNOWN_LIMITATIONS.md: EasyMotion dot-repeat → Fixed; surroundysdot-repeat with text objects → Fixed (simple delimiters), tag/function pending; blockquote fence detection → Fixed; grep regex → Fixed; special marks in picker → Fixed; IM presets/persistence/ex commands → Implemented;loadInitLuarefactor → Implemented; vimrc hot-reload → updated to soft-reload; vimrc loading reliability → updated with retry logic; tagcst/yst→ Verified;ci*Live Preview → Permanent;selectmode=mouse→ PermanentDIFFERENCES.md(fork): Addedcsfsection; updated surround summary; addedys_motiontext object fix; added async motion dot-repeat (_asyncMotionTarget); added surroundystext object dot-repeat (_ysTextObjectMotion)docs/features/oil-explorer.md: Addedg?to oil ex commands tabledocs/configuration/which-key.md: Added Oil explorer context section (fork mode only)docs/configuration/settings.md: Added IM preset row to Input method tabledocs/features/ex-commands.md: Added:IMToggleand:IMStatusto ex commands referenceCHANGELOG.md: Added entries for all 13 implemented items across Phases 1–6
[0.55.0] - 2026-07-13
Changed
- Sign column migrated to dedicated gutter column — vim mark indicators (
a–z,A–Z) now render in a proper CM6gutter()column instead of usingDecoration.line()+ CSS::afteroverlays. Fixes marks cascading vertically into the wrong line, overlapping on multi-mark lines, and inheriting heading font sizes. The gutter layout from left to right is: sign column → line numbers → fold column → content, matching Neovim’s default arrangement. UsesCompartment-based runtime reconfiguration —:set signcolumn=yes/auto/notakes effect without full feature reload.signcolumn=autonow causes layout shift when marks appear/disappear (matching Neovim behavior);signcolumn=yesalways reserves gutter space. (#59)- Plugin:
src/vim/sign-column.ts(rewritten:gutter()+GutterMarker+Compartment),src/vim/mark-gutter.ts(updated re-exports),src/main.ts(unconditional registration,reconfigureSignColumnGutter()),src/settings.ts(removed fromRELOAD_KEYS, dedicated handlers in both settings panels,enableMarkGutterdeprecated),src/vimrc/loader.ts(markguttermapped tosigncolumnviasideEffect),styles.css(gutter element styles replacing::afteroverlay)
- Plugin:
enableMarkGuttersetting deprecated — the booleanenableMarkGutterproperty is now optional with@deprecatedJSDoc. Existing settings are auto-migrated tosigncolumnviasettings-migration.ts. The property is kept in the interface for migration type safety only.
Added
- Dual line number display — new
linenumbermodeoption (hybrid/dual/dual-rel-abs) shows absolute and relative line numbers in separate side-by-side gutter columns.set number relativenumber linenumbermode=dualrenders absolute on the left, relative on the right (configurable viadual-rel-absfor the reverse). The defaulthybridmode is unchanged — existing configs are fully backward compatible. Auto-disabled on mobile viewports (≤600px, falls back to hybrid). Configurable via Settings UI dropdown,set linenumbermode=dual(aliaslnm) in vimrc, orvim.opt.linenumbermode = "dual"in Lua.- Plugin:
src/vim/line-number-gutter.ts(dual compartments,resolveGutters()),src/vim/options.ts(linenumbermodeoption),src/settings.ts(setting + UI dropdown),src/vimrc/loader.ts(linenumbermode/lnm),src/main.ts(passlinenumbermodeto extension creation/reconfiguration)
- Plugin:
- Global vs local mark color differentiation — global marks (
A–Z) render in--text-mutedcolor, local marks (a–z) render in--text-accent. The first character of the label determines the CSS class (.vim-motions-sign-marker-globalor.vim-motions-sign-marker-local).- Plugin:
src/vim/sign-column.ts(SignMarker.toDOM()case detection),styles.css
- Plugin:
- Click-to-navigate on mark labels — clicking a mark label in the sign column gutter moves the cursor to that line. Uses
domEventHandlers.clickon the gutter, querying thesignColumnFieldRangeSet for markers at the clicked line.- Plugin:
src/vim/sign-column.ts(domEventHandlers.click)
- Plugin:
signcolumnwidth modes —signcolumnnow acceptsauto:Nandyes:Nsyntax (N = 1–4) to control sign column character width.set signcolumn=auto:3reserves 3 character slots when marks exist.set signcolumn=yes:2always reserves 2 character slots. Validation via regex; invalid values silently rejected.- Plugin:
src/vim/sign-column.ts(parseSignColumnMode(),isValidSignColumnValue()),src/vim/options.ts(validation),src/settings.ts(type widened tostring),src/vimrc/loader.ts(removedvalidValues)
- Plugin:
statuscolumnAPI — Neovim-compatiblestatuscolumnoption for user-configurable gutter layout. A format string controls which gutter segments appear and in what order:%l(line number),%r(relative number),%s(sign column marks),%C(fold indicators),%=(separator), and literal text. When set, the unified gutter replaces all individual gutter columns. When empty (default), individualsigncolumn/number/relativenumber/foldcolumnsettings manage gutters independently. Configurable viavim.opt.statuscolumn = "%s %l %r %C"in Lua orset statuscolumn(aliasstc) in vimrc. Click handlers on sign and fold segments preserved.linenumbermodedeprecated —dualmaps tostatuscolumn = "%l %r". (#59)- Plugin:
src/vim/statuscolumn.ts(new: parser, compositeStatusColumnMarker, unified gutter,StatusColumnSpacer),src/vim/sign-column.ts(signColumnFieldextracted as standalone extension,SignMarker.labelpublic),src/vim/line-number-gutter.ts(computeLineNumber+getNumberwidthexported),src/vim/mark-gutter.ts(re-exportssignColumnFieldExtension),src/vim/options.ts(statuscolumnoption),src/settings.ts(statuscolumnsetting),src/vimrc/loader.ts(statuscolumn/stc),src/main.ts(standalonesignColumnFieldregistration,statusColumnCompartment,reconfigureStatusColumnGutter()),styles.css(statuscolumn segment styles)
- Plugin:
Fixed
- Status bar vim mode duplication — the vim mode indicator and chord display were duplicated in the status bar when the plugin had a non-default
clipboardortextwidthsetting saved. The settings restoration loop duringonload()(lines 717-739 ofmain.ts, added in v0.53.0 by commit947c6a7) calledapplySettingOverride→reloadFeatures()beforeonload()had created its ownVimModeTrackerandGlobalKeyHandler.reloadFeatures()created these resources, thenonload()overwrotethis.modeTrackerandthis.globalKeyHandlerwith new instances — without destroying the first ones. The orphanedVimModeTrackerleft duplicateaddStatusBarItem()DOM elements in the status bar, and the orphanedGlobalKeyHandlerleft a duplicatekeydownlistener on the document (causing intermittent leader key failures). Fixed by adding aninitializingphase guard toapplySettingOverride— all 6 side-effect branches (5 gutter reconfigurations +reloadFeatures()) are suppressed duringonload(), matching the existingvimrcLoading/luaLoadingguard pattern. Settings mutations tothis.settingsstill apply immediately; only premature side effects are blocked. (#63)- Plugin:
src/main.ts(initializingflag, 6 guard site updates inapplySettingOverride)
- Plugin:
iterateEditorViewscrash on non-editor leaves —reconfigureLineNumberGutter()and other gutter reconfigure methods could throwview.dispatch is not a functionon leaves where the CM6 EditorView chain resolved to a non-EditorView object. Fixed by adding atypeof cm.dispatch === 'function'guard initerateEditorViews.- Plugin:
src/main.ts(iterateEditorViewstype guard)
- Plugin:
Tests
- 10 e2e tests in
test/specs/marks-gutter.e2e.ts(4 new, 6 updated): mark in gutter, mark move, multiple marks, delmarks removal, no marks = empty, dedicated gutter column, no line overlays, ellipsis truncation (4+ marks), consistent font size on headings, nodata-vim-marksattribute - 4 e2e tests in
test/specs/statuscolumn.e2e.ts: no statuscolumn by default, option registered in vim engine, sign column gutter present, mark gutter functionality preserved
Documentation
docs/features/marks.md: rewritten gutter indicators section — dedicated gutter column, fixed font size, truncation, three-mode table, gutter layout orderdocs/configuration/settings.md: updated sign column description, added gutter layout tip with ASCII art exampledocs/configuration/lua-config.md: expanded hybrid line numbers tip with gutter layout exampleKNOWN_LIMITATIONS.md: updatedsigncolumnsection — removed zero-width overlay note, updated behavior descriptionREADME.md: updated marks feature description
[0.54.0] - 2026-07-13
Added
- Which-key auto-resolves Obsidian command names for
:obcommandmappings — when a key is mapped to:obcommand <id><CR>or:ob <id><CR>without an explicitdesc, the which-key popup now displays Obsidian’s native command name instead of the raw ex command string. For example,:ob app:go-back<CR>displays as “Navigate back”. Explicitdescoptions still take priority. Unknown command IDs fall back to the raw string. Descriptions are automatically localized — Obsidian’s built-in commands already have localized names, so descriptions match the user’s Obsidian language setting. Works in both editor which-key (leader bindings,vim.keymap.set) and global which-key (:gmap,vim.obsidian.keymap.set). (#62)- Plugin:
src/ui/which-key.ts(lookupObsidianCommandName(),resolveObCommandDescription(),OB_COMMAND_RHS_REregex matching both literal spaces and<Space>notation),src/ui/global-which-key.ts(describeAction()obcommand resolution)
- Plugin:
Changed
- Consolidated Obsidian internal API access into
src/util/utilities — extracted typed accessor functions for 6 internal Obsidian APIs, replacing ~40 inline(x as unknown as { ... })casts across 16 files. Each utility centralizes the unsafe cast in one location and exposes a clean typed function:src/util/commands.ts:executeCommand(app, id),getCommandRegistry(app)— 20 casts across 11 filessrc/util/editor.ts:getEditorView(view)— extracts CM6EditorViewfromMarkdownView, replacing 6 inline casts across 5 filessrc/util/leaf.ts:getLeafId(leaf),isLeafPinned(leaf),getViewFilePath(view),getViewFileBasename(view)— 12 casts across 3 filessrc/util/metadata.ts:getResolvedLinks(app)— 2 casts across 2 filessrc/util/vault.ts:getVaultConfig(app, key),isBuiltinVimEnabled(app)— 4 casts across 4 filessrc/util/keymap.ts:pushKeymapScope(app, scope),popKeymapScope(app, scope)— 3 casts in 1 file
Tests
- 22 unit tests in
test/unit/which-key.test.ts:lookupObsidianCommandName()(4 tests),describeKeymapEntry()without app (6 tests),describeKeymapEntry()with app and obcommand auto-resolution (12 tests covering:ob/:obcommandshort/long form,<CR>variants,<Space>separator, unknown commands, label priority, edge cases) - 6 e2e tests in
test/specs/which-key-obcommand.e2e.ts: editor which-key auto-resolution for:oband:obcommand(2 tests), explicitdescpriority (1 test), unknown command fallback (1 test), global which-key auto-resolution via registry API (1 test), global unknown command fallback (1 test)
[0.53.0] - 2026-07-13
Fixed
- Which-key descriptions not showing for keymaps set via Lua —
vim.keymap.setandvim.obsidian.leader.setwith adescoption showed raw action names (lua-action-0), command strings (:Oil<CR>), or internal function names (harpoonSelect1) instead of the user’s description. The codemirror-vim fork normalizes literal space characters to<Space>notation in key strings (e.g.," ff"→"<Space>ff"), but the which-key overlay stored and looked up label keys using unnormalized literal spaces — all lookups missed. Additionally, the leader-only which-key mode never triggered with space as leader because thevim-keypressevent emits"<Space>"but the overlay compared against the raw" "character. (#58)- Plugin:
src/ui/which-key.ts(normalizeVimKey()function mirroring the fork’snormalizeKeyString,normalizedLeaderKeyfield for key event comparison, normalized lookups inshowLeaderBindings/showCompletions/onKeyPressLeaderOnly),src/main.ts(rebuildWhichKey()normalizes allcommandLabelsandgroupLabelsmap keys from settings, vimrc, and Lua sources)
- Plugin:
vim.opt.clipboardsilently ignored in init.lua — settingvim.opt.clipboard = "unnamed"or"unnamedplus"in init.lua had no effect. The Luavim.opthandler only routed options through theKNOWN_SET_OPTIONStable, butclipboardwas special-cased in the vimrc loader and missing from that table. Yanks never synced to the system clipboard when configured via Lua. Same issue affectedvim.opt.textwidth. (#56)- Plugin:
src/vimrc/loader.ts(SideEffectOpttype,clipboard/textwidth/guicursoradded toKNOWN_SET_OPTIONS),src/lua/api.ts(special-case blocks removed, unifiedKNOWN_SET_OPTIONSpath),src/main.ts(initial settings load from saved values),src/lua/loader.ts(setOptioncallback)
- Plugin:
- Settings-based clipboard and textwidth not restored on plugin restart — if a user set clipboard or textwidth via the Settings UI, the value was saved to disk but not re-applied to the vim engine on the next Obsidian startup. The saved
this.settings.clipboardandthis.settings.textwidthwere never pushed tovim.setOption()during plugin load. Fixed by applying saved side-effect options afterregisterVimOptions().- Plugin:
src/main.ts(initial settings loop usingKNOWN_SET_OPTIONSsideEffects)
- Plugin:
Changed
- Vim option architecture:
SideEffectOpttype — options that require side effects beyondthis.settings[key] = value(clipboard, textwidth, guicursor) are now declared in theKNOWN_SET_OPTIONStable with asideEffecttype and anapply()callback. Previously, these options were special-cased with separateifblocks in both the vimrc loader and the Luavim.opthandler — adding a new side-effect option required touching 2-3 files manually, and missing one path caused silent failures. Now there is a single declaration point. The vimrc loader, Lua handler, and initial settings load all route through the same table-driven path.- Plugin:
src/vimrc/loader.ts(SideEffectOptinterface,applyKnownSetOptionsideEffect handling, special-case blocks removed),src/lua/api.ts(unifiedKNOWN_SET_OPTIONSpath)
- Plugin:
Added
- Snippets — VS Code-compatible snippet expansion with tabstop navigation, linked mirrors, variable resolution (TM_FILENAME, $UUID, etc.), and context-aware filtering (prose/code/frontmatter). Ships 40+ Obsidian-adapted snippets (headings, callouts, wikilinks, tables, frontmatter, math, date/time). Three trigger mechanisms: CM6 completion menu, Tab expansion (vim-native), and ex commands (
:snippet name,:snippetspicker). Bundled snippets toggleable via settings. - User snippet directory — load custom VS Code JSON snippet files from a configurable directory. Supports vault-relative and absolute paths (with
~expansion, desktop only). User snippets override bundled snippets on prefix collision. - Snippet Lua DSL — LuaSnip-inspired
vim.snippet.*API:s(),t(),i(),c(),rep(),fmt()for static snippets that compile to VS Code JSON at load time.f()(function nodes),d()(dynamic nodes),sn()(snippet nodes),r()(restore nodes) for reactive snippets that execute Lua functions via fengari at edit time. - Snippet context filtering — snippets can be restricted to specific editing contexts (
"prose","code:js","code:*","frontmatter") via a"context"field in JSON or thecontextoption in Lua DSL. - Choice node cycling —
Ctrl+N/Ctrl+Pcycle through choice options (${1|a,b,c|}) on active snippet fields. - Snippet picker —
:snippetsopens the telescope-style fuzzy finder with snippet preview.:snippet nameexpands by name. @codemirror/autocompletefork — recursive descent snippet parser replacing CM6’s regex parser, supporting choice nodes, nested placeholders, transforms (parsed, not applied), and bare$1syntax. Fork at saberzero1/autocomplete.- 4 new settings: Enable snippets, Bundled snippets, Snippet directory, Trigger mode
- 4 new vimrc
setoptions:snippets,snippetbundled,snippetdir,snippettrigger - 13 new
vim.snippet.*Lua API functions - Plugin files:
src/snippets/(14 files),src/lua/snippet-api.ts,src/snippets/dynamic-bridge.ts - Fork files:
~/Repos/autocomplete/src/snippet.ts(parser),~/Repos/autocomplete/src/index.ts(exports)
Tests
- 9 unit tests in
test/unit/which-key.test.ts:normalizeVimKey()— space conversion, idempotence, angle-bracket preservation, mixed notation - 4 e2e regression tests in
test/specs/lua-space-leader.e2e.ts: which-key descriptions with space leader — string command desc, function callback desc,allmode desc,vim.obsidian.leader.setdesc - 11 new e2e tests in
test/specs/lua-config.e2e.ts:vim.opt.clipboard = "unnamed"applies from init.lua (1 test)yy/yw/ddpopulate+register when clipboard set via Lua (3 tests)vim.opt.textwidthandvim.opt.twalias from init.lua (2 tests)- Dual-config override precedence: Lua overrides vimrc for clipboard, textwidth, scrolloff (3 tests)
- Error resilience: unknown Lua option preserves vimrc clipboard; invalid textwidth (
-5) preserves vimrc value (2 tests)
- 7 e2e spec files (22 passing, 5 skipped) covering expansion, tabstops, variables, context, f()/d() nodes, static regression
- 15 LuaSnip golden comparison unit tests (extracted from LuaSnip test suite commit
0abc8f3) - 359 total unit tests passing
Documentation
docs/features/snippets.md(new feature page)docs/features/index.md,docs/reference/keybindings.md,docs/configuration/settings.md,docs/features/ex-commands.md,docs/configuration/vimrc.md,docs/configuration/lua-config.md(updated)AGENTS.md(fork dependency + page ownership)KNOWN_LIMITATIONS.md(7 limitation entries)
[0.52.0] - 2026-07-12
Added
- Cursor line highlight —
set cursorline/set nocursorlinewithcursorlineopt(number/line/both). Compartment-based runtime switching. - Fold column —
set foldcolumnshows ▸/▾ indicators for foldable regions with click-to-fold. Uses CM6foldable()/foldedRanges()APIs. numberwidthoption —set numberwidth=Ncontrols minimum line number column width (1–20, default: 2).- Mobile gutter width reduction — line number gutter uses smaller font and reduced width on viewports ≤ 600px.
- Configurable line number gutter —
set number,set relativenumber, or both for hybrid mode (absolute on current line, relative on others), matching Neovim’s semantics exactly. Uses a custom CM6gutter()withCompartment-based runtime switching —:set number/:set nonumbertake effect instantly without full feature reload. When the plugin’s line number gutter is active, Obsidian’s native line numbers are suppressed via CSS to prevent duplication. Defaults to off (matching Neovim defaults).- Settings: Settings → Vim Motions → Line numbers — two toggles: Line numbers, Relative line numbers
- Vimrc:
set number/set nonumber(aliasnu),set relativenumber/set norelativenumber(aliasrnu) - Lua:
vim.opt.number = true,vim.opt.relativenumber = true - Plugin:
src/vim/line-number-gutter.ts(new),src/settings.ts,src/vim/options.ts,src/vimrc/loader.ts,src/main.ts,styles.css
- Picker provider API — external plugins can register custom picker sources via
window.VimMotions.picker.registerSource(). The API validates namespaced source names (pluginId:sourceName), wraps external source methods in try/catch with 5-second timeout, caps results at 10,000 items, and emitssource-registered/source-unregisteredevents. Consumer plugins discover the API viawindow.VimMotions.pickerorapp.plugins.plugins['vim-motions'].pickerAPI.- Lifecycle:
vim-motions:picker-readyworkspace event fires after API installation; consumers useonLayoutReady+ event listener pattern for load-order safety. - Type definitions:
src/picker/picker-api.d.tsships standalone types for consumer plugins with full JSDoc and usage examples. - Plugin:
src/picker/api.ts(new),src/picker/picker-api.d.ts(new),src/picker/registry.ts(extended),src/picker/types.ts(metadata fields),src/main.ts(API wiring)
- Lifecycle:
- Meta-picker —
:Picker(no arguments) opens a source browser listing all registered picker sources grouped by “Built-in” / “Extensions”, with display names, icons, and keymap bindings shown. Selecting an entry opens that picker.:Picker <source>opens a named source directly.- Plugin:
src/picker/sources/pickers.ts(new),src/workspace/commands.ts(:Pickerex command),src/main.ts(picker-pickersObsidian command)
- Plugin:
- Picker source metadata —
PickerSourceinterface extended with optionaldisplayName,icon,description, andpriorityfields. All 12 built-in sources now include metadata for display in the meta-picker.- Plugin:
src/picker/types.ts,src/picker/sources/*.ts(all 12 source files)
- Plugin:
- Bundled picker integrations — three built-in picker sources that auto-detect and integrate with popular plugins:
- Omnisearch (
omnisearch) — dynamic full-text vault search viaglobalThis.omnisearch.search(). 150ms debounce, min 2-char query. Jumps to first match offset on selection. - Obsidian Tasks (
tasks) — shows all incomplete tasks sorted by due date, grouped by status type. Cached with event-based invalidation viaobsidian-tasks-plugin:cache-update. Jumps to task line on selection. - Dataview (
dataview) — lists all Dataview-indexed pages with tags and aliases in description. Filterable by the picker’s fuzzy matcher. - All three are gated by settings toggles in Settings → Vim Motions → Third-party integrations (default: enabled) and by runtime plugin detection via
onLayoutReady. - Plugin:
src/picker/sources/omnisearch.ts(new),src/picker/sources/tasks.ts(new),src/picker/sources/dataview.ts(new),src/main.ts(detection + registration),src/settings.ts(3 toggles)
- Omnisearch (
Fixed
- Obsidian line numbers leaking into table cell editors — when Obsidian’s “Show line numbers” setting is enabled, line number gutters appeared inside embedded table cell editors where they shouldn’t. Fixed by suppressing
.cm-guttersin cell editors via CSS. (#19)- Plugin:
styles.css
- Plugin:
Changed
enableMarkGuttermigrated tosigncolumn— the boolean toggle is now a dropdown with Auto/Always/Off modes matching Neovim’ssigncolumnoption. Existing settings are auto-migrated.set markgutter/set nomarkgutterremain as backward-compatible aliases.- Mark gutter internals refactored — the mark gutter implementation has been extracted into a dedicated
sign-column.tsmodule. The rendering approach is unchanged (line decorations + CSS::afteroverlay, zero layout shift), but the internal architecture now cleanly separates the sign column field from the refresh scheduling API. No user-facing changes.- Plugin:
src/vim/sign-column.ts(new),src/vim/mark-gutter.ts(refactored to delegate)
- Plugin:
Documentation
docs/configuration/settings.md: addednumberwidth,cursorline,cursorlineopt,signcolumn, andfoldcolumnsettingsdocs/configuration/vimrc.md: added new gutter options andmarkgutteralias to options tablesdocs/configuration/lua-config.md: added new gutter options tovim.opttabledocs/features/marks.md: updated to mentionsigncolumnas the canonical way to toggle mark indicatorsKNOWN_LIMITATIONS.md: added notes onsigncolumnoverlay behavior andcursorlineopt=screenlinesupportdocs/configuration/settings.md: added Line numbers settings groupdocs/configuration/vimrc.md: addednumber/nu,relativenumber/rnuto boolean options tabledocs/configuration/lua-config.md: addedvim.opt.number,vim.opt.relativenumberwith hybrid mode tipKNOWN_LIMITATIONS.md: removednumberandrelativenumberfrom “not implemented” options listdocs/development/picker-api.md: new provider API reference with consumer guide, API surface, integration examples (Omnisearch, Tasks), and lifecycle documentationdocs/development/index.md: added picker provider API linkdocs/reference/keybindings.md: added:Picker/:Pickto ex commands tableKNOWN_LIMITATIONS.md: added picker provider API pop-out window limitationdocs/configuration/settings.md: added Third-party integrations settings groupdocs/development/picker-api.md: added bundled integrations sectioneslint.config.mts: added.obsidian-cacheto global ignores (downloaded community plugin JS files)wdio.conf.mts: added Omnisearch, Tasks, Dataview asenabled: falseplugins for integration testingtest/specs/picker-integration.e2e.ts: 12 e2e tests covering source registration, picker opening, search results, meta-picker listing, and plugin disable/enable lifecycle
[0.51.1] - 2026-07-11
Fixed
- Yank highlight over-extending on headings — linewise yank (
yy) on a heading with text on the very next line highlighted both lines, even though only the heading line was yanked. The highlight range calculation usedstate.doc.lineAt(sel.to).toto find the end of the highlight, but the codemirror-vim fork’sexpandSelectionToLine()setssel.toto the start of the next line (viacurEnd.line++). CallinglineAt()on that position resolved to the entire next line, extending the highlight one line too far. Fixed by usingsel.todirectly as the highlight end boundary, which already points to the correct position (the start of the line after the last yanked line). (#53)- Plugin:
src/main.ts(attachYankHighlightlinewise range calculation)
- Plugin:
[0.51.0] - 2026-07-11
Added
- Input method switching for CJK users — automatic IM switching when entering/leaving insert mode. Supports macism (macOS), im-select (macOS/Windows), fcitx5-remote (Linux), ibus (Linux), and any external IM switching binary. Per-editor IM state tracking, 50ms debounced switching, composition guard (never switches mid-IME composition), and error throttling with auto-disable. Desktop only — graceful no-op on mobile. (#55)
- Lua API:
vim.obsidian.im.get(),.set(id),.save(),.restore(),.enabled,.auto— programmatic control for advanced use cases. Setvim.obsidian.im.auto = falseto disable auto-wiring and handle switching via Lua autocmds. - Settings: 7 new settings in Settings → Vim Motions → Input method — master toggle, binary path, obtain/switch args, normal mode IM, restore behavior (restore previous / use fixed default), default insert IM.
- Security: Uses
child_process.execFile(no shell interpretation). Binary path must be absolute. IM identifiers validated against shell metacharacters. Scoped process access following theexternal-fs.tspattern. - Plugin:
src/im/im-process.ts(new),src/im/im-switcher.ts(new),src/settings.ts,src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts,src/lua/autocmd.ts,src/main.ts,src/util/external-fs.ts - Tests:
test/unit/im-process.test.ts(new),test/unit/im-switcher.test.ts(new)
- Lua API:
CmdlineEnter/CmdlineLeaveautocmd events — fire when entering/leaving the:,/, or?command-line prompt. Event data includescmdtype(":","/", or"?").CmdlineLeaveis auto-wired to IM switching (switches to normal mode IM on prompt exit), matching im-select.nvim’s default behavior.- Plugin:
src/lua/autocmd.ts,src/lua/loader.ts,src/vim/mode-tracker.ts,src/im/im-switcher.ts,src/main.ts
- Plugin:
Documentation
docs/configuration/lua-config.md: addedvim.obsidian.imAPI section with function reference and Lua examplesdocs/configuration/settings.md: added “Input method” settings group tableKNOWN_LIMITATIONS.md: added IM switching limitations section (desktop-only, no command-line/search mode, Flatpak/Snap, system-wide switching); updated “Intentionally not supported” table to mark IM switching as built-in
[0.50.1] - 2026-07-11
Fixed
- Table widget duplication with third-party decoration plugins — the embedded/cursor-aware table widget could show duplicated table content when another plugin (e.g., aDHL — Another Dynamic Highlights) applied
Decoration.markranges over the same document region covered by the table’sDecoration.replaceblock widget. CM6’s decoration merging resolved the conflicting mark and replace decorations at default precedence, allowing raw table text to leak through alongside the rendered widget during re-entrant view updates. Fixed by wrapping thetableRenderFieldStateField inPrec.high(), ensuring the replace decoration takes precedence over default-priority mark decorations from other plugins. (#55)- Plugin:
src/vim/table-render-widget.ts(Prec.high()wrap ontableRenderField)
- Plugin:
[0.50.0] - 2026-07-11
Added
- Fold viewport scroll compensation — the viewport automatically scrolls to keep the cursor visible after any fold/unfold operation, including Obsidian’s “Toggle fold properties” command. Uses a
TransactionExtenderfor CM6 fold effects and aMutationObserverfor the properties widget’s CSS class toggle. (#54)- Plugin:
src/vim/fold-sync.ts(new),src/main.ts
- Plugin:
- Fold create/delete commands —
zf{motion}creates a manual fold over the motion range (works in both visual and operator-pending mode).zd/zDdelete the fold at the cursor.zEeliminates all folds in the document.- Ex commands:
:folddelete(zd),:foldeliminate(zE) - Plugin:
src/fold/commands.ts(new),src/operators/register.ts
- Ex commands:
- Incremental fold level —
zmfolds one more heading level (h1 first, then h2, etc.).zrunfolds one heading level. A customStateFieldtracks the current fold depth (0–6).- Ex commands:
:foldmore(zm),:foldless(zr) - Plugin:
src/fold/fold-level.ts(new),src/workspace/navigation.ts,src/main.ts
- Ex commands:
- Markdown fold provider — custom
foldServiceregisters frontmatter (---blocks) and callouts (> [!type]) as foldable regions. These are now foldable viazc/zo/zain addition to the standard CM6 heading and code block folds.- Plugin:
src/fold/provider.ts(new),src/main.ts
- Plugin:
- Fold placeholder text — folded regions show descriptive placeholder text: heading title + line count, code block language, callout type, or frontmatter field count. Uses
codeFolding({ preparePlaceholder, placeholderDOM }).- Plugin:
src/fold/placeholder.ts(new),src/main.ts
- Plugin:
- Fold-aware navigation — when enabled, navigating into a folded section (e.g.,
]hto a folded heading) automatically unfolds it. Matches Neovim’s defaultfoldopenbehavior. Configurable via Settings → Vim Motions → Fold-aware navigation (default: on).- Plugin:
src/vim/fold-sync.ts,src/settings.ts
- Plugin:
- Fold persistence — fold state is remembered across file switches and sessions. Folds are captured on leaf change and restored when re-opening a file. Capped at 500 files with 30-day TTL eviction. Cleans up on file rename/delete.
- Plugin:
src/fold/persistence.ts(new),src/main.ts,src/settings.ts
- Plugin:
Documentation
docs/features/workspace-navigation.md: added fold provider, placeholder, fold-aware navigation, and persistence documentationdocs/reference/keybindings.md: addedzf,zd,zD,zE,zm,zrkeybindingsdocs/features/ex-commands.md: added:folddelete,:foldeliminate,:foldmore,:foldlessdocs/configuration/settings.md: added Fold-aware navigation and Fold persistence settingsKNOWN_LIMITATIONS.md: addedzn/zNas known deviations; updated fold command coveragetest/neovim-command-index.yaml: added 6 new fold command entries
[0.49.0] - 2026-07-11
Added
- Which-key sort order setting — configurable sort order for the which-key popup. “which-key” (default) matches which-key.nvim defaults: individual keys first, groups last, alphanumeric before special keys, natural alphabetical tiebreaker. “Groups first” shows groups before individual keys, both sorted alphabetically. Configurable via Settings → Vim Motions → Which-key sort order,
vim.opt.whichkeysortin Lua, orset whichkeysort=<order>(aliaswks) in vimrc.- Plugin:
src/ui/which-key.ts(sortWhichKeyEntries,WhichKeySortOrdertype),src/ui/global-which-key.ts,src/settings.ts,src/main.ts,src/vim/options.ts,src/vimrc/loader.ts
- Plugin:
- Which-key Lucide icons — optional Lucide icon support for the which-key popup, inspired by which-key.nvim. Icons render as inline SVGs via Obsidian’s
setIcon()API, colored using Obsidian’s CSS color variables or arbitrary CSS color strings. Each row displays: key → separator (➤) → icon → description, matching which-key.nvim’s column layout.- Global toggle:
whichKeyIconssetting (default: on). Configurable via Settings UI,vim.opt.whichkeyiconsin Lua, orset whichkeyicons/set nowhichkeyiconsin vimrc. - Per-entry icons: assign icon and color to any group label or command label via Settings UI, Lua (
vim.obsidian.whichkey.set_group("<leader>t", "Table", { icon = "table", color = "blue" })), or vimrc (whichkeygroup <leader>t Table icon=table color=blue). - Color system: 8 named Obsidian colors (
red,orange,yellow,green,cyan,blue,purple,pink) mapped to theme CSS variables, plus arbitrary CSS color strings. Default icon color:--text-muted. - Default icons: Table (
table, blue), EasyMotion (zap, yellow), Harpoon (anchor, orange) — applied automatically to built-in groups. - Alignment: spacer spans for rows without icons when icons are globally enabled, ensuring consistent column alignment.
- Plugin:
src/ui/which-key.ts(WhichKeyLabelInfo,resolveIconColor),src/ui/global-which-key.ts,src/settings.ts(GroupLabel/CommandLabel extended withicon?/color?),src/main.ts,src/lua/api.ts,src/lua/obsidian-api.ts,src/lua/loader.ts,src/vim/options.ts,src/vimrc/loader.ts,src/vimrc/parser.ts,src/workspace/global-mapping-registry.ts,src/easymotion/register.ts,src/motions/tables.ts,styles.css
- Global toggle:
- Harpoon-style file pinning — pin files to numbered slots for instant switching.
<leader>hapins,<leader>1–<leader>9jumps to slots,<leader>hpopens the harpoon picker. Cursor position is tracked per-pinned-file and restored on navigation. Pins persist across sessions. File renames auto-update pins; file deletes auto-remove them.- 6 ex commands:
:HarpoonAdd,:HarpoonRemove [N],:Harpoon,:HarpoonSelect N,:HarpoonNext,:HarpoonPrev - 14 Obsidian commands for command palette access
- 15 leader keybindings with which-key “Harpoon” group
- Picker with slot-ordered display, fuzzy search, preview, and split-open support
- Plugin:
src/vim/harpoon-store.ts(new),src/vim/harpoon-nav.ts(new),src/picker/sources/harpoon.ts(new),src/main.ts,src/settings.ts
- 6 ex commands:
Documentation
docs/configuration/which-key.md: added Sort order and Icons sections with Lua/vimrc/Settings examples, color table, and default iconsdocs/configuration/settings.md: added Which-key sort order and Which-key icons rows to Which-key hints tabledocs/configuration/vimrc.md: addedwhichkeysort(wks) andwhichkeyicons(wki) optionsdocs/configuration/lua-config.md: addedwhichkeysortandwhichkeyiconsoptions tovim.opttableKNOWN_LIMITATIONS.md: added Sort order and Icons subsections to Which-key overlay sectiondocs/features/harpoon.md: new feature pagedocs/features/index.md: added harpoon to Jump navigation sectiondocs/configuration/settings.md: added Harpoon file pinning to Jump navigation tabledocs/reference/keybindings.md: added Harpoon section with leader bindings and ex commands
[0.48.0] - 2026-07-11
Added
- Mark gutter indicators — vim mark letters (
a–z,A–Z) appear in the gutter area next to marked lines, providing visual feedback on where marks are set. Multiple marks on the same line are shown together (e.g.,ab). The indicators use line decorations with CSS::afterpositioning — zero horizontal space consumed, no document shift. Updates on mark set/move/delete and on document edits. Toggle via Settings → Vim Motions → Vim features → Mark gutter indicators (default: on).- Plugin:
src/vim/mark-gutter.ts(new),src/main.ts,src/settings.ts,styles.css
- Plugin:
- Global mark persistence — marks
A–Zare persisted across files and plugin restarts. SettingmAstores the file path and cursor position; navigating to'Afrom any file opens the target and jumps to the saved position. Marks are saved via a 30-second polling interval with dirty-flag checking, plus immediate save on unload.- Plugin:
src/vim/mark-store.ts(new),src/main.ts,src/settings.ts(persistedMarksfield)
- Plugin:
- Enhanced marks picker —
:marksand<leader>fmnow show marks grouped by category: “Buffer marks” (a–z) with line preview, “Global marks” (A–Z) with file path. Selecting a global mark opens the target file and navigates to the saved position. Built on aMarkProviderabstraction for future extensibility (harpoon-style file marks).- Plugin:
src/picker/sources/mark-providers.ts(new:MarkProviderinterface,VimBufferMarkProvider,GlobalMarkProvider),src/picker/sources/marks.ts(rewritten),src/picker/types.ts(groupfield),src/picker/picker.ts(group header rendering)
- Plugin:
- Picker group headers —
PickerIteminterface extended with optionalgroupfield. When set, the picker renders non-selectable section headers when the group changes between consecutive items. Available to all picker sources.- Plugin:
src/picker/types.ts,src/picker/picker.ts,styles.css
- Plugin:
Fixed
:delmarksnot refreshing gutter — deleting marks via:delmarks adid not update the mark gutter indicators because the plugin’s ex command handler didn’t trigger a gutter refresh. Fixed by adding anonMarksChangedcallback tocreateDelmarksCommandthat schedules a gutter refresh.- Plugin:
src/workspace/commands.ts,src/main.ts
- Plugin:
- Yank highlight not working on first load — the yank highlight handler was not attached to the initially open editor on plugin startup, only activating after switching to a different pane.
reloadFeatures()now callsattachYankHighlight()(cleanup + re-attach) instead of only cleaning up, ensuring the handler is attached when vimrc/lua loading triggers the first feature reload. (#53)- Plugin:
src/main.ts(reloadFeaturescallsattachYankHighlight())
- Plugin:
Documentation
docs/features/marks.md: new feature page covering mark gutter indicators, global mark persistence, and grouped marks pickerdocs/features/index.md: added marks entry to Quality of life sectiondocs/configuration/settings.md: added Mark gutter indicators row to Vim features tabledocs/reference/keybindings.md: updated:marksdescription to reflect grouped pickerdocs/features/quality-of-life.md: added “Yank highlight” section with mode descriptions, configuration, CSS override tip, and fork-mode-only calloutdocs/features/index.md: added yank highlight to Quality of life bulletdocs/configuration/settings.md: added Yank highlight and Yank highlight duration rows to Vim features table; added CSS override tip calloutKNOWN_LIMITATIONS.md: updated “Yank highlighting” row from external plugin recommendation to built-in; added marks section
[0.47.0] - 2026-07-10
Added
- Yank highlight — yanked text is briefly highlighted, providing visual feedback on what was yanked. Three modes available in Settings → Vim Motions → Vim features → Yank highlight: “Solid” (default, Neovim-style — instant appear, hold, disappear), “Fade” (gradual fade-out animation), or “Off”. Duration is configurable via the Yank highlight duration slider (50–3000ms, default 200ms). Highlight color adapts to the active theme via
--text-accentand can be overridden with the--vim-motions-yank-bgCSS custom property. Respectsprefers-reduced-motion. Replaces the external obsidian-vim-yank-highlight plugin. (#53)- Works with remapped yank keys (detects actual yank operations via the
vim-yankevent, not keypress sniffing) - Handles rapid successive yanks (new highlight replaces previous), large yanks (>1000 lines skipped), and disposed views (tab close during highlight)
- Requires bundled fork mode (built-in vim mode OFF) — the built-in vim does not emit the
vim-yankevent - Blockwise yank highlight deferred to a future release
- Plugin:
src/vim/yank-highlight.ts(new),src/main.ts,src/settings.ts,styles.css
- Works with remapped yank keys (detects actual yank operations via the
- Embedded table editing mode — new
'embedded'option for Settings → Vim Motions → Table widget in Live Preview. Tables render as themed HTML with a two-layer editing model:- Table navigation:
h/j/k/lmoves a cell highlight across the rendered table.j/kat the top/bottom row exits the table. - Cell editing:
i/a/c/s/Enteropens a vim-enabled editor in the highlighted cell with full vim support (modes, motions, text objects, auto-formatting).Escapereturns to table navigation; a secondEscapeexits the table.Tab/Shift-Tabmoves between cells. - Table manipulation:
o/O(add row below/above),dd(delete row),dc(delete column),J/K(move row down/up),H/L(move column left/right),I/A(add column left/right),=(realign). These operate directly on the raw markdown — no dependency on Obsidian’s internal table commands. - Cell edits are written back per-cell with per-cell undo granularity.
- Configurable via
set tablewidget=embeddedin vimrc orvim.opt.tablewidget = "embedded"in Lua. - Plugin:
src/vim/table-nav-controller.ts(new),src/vim/table-cell-editor.ts(new),src/vim/table-operations.ts(new),src/vim/table-utils.ts(new),src/vim/table-render-widget.ts(modified: data attributes, embedded mode toggle, re-render guard),src/vim/table-embedded-editor.ts(rewritten),src/vim/table-auto-format.ts(getVimMode accepts EditorView),src/motions/tables.ts(exported helpers),src/main.ts,src/settings.ts,src/vim/options.ts,src/vimrc/loader.ts,styles.css
- Table navigation:
Changed
- Oil explorer architecture rewritten — oil no longer creates temporary
oil~*.mdfiles in the vault. The directory listing is rendered in a dedicatedoil-explorerview type with an embedded CodeMirror 6 editor, eliminating temp file visibility in tabs, search, and graph. Vim mode (both built-in and bundled fork) works natively in the embedded editor. View state (current directory) persists across workspace restarts viagetState()/setState().- New: reusable
EmbeddableMarkdownEditorabstraction (src/editors/embeddable-editor.ts) — extracts Obsidian’s internalScrollableMarkdownEditorprototype viaapp.embedRegistryand exposes a lightweight editor mountable in any DOM container with full CM6 + vim support. Designed for future use by the table editor and other features needing embedded markdown editing. - New:
OilViewcustom view (src/oil/oil-view.ts) — extendsViewwith view type'oil-explorer', embedded editor with oil conceal extension, directory state management, and previous-file tracking for workspace restoration on close. - Changed:
:q/:wq/:x/qin oil now restore the previously open file instead of leaving an empty workspace. - Changed: Oil keybinding registration uses
vim.mapwith<CR>notation instead ofvim.noremapwith literal newline, fixing command execution in the embedded editor. - Changed: Lua
vim.obsidian.oil.*callbacks call manager methods directly instead of routing through ex commands, fixing Lua API calls when no MarkdownView is active. - Removed:
OIL_TEMP_PREFIX,tempToDirmap,getTempFilePath(),forgetTempPath(),cleanupOrphanedTempFiles(),forceSourceMode(),userIgnoreFiltersmanagement, CSS.nav-file-title[data-path^='oil~']hiding rule. - Migration: Legacy
oil~*.mdfiles from previous versions are automatically cleaned up on first load (cleanupLegacyTempFiles()). - Plugin:
src/editors/embeddable-editor.ts(new),src/oil/oil-view.ts(new),src/oil/manager.ts(rewritten),src/oil/keybindings.ts(refactored),src/oil/render.ts(OIL_TEMP_PREFIX filter removed),src/workspace/commands.ts(OilView detection for:w/:wq/:x/:q),src/main.ts(registerView, Lua callbacks simplified),styles.css(OilView styling),test/specs/oil-poc.e2e.ts(rewritten for OilView, 16 tests)
- New: reusable
Fixed
- Oil confirm dialog button not focused — the confirmation dialog shown when deleting files now auto-focuses the Confirm button, matching pre-migration behavior.
[0.46.0] - 2026-07-10
Fixed
- Which-key overlay hidden behind status bar — the which-key popup (both editor-level and global workspace) was positioned at
bottom: 0of its container, causing the bottom rows to be obscured by Obsidian’s status bar. The overlay now detects the status bar height and addspadding-bottomto keep content above it. In split views, padding is only applied when the editor pane’s bottom edge is adjacent to the status bar (top splits are unaffected).- Plugin:
src/ui/which-key.ts(status bar height detection withgetBoundingClientRectadjacency check),src/ui/global-which-key.ts(same padding for global which-key)
- Plugin:
Added
- Remappable keybindings — every plugin keybinding is now user-remappable across all contexts (editor, oil explorer, picker, global workspace navigation). See the remapping guide for details.
- 46 new ex command aliases for editor-context actions: structural navigation (
:nextheading,:prevheading,:nextheading1–6,:prevheading1–6,:nextlistitem,:prevlistitem,:nextlink,:prevlink,:nextbuffer,:prevbuffer), table navigation (:tablenextcell,:tableprevcell,:tablenextrow,:tableprevrow), workspace navigation (:focuspaneleft/right/up/down,:splitvertical,:splithorizontal,:closetab,:closeothertabs,:nexttab,:prevtab,:gototab,:gotodefinition,:foldclose/open/toggle/all,:unfoldall,:documentoutline,:openurl,:docstats,:renamenote,:showbacklinks,:opengotofile,:contextactions,:charinfo), and hint mode (:hintactivate,:hintopennew,:hintyank,:hintclose). Users can remap any keybinding vianmap key :excommand<CR>in vimrc orvim.keymap.set('n', 'key', ':excommand<CR>')in Lua. - Plugin:
src/keybindings/action-registry.ts(new:exCommandFromMotion/exCommandFromActionhelpers),src/motions/register.ts,src/workspace/navigation.ts,src/main.ts
- 46 new ex command aliases for editor-context actions: structural navigation (
- Oil explorer remappable keybindings — all 9 oil keybindings are now user-remappable via Lua autocmds or vimrc
- 9 oil ex commands:
:oilopen,:oilparent,:oilroot,:oilrefresh,:oilclose,:oiltogglehidden,:oilcyclesort,:oilyankpath,:oilreveal - 8 new Lua functions in
vim.obsidian.oil:parent(),root(),refresh(),toggle_hidden(),cycle_sort(),yank_path(),reveal(),open_entry() OilEnter/OilLeaveautocmd events — fire when entering/leaving an oil buffer, enabling Neovim-style buffer-local keymaps- Oil defaults now registered as
vim.noremapmappings pointing to ex commands (previouslymapCommand), making them visible in:mapoutput and overridable by user mappings - Plugin:
src/oil/keybindings.ts(refactored),src/lua/api.ts,src/lua/obsidian-api.ts,src/lua/loader.ts,src/main.ts
- 9 oil ex commands:
- Picker keybinding configurability — picker modal keybindings (
<C-n>,<C-p>,<C-x>,<C-v>,<C-t>,<C-d>,<C-u>) are now configurable via Luavim.obsidian.pick_keymap()accepts a table of action→key arrays with snake_case field names- Custom keymap persisted in settings and applied to all picker instances including tag sub-pickers
- Plugin:
src/picker/types.ts(PickerKeymap,matchesPickerKey),src/picker/picker.ts(refactored keydown handler),src/picker/sources/tags.ts,src/settings.ts,src/lua/api.ts,src/lua/obsidian-api.ts,src/lua/loader.ts,src/main.ts
- Global workspace navigation remappable keybindings — non-editor keybindings (
<C-w>*,gt/gT,j/kscroll,H/L,:) are now remappablevim.obsidian.keymap.set/delnow operate on the liveGlobalMappingRegistryat runtime (previously only at config-load time):gmap key :command— new ex command to add global keybindings from the editor command line or non-editor:modal:gunmap key— new ex command to remove global keybindings:gmaps— renamed from:gmap(display-only) to avoid collision with the new mapping command- All 26 default global mappings tagged with stable
namefields for documentation - Plugin:
src/workspace/global-mapping-registry.ts,src/workspace/global-defaults.ts,src/ui/global-ex-command.ts,src/workspace/commands.ts,src/lua/loader.ts,src/main.ts
- Remapping guide — new documentation page
docs/configuration/remapping.mdwith examples for all 4 remapping contexts (editor, oil, picker, global)
Changed
:gmap(display) renamed to:gmaps— the:gmapcommand now creates global keybindings instead of displaying them. Use:gmapsto list all active global mappings.- Autocmd events — 15 → 17 supported events (added
OilEnter,OilLeave)
Documentation
docs/configuration/remapping.md: new unified remapping guidedocs/features/oil-explorer.md: added remapping section with ex commands table, Lua examples, vimrc examples, andvim.obsidian.oilfunction referencedocs/features/ex-commands.md: added navigation/action/oil/hint/global mapping ex command tables; updated command count to 100+docs/reference/keybindings.md: added ex command column to oil table; added remapping section linkdocs/configuration/lua-config.md: addedvim.obsidian.oilnamespace (10 functions),OilEnter/OilLeaveautocmd events,vim.obsidian.pick_keymap()APIdocs/configuration/vimrc.md: updated:gmap/:gunmap/:gmapsdocumentationdocs/configuration/index.md: added remapping guide to quick linksKNOWN_LIMITATIONS.md: updated keybinding remappability section to “Implemented” across all contexts; updated autocmd event count to 16AGENTS.md: updated change-to-page routing table withconfiguration/remapping.md
[0.45.0] - 2026-07-09
Fixed
gk/gjtakes extra keypress to traverse non-wrapped headings —gkrequired two presses to cross a heading line that was visually tall (large font/line-height) but did not wrap. CM6’smoveVerticallysaw the heading’s line block as spanning multipledefaultLineHeightsteps, causing a spurious within-line cursor move before crossing to the adjacent line. The fork’sfindPosVnow detects whenmoveVerticallystays on the same document line with negligible Y-coordinate change (less than halfdefaultLineHeightviacoordsAtPoscomparison) and force-moves to the adjacent document line. Legitimate wrapped-line navigation (Y delta ≥ threshold) is unaffected. (#26)- Fork:
src/cm_adapter.ts(findPosVY-delta spurious move detection)
- Fork:
Added
- Oil explorer — oil.nvim-inspired file explorer that renders vault directories as editable buffers. Create, rename, delete, and move files with standard vim commands, then commit all changes with
:w. (oil.nvim-inspired):Oil [path]opens the current file’s directory (or a specified path) as an editable buffer in a new tab. Each line represents a file or folder with a concealed entry ID. The buffer is a regular markdown file — all existing vim features (EasyMotion, surround, text objects, which-key, status bar) work natively.- File operations via vim commands:
o(new line) +:wcreates a file,dd+:wdeletes,cw+:wrenames. Filenames without an extension default to.md. Names ending with/create folders. Renames update backlinks viaapp.fileManager.renameFile(). Deletes respect user trash settings viaapp.fileManager.trashFile(). - Cross-directory moves:
ddin one oil buffer,pin another,:wmoves the file. The diff engine detects moves by matching entry IDs across buffers. - Navigation keybindings (active only in oil buffers):
<CR>open/enter,-parent directory,~vault root,qclose,<C-l>refresh,g.toggle hidden files,gscycle sort order,y.yank file path - Auto-refresh: vault event listeners (create/delete/rename) with 200ms debounce refresh open oil buffers when files change externally
- Confirmation dialog: shown when deleting files exceeding the configurable threshold (default: 1)
- Stale file cleanup: orphaned temp files from previous sessions are removed on plugin startup
- Tab title: reflects current directory (e.g.,
oil~notes) and updates on navigation :w/:wq/:x/:updatedispatch: active file path is checked for theoil~prefix — oil commits route through the diff/validate/execute pipeline; normal files save normally- Global ex command:
:Oilavailable in the non-editor:command modal (same pattern as picker commands) - Setting gate:
:Oilcommand and keybindings only registered when theoilExplorersetting is enabled (default: on) - Plugin:
src/oil/(manager.ts, cache.ts, diff.ts, actions.ts, render.ts, parser.ts, extensions.ts, keybindings.ts, types.ts),src/workspace/commands.ts(:wdispatch,:Oilregistration),src/main.ts(OilManager/OilKeybindingManager lifecycle),src/settings.ts(4 settings),src/ui/global-ex-command.ts(:Oilin global modal),src/workspace/global-defaults.ts(oil manager threading),styles.css(file explorer hiding)
- Oil explorer Lua API —
vim.obsidian.oil.open(path)opens oil for a directory,vim.obsidian.oil.close()closes the active oil buffer and cleans up the temp file- Plugin:
src/lua/api.ts(oilOpen/oilClosecallbacks),src/lua/obsidian-api.ts(vim.obsidian.oilsub-table),src/lua/loader.ts(callback wiring)
- Plugin:
- Oil explorer settings — 4 new settings in Settings → Vim Motions → File explorer:
oilExplorer(toggle, default: on) — enable/disable the oil exploreroilShowHiddenFiles(toggle, default: off) — show dotfiles in oil viewsoilConfirmDeleteThreshold(slider, 1–20, default: 1) — confirmation dialog thresholdoilDefaultSort(dropdown: name/mtime/size, default: name) — directory sort order- Plugin:
src/settings.ts
- Oil explorer e2e test suite — 10 regression tests covering:
:Oilopens temp file, regular markdown view, vault file listing with concealment, current-directory default, file creation, folder creation, file deletion, file rename, no-op save, and temp file exclusion from listings- Plugin:
test/specs/oil-poc.e2e.ts
- Plugin:
wdio.conf.mtsworkspace cleanup —onPreparehook deletes staleworkspace.jsonbefore e2e tests to prevent flaky failures from leftover workspace state- Spike test for reporter’s exact content (
spike-gk-issue26-repro.e2e.ts, 6 tests: full-document gk/gj traversal, consecutive h2 headings, long wrapped line, h2-longline-h2 transitions)
Changed
- Oil temp file hiding — oil temp files (
oil~*.md) are hidden from the file explorer via a static CSS prefix selector ([data-path^="oil~"]) instyles.css, and from search/graph/quick switcher via Obsidian’suserIgnoreFiltersmechanism. User-configured ignore filters are preserved — oil only adds/removes its own entries.
Documentation
docs/features/oil-explorer.md: new feature page covering overview, opening commands, file operations, navigation, configuration, and implementation detailsdocs/features/index.md: added oil explorer entry to workspace & commands sectiondocs/reference/keybindings.md: added Oil explorer keybinding table (13 entries)docs/configuration/settings.md: added File explorer settings group (4 settings)KNOWN_LIMITATIONS.md: added oil explorer section with cross-directory move requirements, temp file mechanism, and dotfile limitationKNOWN_LIMITATIONS.md: updated “Visual line navigation” section with three-correction architecture (multi-line clamp, tall non-wrapped line detection, column 0 fallback); updated test coverage note- Fork
DIFFERENCES.md: updated “Widget-aware vertical navigation” section with Y-delta spurious move detection
[0.44.1] - 2026-07-09
Removed
- nucleo-matcher-wasm dependency removed — the WASM-based fuzzy matcher from the Helix editor has been removed. The
nucleoandautopicker engine options are no longer available. The bundled wasm-bindgen glue code contained afetch()call (in the unused async init path) that triggered the Obsidian community directory scanner’s network request warning, along with other WASM-related scanner flags. Since uFuzzy performs comparably and nucleo was disabled by default, the dependency has been dropped entirely to eliminate scanner warnings and reduce bundle size.- Plugin:
src/picker/matcher-nucleo.ts(deleted),src/picker/matcher.ts(nucleo branch andauto/nucleoengine options removed),src/settings.ts(pickerMatcherEnginetype narrowed to'ufuzzy' | 'obsidian', dropdown options reduced),esbuild.config.mjs(WASM binary loader plugin removed),package.json(nucleo-matcher-wasmdependency removed) - Tests:
test/unit/picker/matcher.test.ts(nucleo engine removed from test matrix, nucleo-specific test suite removed),test/bench/matcher.bench.ts(nucleo benchmarks removed),test/specs/picker.e2e.ts(nucleo removed from engine switching test) - Docs:
KNOWN_LIMITATIONS.md(nucleo entries removed from engine list, limitations, and bundle size section),docs/configuration/settings.md(engine options updated),docs/index.md(0.44.0 summary updated),ACKNOWLEDGEMENTS.md(nucleo attribution removed),AGENTS.md(nucleo-matcher-wasm fork section removed)
- Plugin:
Changed
- Picker matching engine — setting reduced from four options (
ufuzzy,nucleo,obsidian,auto) to two (ufuzzy,obsidian). Default remainsufuzzy. - Bundle size — production bundle reduced by ~193KB (embedded WASM binary) plus ~30KB of wasm-bindgen glue code.
[0.44.0] - 2026-07-09
Changed
- Picker modal Telescope-style presentation — the unified fuzzy picker now uses a terminal-inspired visual style matching the which-key overlay aesthetic. All text elements use
var(--font-monospace)at compact sizes (11–13px). Items are denser (3px vertical padding, no minimum height). The selected item uses an accent-tinted background (hsla(var(--interactive-accent-hsl), 0.15)) instead of the generic hover color. The modal itself has minimal border-radius (2px), a subtle box-shadow, and an accent-colored border on the input and results panels. The result count bar usesvar(--text-faint)at 11px with a border separator. Preview pane font sizes are unified at 12px. All colors use Obsidian CSS variables for full theme compatibility. (telescope.nvim-inspired)- Plugin:
styles.css(picker CSS section rewritten)
- Plugin:
- Picker floating border titles — each picker section (prompt, results, preview) now displays a centered title label that overlays the top border, matching telescope.nvim’s
─── Files ───presentation. The prompt shows the source name (e.g. “Files”, “Buffers”, “Commands”, “Livegrep”), the results list shows “Results”, and the preview pane shows “Preview”. Titles use monospace font at 11px withvar(--text-muted)color and avar(--modal-background)background to mask the border behind them.- Plugin:
src/picker/picker.ts(formatTitlehelper,.vim-motions-picker-sectionwrapper divs with.vim-motions-picker-titlespans for input, results, and preview sections),styles.css(.vim-motions-picker-section,.vim-motions-picker-titlerules, updated flex layout for preview body wrappers)
- Plugin:
- Picker positional previews use raw text — positional previews (grep, live grep, headings, marks) now render as monospace plain text instead of rendered markdown. This ensures uniform line heights so the line-number gutter stays perfectly aligned with the content —
MarkdownRenderer.render()produces variable-height elements (headings, block elements) that caused the gutter and content to drift apart. Non-positional previews (full file preview without line numbers) continue to use markdown rendering.- Plugin:
src/picker/picker.ts(renderMarkdownPreviewpositional branch rewritten to emit<pre>with per-line<div>elements),styles.css(.vim-motions-picker-preview-code,.vim-motions-picker-preview-code-linerules)
- Plugin:
Fixed
- Neovim golden recorder produced incorrect results for visual-block operations — the
NeovimClient.input()method usednvim_feedkeyswith'tx'flags, which does not fully execute block-insert replication (where<C-v>I/A+ text +<Esc>applies the inserted text to all selected lines) or visual mode-switch + operator combos within a single RPC call. Block insert operations only appeared on the last selected line, and<C-v>→v/Vmode switches produced incorrect deletion scopes. Fixed by using:execute "normal ..."(vianvim.command()) for key sequences containing<C-v>, which processes synchronously within Neovim’s command loop. Non-block sequences still usenvim_feedkeys(needed for macro recording/replay which:normaldoesn’t support). AddedescapeForNormal()helper to convert control characters to Vim\<...>notation.- Plugin:
test/neovim/client.ts(hybridinput()method,escapeForNormalfunction) - Golden data:
upstream-gaps.json(4 cases corrected),visual-block.json(15 cases corrected — block I/A/c/C/x/~ now correctly affect all selected lines),select-mode.jsonandselect-mode-extended.json(minor corrections from improved key processing)
- Plugin:
- Picker preview gutter misaligned on files with frontmatter — positional previews (grep, live grep, headings, marks) showed line numbers for YAML frontmatter lines, but
MarkdownRenderer.render()silently strips frontmatter from the output. This caused the rendered text to shift up relative to the gutter by the number of frontmatter lines. Fixed by detecting----delimited frontmatter inreadLinesAroundPositionand clamping the preview slice to start after the frontmatter block, so both the gutter and content exclude frontmatter lines.- Plugin:
src/picker/sources/preview-utils.ts(getFrontmatterEndhelper,effectiveStartclamping inreadLinesAroundPosition)
- Plugin:
Added
- Picker matching engine setting — selectable fuzzy matching engine for the picker (Settings → Vim Motions → Picker matching engine). Four options:
ufuzzy(default),nucleo,obsidian,auto. The setting takes effect immediately on the next picker invocation without restarting Obsidian.- uFuzzy (default): Pure JavaScript matcher with filename-aware ranking — prefers exact filename prefix matches over partial path matches (e.g.,
Header.tsxranks aboveheader/utils.tsfor query"Header"). Fastest engine in benchmarks across all query types. Supports typo tolerance. - nucleo (opt-in): WASM-compiled matcher from the Helix editor (~193KB binary). Provides fzf-compatible scoring with optimal Smith-Waterman alignment and path-aware matching. Fork at saberzero1/nucleo-matcher-wasm adds
matchLiteralIndexedWithIndicesandmatchPatternIndexedWithIndicesmethods for efficient WASM boundary crossing. - obsidian (opt-in): Obsidian’s built-in
prepareFuzzySearchAPI. Zero bundle cost. May be slower on large vaults. - auto: nucleo on desktop, uFuzzy on mobile. Falls back to uFuzzy if WASM initialization fails.
- Plugin:
src/picker/matcher.ts(factory),src/picker/matcher-ufuzzy.ts(enhanced sort),src/picker/matcher-nucleo.ts(WASM adapter),src/picker/matcher-obsidian.ts(Obsidian API adapter),src/picker/matcher-utils.ts(shared utilities),src/settings.ts(pickerMatcherEnginesetting),esbuild.config.mjs(WASM binary loader plugin)
- uFuzzy (default): Pure JavaScript matcher with filename-aware ranking — prefers exact filename prefix matches over partial path matches (e.g.,
- Enhanced uFuzzy file-picker sort — the uFuzzy matcher now uses a filename-aware ranking algorithm instead of the default sort. The sort prefers: (1) exact filename prefix matches, (2) shorter basenames among prefix matches, (3) filename matches over path-only matches, (4) more exact term boundaries, (5) tighter fuzzy matches, (6) shorter paths. The info phase is capped at 500 items with filename-prefix candidates prioritized, keeping sort overhead bounded for broad queries. Benchmarks show this produces the same #1 result as nucleo’s Smith-Waterman scoring for 7 out of 8 test queries at ~25% overhead vs the default sort.
- Plugin:
src/picker/matcher-ufuzzy.ts(filePickerSortfunction, 3-phasefilter()→info()→ custom sort pipeline)
- Plugin:
- Matcher benchmark suite —
npm run test:benchruns a vitest benchmark comparing all three matching engines (uFuzzy, nucleo, obsidian) across 8 query patterns at 1K/5K/10K item counts. Uses realistic file path data (16 directories × 50 filenames × 6 extensions).- Plugin:
test/bench/matcher.bench.ts,vitest.config.ts(benchmark configuration),package.json(test:benchscript)
- Plugin:
- Picker engine switching e2e test — validates that all three engines (ufuzzy, nucleo, obsidian) can be switched at runtime via settings and produce results in the picker.
- Plugin:
test/specs/picker.e2e.ts(matcher engine switching section)
- Plugin:
- Matcher unit tests expanded — parameterized test suite runs 18 shared test cases across all three engines (54 tests total). Nucleo-specific tests cover fzf syntax chars as literals, emoji UTF-32/UTF-16 index correction, CJK characters, and 10K-item performance. Matcher-utils tests cover
indicesToRangesandutf32ToUtf16Indices.- Plugin:
test/unit/picker/matcher.test.ts(68 tests, up from 18)
- Plugin:
- 3 Neovim golden comparison cases for
gkcolumn preservation across headings (gk over heading preserves column,gk over heading then above preserves column,gk gj round-trip preserves column), recorded against Neovim 0.12.2 - 2 spike test suites:
spike-gk-font-variations.e2e.ts(11 tests: CSS theme stress-testing with varying font sizes, line heights, heading sizes, editor widths, padding/margins),spike-gk-column-drift.e2e.ts(4 tests: column drift measurement per heading level with Neovim comparison data)
Documentation
KNOWN_LIMITATIONS.md: updated picker section with four-engine description, filename-aware ranking, bundle size impact; added “gk/gjcolumn drift on heading lines” section documenting the pixel-vs-character column deviation from Neovim with measurement data table; addedgj/gkcolumn row to behavioral deviations table with “Pixel drift” status; updated golden test coverage note (7 → 10 heading tests, 3 golden comparison cases)docs/configuration/settings.md: added picker matching engine setting row with four options and notes sectionAGENTS.md: added nucleo-matcher-wasm fork section (dependency URL, build instructions, WASM binary size, fork API additions, license)ACKNOWLEDGEMENTS.md: added third-party attribution for nucleo-matcher-wasm (MPL-2.0), codemirror-vim (MIT), fengari (MIT)test/neovim/deviations.ts: registered 2 known deviations forgkcolumn preservation across heading lines (pixel-basedposAtCoordsvs Neovim’s character-basedcurswant)- Fork
DIFFERENCES.md: updated “Widget-aware vertical navigation” section with clamp-all-jumps approach andposAtCoordscolumn fixup relaxation
[0.43.0] - 2026-07-08
Fixed
- Cursor snapping over double-character formatting marks in Live Preview — moving through
**bold**,__underline__,~~strikethrough~~, or==highlight==withh/lskipped positions inside the**/__/~~/==delimiters instead of visiting each character. The cursor would jump from the first delimiter character to the content, skipping the second delimiter character. Investigation found that theEditorState.transactionFilterintroduced to correct cursor positioning near formatting marks was the sole cause of the snapping — Obsidian’s Live Preview natively handles mark visibility based on cursor proximity, and all formatting marks are full-width DOM elements on the active line. The transaction filter, theformattingMarkModesetting, and theformattingmarkmodevim option have been removed. (#33)- Plugin: removed
src/vim/formatting-mark-fix.ts,src/vim/formatting-mark-ranges.ts; removedformattingMarkModefrom settings interface, defaults, settings UI, Style Settings definition, vimrc loader, and vim options
- Plugin: removed
gk/gjstill skips lines in documents with mixed headings and lists —gkcould jump over multiple document lines when navigating upward through a document containing headings of varying sizes (###,####) separated by empty lines. The previous fix (v0.18.0) only clamped multi-line jumps when a replaced widget decoration (dec.point) was present in the skipped range, so headings — which use mark decorations with larger fonts, not replaced widgets — still triggered overshooting from CM6’s pixel-basedmoveVertically. The fork’sfindPosVnow clamps all multi-document-line jumps to ±1 when no fold is present, regardless of decoration type.posAtCoordsresolves horizontal position on the clamped target line; thegoalColumn > 0guard is relaxed togoalColumn != nullso the column fixup also fires at column 0. (#26)- Fork:
src/cm_adapter.ts(findPosVline-jump clamp,posAtCoordsresolution on clamped target)
- Fork:
Added
- 6 regression tests for cursor movement through double-character formatting marks (
**,__,~~,==) — asserts every position is visited in bothlandhdirections - E2E tests for
gkover h4/h5/h6 headings: cursor horizontal position preserved across all heading levels - E2E test for
gkthrough mixed headings, text, and lists: verifies no document lines are skipped and horizontal position is preserved on non-empty lines
Documentation
KNOWN_LIMITATIONS.md: updated “Visual line navigation and replaced widget decorations” section with clamp-all-jumps approach; updatedgj/gkwidgets behavioral deviation entry; updated test coverage count (3 → 7 heading tests)
[0.42.0] - 2026-07-08
Added
- Mobile opt-in setting and toggle command — the plugin is now disabled by default on mobile devices. A new
enableOnMobilesetting (default: off) controls whether the plugin activates on mobile. When disabled, the plugin skips all Vim engine initialization — no editor extensions, event listeners, commands, or status bar elements are registered — leaving Obsidian’s editor in its default state. The settings tab and a toggle command (Vim Motions: Toggle enable on mobile) remain accessible even when the plugin is disabled, so users can re-enable without needing a desktop device. Changing the setting requires an Obsidian reload. Hardware keyboard users on tablets can opt in; soft-keyboard-only users are no longer stuck in Normal mode with no way to escape. (#52)- Plugin:
src/settings.ts(enableOnMobileinVimMotionsSettingsinterface,DEFAULT_SETTINGS,getSettingDefinitions()Mobile group,display()Mobile toggle),src/main.ts(early return inonload()whenPlatform.isMobile && !enableOnMobile,toggle-enable-on-mobilecommand registered before the gate)
- Plugin:
showConfigNotificationssetting — a new toggle in Settings → Vim Motions → Vimrc & key bindings → Show config load notifications (default: on) controls whether the plugin shows Obsidian Notice popups when vimrc or init.lua files are loaded on startup. When disabled, success and informational notifications (“loaded N commands from …”, “loaded but contained no commands”, “no config files found”) are suppressed. Error notifications (lua syntax/runtime errors) and single-mode “not found” warnings (e.g. configMode isluabut no init.lua exists) always show regardless of this setting.- Plugin:
src/settings.ts(showConfigNotificationsinVimMotionsSettingsinterface,DEFAULT_SETTINGS, toggle in Vimrc & key bindings group),src/main.ts(notification gating in vimrc loading, lua loading, and dual-mode fallback)
- Plugin:
Changed
- Config load notifications scoped and improved — startup notifications for vimrc and init.lua loading are now better scoped. “Not found” messages only appear when the specific config type is the sole configured mode (e.g. configMode is
vimrcbut no vimrc exists) and now include the searched path. In dual-mode (lua-vimrc), “no config files found” lists both searched paths. Success and empty-file notifications respect the newshowConfigNotificationssetting. Error notifications (lua parse/runtime errors) always show.- Plugin:
src/main.ts(vimrc notification block, lua notification block, dual-mode fallback notification)
- Plugin:
- Picker preview pane renders markdown — full-file picker preview windows now render file content through Obsidian’s
MarkdownRenderer.render()instead of displaying raw markdown text in<pre><code>blocks. Headings, bold, italic, code blocks, images, links, callouts, and other markdown formatting are fully rendered. Links inside the preview are non-interactive (click-through disabled viapointer-events: none). Positional previews (grep, live grep, headings, marks) use a line-number gutter that highlights the target line.Componentlifecycle is managed per preview update (load()on render,unload()on preview change and modal close) to prevent memory leaks. Plain-string previews (commands, registers) remain unchanged. The picker modal now uses a fixed height (50vh) to prevent layout shifts when switching between files, and the result count element reserves its line height when empty.- Plugin:
src/picker/picker.ts(renderMarkdownPreviewmethod,Componentlifecycle,PreviewResultdispatch),src/picker/types.ts(PreviewResultinterface,PreviewReturnunion type),src/picker/sources/preview-utils.ts(returnsPreviewResultwithsourcePathand optionallineRange),styles.css(rendered preview content, positional gutter, fixed modal height)
- Plugin:
Fixed
- Cursor-aware table widget does not render inline markdown — images, bold, italic, math, links, and other inline formatting inside table cells were displayed as plain text when the cursor-aware table widget was active. The
TableRenderWidgetusedtextContentto populate cells, which strips all markup. Replaced withMarkdownRenderer.render()to process cell content through Obsidian’s markdown pipeline. Plain text is shown instantly as a fallback while the async render completes. The<p>wrapper added byMarkdownRendereris unwrapped to avoid block-level spacing in cells.Componentlifecycle is managed per widget (load()intoDOM,unload()indestroy) to prevent memory leaks.editorInfoFieldprovidesappandsourcePathfrom the editor state for correct relative image path resolution. (#50)- Plugin:
src/vim/table-render-widget.ts(renderCellfunction,MarkdownRenderer.render()integration,Componentlifecycle,editorInfoFieldfor app/sourcePath access)
- Plugin:
:obcommandunavailable in Lua-only config mode —vim.cmd('obcommand ...')failed with “Not an editor command” whenconfigModewas set tolua(without vimrc). Theobcommandex command was only registered insideregisterVimrcExCommands(), which only runs when vimrc loading is enabled. Movedobcommandregistration toregisterObCommand()alongsideob, sharing the same handler. Both commands are now available in all config modes (lua, vimrc, lua-vimrc, settings-only). Additionally,:obcommandwith no arguments now opens the command picker (matching:obbehavior) instead of silently doing nothing.- Plugin:
src/workspace/commands.ts(registerObCommandregisters bothobandobcommand),src/vimrc/loader.ts(removed duplicateobcommandregistration and unusedexecuteCommandByIdhelper)
- Plugin:
Documentation
docs/configuration/settings.md: added Mobile section withenableOnMobilesetting; addedshowConfigNotificationstoggle to Vimrc & key bindings tabledocs/getting-started/installation.md: added Mobile section with enable instructionsKNOWN_LIMITATIONS.md: updated Mobile support section with opt-in setting, toggle command, and revised platform feature table; added config load notification scoping section under Config file resolution- 7 new e2e tests —
config-notifications.e2e.tscovering: lua loaded notification shown/suppressed, lua error notification always shown even when suppressed, lua empty-file notification shown/suppressed, notification includes config file path, setting default verification - Shared test helpers —
setPluginSetting,getNotices,getVimMotionsNotices,dismissNoticesadded totest/helpers.ts
[0.41.0] - 2026-07-08
Added
- External config file paths (desktop only) — custom vimrc and init.lua paths now accept absolute filesystem paths (e.g.
~/.config/obsidian/init.lua,C:\Users\<you>\.config\obsidian\vimrc), enabling shared config across multiple vaults. Paths starting with/,~, or a drive letter are read directly from the filesystem viawindow.requireinstead of the vault adapter. Tilde (~) is expanded to the user’s home directory. Mobile gracefully falls back to vault-only paths. (#51)- Plugin:
src/util/external-fs.ts(new module:isAbsolutePath,readExternalFile,externalFileExists,expandTilde,getObsidianUserDataDir),src/lua/loader.ts(fileExists/readLuaFileexternal path fallback),src/vimrc/loader.ts(fileExists/readVimrcFileexternal path fallback),src/settings.ts(updated descriptions)
- Plugin:
- Unified picker / fuzzy finder — telescope.nvim-inspired fuzzy picker with 11 sources, preview pane, live grep, frecency scoring, and split-open support
- 10 built-in sources: files (
:files), buffers (:buffers), commands (:commands), headings (:headings), outline (:outline), backlinks (:backlinks), tags (:tags), recent files (:recent), marks (:marks), registers (:registers) - Live grep (
:livegrep): real-time vault content search with 200ms debounce, generation-based cancellation, and minimum 2-character query - Preview pane: side-by-side file content preview with per-source content (file content, surrounding lines for headings/grep/marks, command info, register content), responsive collapse on narrow screens (<600px),
<C-d>/<C-u>preview scrolling - Frecency scoring: recently/frequently accessed items rank higher. Time-bucket weights (1h–30d), 1000-entry cap, persists across restarts via plugin data. Applies to files, buffers, commands, headings, backlinks, grep, recent.
- Picker resume:
:resume/<leader>fp/vim.obsidian.pick('resume')reopens the last picker with the same query and selection - Split-open:
<C-x>(horizontal split),<C-v>(vertical split),<C-t>(new tab) from any file-based picker - Leader mappings: 11
<leader>f*bindings with which-key “Find” group (opt-out viapickerLeaderMappingssetting, default: on) - Keyboard navigation:
<C-n>/<C-p>,<C-j>/<C-k>, arrows,<Enter>,<Escape>,<C-c> - Matching engine: uFuzzy (7.5KB, unicode support) with match highlighting
- Fallback setting:
pickerboolean (default: true) — when disabled, migrated commands (:buffers,:marks,:registers,:grep,:backlinks,:ob) fall back to previous VimInfoModal/SuggestModal behavior - Lua API:
vim.obsidian.pick(source, opts?)— invoke any picker source from Lua - Obsidian command palette: 12 picker commands registered via
addCommandfor discoverability - Global ex command support: all picker commands available in non-editor views via
:global ex command modal - 200-item render cap with
requestAnimationFrame-free synchronous rendering for flicker-free updates - Plugin:
src/picker/(picker.ts, matcher.ts, registry.ts, frecency.ts, types.ts, sources/*.ts),src/picker/sources/(files, buffers, commands, grep, live-grep, headings, backlinks, tags, recent, marks, registers, split-open, preview-utils)
- 10 built-in sources: files (
isEasyMotionActive()guard — exported fromsrc/easymotion/register.tsto prevent picker from opening during EasyMotion label selection- Plugin:
src/easymotion/register.ts
- Plugin:
Changed
:buffers/:lsnow opens fuzzy picker instead of VimInfoModal table (whenpickersetting enabled):marksnow opens fuzzy picker with jump-to-mark action (whenpickersetting enabled):registersnow opens fuzzy picker with paste-at-cursor action (whenpickersetting enabled):ob(no args) now opens commands picker instead of VimInfoModal command list (whenpickersetting enabled):grep(no args) now opens live grep picker instead of showing “Usage” notice (whenpickersetting enabled):grep <query>now opens picker with pre-computed results instead of SuggestModal (whenpickersetting enabled):backlinksnow opens fuzzy picker instead of VimInfoModal table (whenpickersetting enabled)- Bundle size: +17.5KB from uFuzzy dependency (unicode mode)
Documentation
docs/features/ex-commands.md: added picker commands sectiondocs/reference/keybindings.md: added picker ex commands and<leader>f*mappingsdocs/configuration/lua-config.md: addedvim.obsidian.pick()API documentation; added “Shared config across vaults” subsection documenting external path supportdocs/configuration/vimrc.md: added “Shared config across vaults” subsection documenting external path supportdocs/configuration/settings.md: updated custom path descriptions to mention absolute path supportKNOWN_LIMITATIONS.md: added picker section with limitations; updated config file resolution section with external path support
[0.40.0] - 2026-07-07
Added
vim.keymap.setleader bindings appear in which-key — leader-prefixed keymaps registered viavim.keymap.setwith adescoption now automatically appear in the which-key overlay, matchingvim.obsidian.leader.addbehavior. Group labels fromvim.obsidian.whichkey.add()work with bothvim.keymap.setandvim.obsidian.leader.addbindings. Buffer-local keymaps (buffer = 0) are excluded from global which-key. (#27)- Plugin:
src/lua/api.ts(leader prefix auto-detection invim.keymap.set),src/main.ts(consumeluaResult.leaderBindingsin LeaderRegistry)
- Plugin:
- Synthetic
BufEnterfor initial file —BufEnterautocmds now fire for the file already open when the plugin loads, matching Neovim behavior. Previously,BufEnteronly fired on subsequent file opens.- Plugin:
src/lua/autocmd.ts(activate()acceptsinitialFilePath),src/lua/loader.ts(passes current file path)
- Plugin:
Fixed
vim.cmd()broken at runtime —vim.cmd()called from function-mapped keymaps, autocmd callbacks, timer callbacks, and user commands silently failed because commands were queued but never executed after initial load. Fixed with aruntimeExHandlerthat executes commands immediately viavim.handleEx(). Cleanup on plugin unload prevents stale callbacks. (#49, #27)- Plugin:
src/lua/loader.ts(runtimeExHandler,activateRuntimeExHandler,deactivateRuntimeExHandler),src/main.ts(wire runtime handler, cleanup inonunload)
- Plugin:
- Function-callback keymaps lost after feature reload —
vim.keymap.setwith function callbacks registered keymaps that were silently destroyed whenreloadFeatures()calledvim.resetKeymap(). String-RHS keymaps survived but function callbacks did not. Fixed by movingapplyLuaMaps()to run afterreloadFeatures()and clearingluaActionNamesinloadLuaConfigForTest().- Plugin:
src/main.ts(applyLuaMapsordering,loadLuaConfigForTestcleanup)
- Plugin:
- Space as leader key breaks which-key —
vim.g.mapleader = " "with which-key in “all” mode now works correctly: space doesn’t move the cursor, bindings execute, and grouped which-key displays. The “leader-only” mode still has a known limitation (see KNOWN_LIMITATIONS.md). (#49) - Surround nvim-surround parity (19 golden test fixes) — comprehensive alignment with nvim-surround semantics. Golden comparison tests passing: 54 → 73 out of 74. (#41)
ds}/ds]/ds)/ds>now preserve inner spaces (only opening bracket formsds{/ds[/ds(/ds<strip spaces)csbBysaBbchain —_surroundTypegating prevents stale replacement leaking across different surround operation typescsba..dot-repeat — search position offset by replacement delimiter width for correct nested pair iterationdsbon multiline content — cursor clamped to valid line length after bracket deletion- Count-prefixed
ds/cs(2dsb,3dsb,2csbB,3csbr) — changed from “find Nth pair” to “apply N times” semantics, matching nvim-surround yswith line-crossing motions (ysjb,ys2jB) — linewise motions now expand range to full linesySS/VSB/cS/yS/gSnewline indentation — single-line content no longer gets extra 2-space indent, matching nvim-surroundVS(linewise visual surround) — selection expanded to full lines, uses newline wrapping mode- Visual block
Ctrl-V $ S}— each line wrapped individually instead of entire block dsf— new operator: delete surrounding function call (some_func(args)→args), with nested call support- Fork:
src/vim.js—deleteSurroundPairspace/cursor,findSurroundingFunction, count loops, linewise/block visual handling,_surroundTypedot-repeat isolation - Fork:
src/types.ts—_surroundTypefield onInputStateInterface
Documentation
docs/configuration/lua-config.md: added leader key subsection withvim.g.mapleaderexamples and ordering warning; added tip callout comparingvim.cmd()vsvim.obsidian.leader.add()for leader bindingsdocs/configuration/which-key.md: added “Automatic labels from vim.keymap.set” section documentingdescoption integration with which-key and group label composition withwk.add()KNOWN_LIMITATIONS.md: added 7 Lua runtime entries (4 fixed, 3 open); updated test coverage (9 → 43 e2e tests); updated surround parity section- 34 new e2e tests across 4 suites —
lua-runtime.e2e.ts(8 tests: runtime vim.cmd execution from all callback contexts),lua-leader-whichkey.e2e.ts(9 tests: leader binding registration and which-key integration),lua-space-leader.e2e.ts(7 tests: space as leader key with regression coverage),lua-doc-examples.e2e.ts(10 tests: every documented Lua runtime callback example) - Shared test helpers extracted —
loadLuaConfig,focusEditor,setWhichKeyMode,hasWhichKeyOverlay,waitForWhichKey,getWhichKeyKeys,getWhichKeyDescriptions,getWhichKeyGroups,getLeaderBindings,getLeaderKey,getPluginSettingmoved totest/helpers.tsfrom local definitions
[0.39.0] - 2026-07-06
Added
vim.ob.*API expansion (47 new functions across 4 sub-namespaces) — thevim.obsidian/vim.obLua namespace grows from 21 to 68 functions- Leaf introspection (Tier 1):
vim.ob.get_leaf_type()returns the active view type string,vim.ob.get_active_leaf()returns{id, type, pinned, file_path}table,vim.ob.list_leaves()returns all open tabs,vim.ob.is_markdown_view()returns boolean - Command wrappers (Tier 2):
vim.ob.follow_link(),vim.ob.backlinks(),vim.ob.daily(),vim.ob.search(),vim.ob.tags(),vim.ob.new_note(),vim.ob.rename(),vim.ob.toggle_checkbox(),vim.ob.template()— thin wrappers around Obsidian commands, silent no-op if required core plugin is disabled - Leaf management (Tier 3):
vim.ob.focus(direction)navigates panes ("left","right","top","bottom"),vim.ob.close_leaf()closes active tab,vim.ob.split(direction)splits vertically/horizontally,vim.ob.get_leaf_for_file(path)finds which leaf has a file open vim.ob.meta.*sub-namespace (9 metadata query functions) — read-only access to note metadata via Obsidian’sMetadataCachevim.ob.meta.frontmatter(path?)— returns YAML frontmatter as a Lua table, or nilvim.ob.meta.tags(path?)— returns combined body + frontmatter tags asstring[]vim.ob.meta.links(path?)— returns outgoing links as{link, display, original}[]vim.ob.meta.backlinks(path?)— returns source file paths linking to this file asstring[]vim.ob.meta.headings(path?)— returns headings as{heading, level}[]vim.ob.meta.embeds(path?)— returns embedded content as{link, display}[]vim.ob.meta.aliases(path?)— returns YAML aliases asstring[]vim.ob.meta.tasks(path?)— returns checklist items as{text, status, line}[]vim.ob.meta.lists(path?)— returns all list items as{text, line, indent}[]- All functions default to the current file when
pathis omitted - Plugin:
src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts
vim.ob.fs.*sub-namespace (11 vault filesystem functions) — read and write vault files with config-dir guards- Read:
vim.ob.fs.files(pattern?),vim.ob.fs.all_files(),vim.ob.fs.folders(),vim.ob.fs.exists(path),vim.ob.fs.stat(path?) - Write:
vim.ob.fs.create(path, content?),vim.ob.fs.write(content)orvim.ob.fs.write(path, content),vim.ob.fs.append(content)orvim.ob.fs.append(path, content) - Management:
vim.ob.fs.rename(new_path)orvim.ob.fs.rename(path, new_path),vim.ob.fs.move(dest)orvim.ob.fs.move(path, dest)(detects folder dest and appends filename),vim.ob.fs.trash(path?) - Write/rename/move/trash operations silently reject paths inside the vault config directory (
app.vault.configDir) renameusesfileManager.renameFile()which updates backlinks;trashusesfileManager.trashFile()which respects the user’s trash preference- Write operations are fire-and-forget (async internally, Lua returns immediately)
- All write operations default to the current file when path is omitted
- Plugin:
src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts
- Read:
vim.ob.ui.*sub-namespace (4 UI control functions) — control Obsidian UI from Luavim.ob.ui.sidebar(side, state?)— toggle/open/close sidebar ("left"/"right", optional"open"/"close"/"toggle")vim.ob.ui.command_palette()— open command palettevim.ob.ui.quickswitch()— open quick switchervim.ob.ui.notice(msg)— alias forvim.notify(convenience for staying invim.obnamespace)- Plugin:
src/lua/obsidian-api.ts
vim.obeditor state and convenience functions — cursor, selection, mode, and notification accessvim.ob.get_cursor()— returns{line, col}(1-indexed, Lua/Neovim convention)vim.ob.set_cursor(line, col)— sets cursor position (1-indexed)vim.ob.get_selection()— returns visual selection text or nilvim.ob.mode()— alias forvim.fn.mode()(convenience)vim.ob.notice(msg)— alias forvim.notify(convenience)- Plugin:
src/lua/obsidian-api.ts,src/lua/api.ts,src/lua/loader.ts
- Leaf introspection (Tier 1):
- 3 new autocmd events —
LeafEnter,LeafLeave,FileType(total: 15 events)LeafEnter— fires when a new leaf gains focus (debounced 50ms), event data includes{type, leaf_id}inev.dataLeafLeave— fires when a leaf loses focus (immediate, beforeLeafEnter)FileType— fires afterBufEnterwithev.matchset to detected filetype from file extension (.md→"markdown",.ts→"typescript", etc.)- Enables Neovim-style per-filetype keymaps:
vim.api.nvim_create_autocmd("FileType", { pattern = "markdown", callback = function() ... end }) - Plugin:
src/lua/autocmd.ts(fireFileType,onActiveLeafChangeextension),src/main.ts(leaf info passthrough)
workspaceNavViewTypessetting — comma-separated list of view types where scroll and count keys are intercepted. Defaults tomarkdown,graph,pdf,canvas,empty,image. Plugin views not in this list receive their own keystrokes. Configurable via Settings → Vim Motions → Workspace navigation view types, vimrc (set workspacenavviewtypes=...), or Lua (vim.opt.workspacenavviewtypes = "..."orvim.opt.workspacenavviewtypes = {"markdown", "graph", "pdf"})- Plugin:
src/settings.ts,src/vim/options.ts,src/vimrc/loader.ts
- Plugin:
vim.opttable (array) support for string options — string-type options can now be set using Lua tables:vim.opt.workspacenavviewtypes = {"markdown", "graph", "pdf"}is equivalent tovim.opt.workspacenavviewtypes = "markdown,graph,pdf". Elements are joined with commas. Applies to all string-type options.- Plugin:
src/lua/api.ts(vim.opt.__newindextable handling)
- Plugin:
Fixed
- Surround opening bracket semantics (
ds(/ds[/ds{/cs({) —findSurroundingBracketsreceived swapped parameters when the target was an opening bracket ((,[,{,<), causing the backward search to look for the wrong bracket character.ds(on( hello world )was a no-op because the search looked for)going backward. Fixed by detecting opening bracket targets and swapping parameters so the closing bracket is always passed as the forward-search character. Also fixescs({,ds(on nested/multiline content, andds<. (#41)- Fork:
src/vim.js—findSurroundingPairbracket parameter ordering
- Fork:
- Surround cursor position after
ys/yss/visualS—addSurroundToRangeplaced the cursor atfrom.ch + pair.open.length(after the opening delimiter). nvim-surround places it atfrom.ch(on the opening delimiter). Fixed by removing+ pair.open.length. The_surroundSelOffset.chDeltaused for dot-repeat now addspair.open.lengthat recording time to compensate, preserving correct visual surround replay ranges. (#41)- Fork:
src/vim.js—addSurroundToRangecursor,surroundVisualoffset recording
- Fork:
- Visual-block cursor displaced rightward at end-of-line — in visual-block mode (
<C-v>), selecting to the end of a line (via$orlto EoL) caused the block cursor to render one position past the last visible character. ThemeasureCursor()function in the fork’sblock-cursor.tshad a guard (!vim.visualBlock) that prevented the EOL step-back for visual-block mode. This guard was originally correct whenmakeCmSelectionproducedtoCh + 1without clamping, but after the per-line clamping fix (issue #38), block selection heads legitimately land on newline positions and need the step-back. Fixed by removing the!vim.visualBlockexclusion. (#41)- Fork:
src/block-cursor.ts—measureCursor()EOL adjustment guard
- Fork:
- Visual-block
Askips short lines instead of padding — in visual-block mode,A(append) on a block spanning lines shorter than the block column skipped those lines entirely. Neovim pads short lines with spaces to reach the block’s right edge before appending. Fixed by adding apadShortLinesparameter toselectForInsertin the fork —Apads,Istill skips (matching Neovim). (#41)- Fork:
src/vim.js—selectForInsert()padding,enterInsertModepasses flag forendOfSelectedArea
- Fork:
- Visual charwise
rreplaces one fewer character across line boundary — thereplaceaction in the fork usedcurEnd = selEnd(the inclusive head position) for charwise visual mode, butcm.getRange()treats the end as exclusive. This causedr <Space>across a line boundary to replace one fewer character than the visual selection covered. Fixed by usingselEnd.ch + 1for the exclusive end. (#41)- Fork:
src/vim.js—actions.replacecharwise visual branch
- Fork:
set insertmodeescape=jkleavesjin buffer after escaping insert mode — theInsertEscapeHandlersentvim.handleKey(adapter, '<BS>')to delete typed characters before sending<Esc>, but codemirror-vim does not handle<BS>in insert mode (returns false, expecting the browser default action). SincehandleKeyis called programmatically with no DOM event, the backspace had no effect and the first character(s) of the escape sequence remained in the buffer. Fixed by replacing thehandleKey('<BS>')loop with a directadapter.replaceRange()call that deletes exactlyescapeSeq.length - 1characters before the cursor (the last key in the sequence is already intercepted bypreventDefaultand never enters the document). The nativeimap jk <Esc>mapping (via vimrc orvim.map()) was unaffected — codemirror-vim’schangeQueuecleanup handles that path correctly.- Plugin:
src/vim/insert-escape.ts(onKeyDownmethod)
- Plugin:
scrolloffvalues above ~30 pin view at bottom of document — highscrolloffvalues (e.g.,set scrolloff=999to center the cursor) caused the viewport to pin at the top or bottom instead of centering. The scroll margin was passed to CodeMirror’sEditorView.scrollMarginsunclamped, producing a target rect taller than the viewport. CM6’sscrollRectIntoViewresolved the conflicting top/bottom constraints by favoring one side based on cursor direction. Fixed by clamping the margin to half the viewport height, mirroring Vim’s silent cap ofscrolloffto(window_height - 1) / 2. (#48)- Plugin:
src/vim/scrolloff.ts—createScrolloffExtension()viewport-relative clamp
- Plugin:
- Workspace navigation intercepting keystrokes in plugin leaves — when workspace navigation was enabled, the global key handler consumed keystrokes (
1,2,3,0,j,k, etc.) in non-editor plugin views (Spaced Repetition, Excalidraw, etc.) before the plugin could process them. Fixed with a three-gate interception system: structural keys (<C-w>*,gt/gT,<C-o>/<C-i>,:) always work in non-editor views, content keys (scroll, digits, tab shortcuts) only intercept in whitelisted view types, and plugin views receive their own keystrokes. (#47)- Plugin:
src/workspace/global-mapping-registry.ts(GlobalMapGate→'standard' | 'hint' | 'structural'),src/workspace/global-defaults.ts(gate assignments),src/workspace/global-key-handler.ts(three-gateonKeydownrewrite,GLOBAL_NAV_VIEW_TYPESwhitelist,shouldInterceptContent/shouldInterceptStructuralmethods)
- Plugin:
Changed
minAppVersionbumped from 1.4.10 to 1.6.6 — required forVault.getAllFolders()used byvim.ob.fs.folders()vim.obsidian.*namespace extracted to dedicated module — the Obsidian-specific Lua API is now insrc/lua/obsidian-api.ts(extracted fromapi.ts), following the pattern offn.ts,stdlib.ts,timers.ts,highlight.ts. No behavioral change.api.tsshrinks by ~504 lines.
Documentation
docs/configuration/lua-config.md: addedvim.ob.meta.*(9 functions),vim.ob.fs.*(11 functions),vim.ob.ui.*(4 functions), editor state functions (5 functions) with API tables and examplesKNOWN_LIMITATIONS.md: added “Workspace navigation in plugin views” section documenting three-gate interception and thegg-in-plugin-leaf trade-off; marked #47 as fixed; added “Neovim golden test coverage gaps” section documenting non-verifiable areas (scroll/viewport, fold, jumplist, cursor rendering); updated visual mode EOL cursor section with visual-blockApadding and visualroff-by-one fixes- Neovim golden test coverage expansion — 106 new golden comparison test cases across 6 suites, recorded against Neovim 0.12.2:
surround(74 cases): comprehensive nvim-surround parity —ds/cs/ys/yss/visualSwith all delimiter types, count-prefixed operations (2dsb,2csbB), dot-repeat (ysiwb..,dsb..,csba..), tag surround (dst,cst), function surround (dsf),ysa(around surround), empty content, whitespace cascade (ds{strips /ds}preserves), motion-based (ys$,ysjb), newline variants (ySS,VSB), angle brackets, arbitrary delimiters (|,^), multiline, nesting, and cursor positioning. Ground truth shifted from tpope/vim-surround to nvim-surround — better maintained, comprehensive test suite, Lua-native, superset of tpope behavior. 54 pass, 20 tracked deviations.dot-repeat(17 cases):.after2dw,dd,3i,3o,cw,R,2dl,d2w,g~2w,V>,3J,3I, visual block~,oselect-mode-extended(6 cases):gh/gHenter select, type replaces,<BS>deletes,<Esc>exits,<C-g>toggles visual↔selectex-sort(6 cases)::sort,:sort!,:sort i,:sort u,:sort n,:2,3sortex-global(3 cases)::g/pattern/d,:v/pattern/d,:g/a/s/a/x/upstream-gaps(7 cases):dipparagraph, backward blockA, blockAshort-line padding, visualrcross-line, block↔char/line mode switch, macro replay- Test infrastructure:
SuiteDefinition.nvimSetupfield for per-suite Neovim commands (loads nvim-surround for surround suite),NeovimClient.executeCommand()method - Total golden test coverage: 276 → 382 cases across 28 suites
docs/configuration/settings.md: addedWorkspace navigation view typesto Vim features tabledocs/configuration/vimrc.md: addedworkspacenavviewtypes(wnvtalias) to string options tabledocs/configuration/lua-config.md: added 17 newvim.ob.*functions, 3 new autocmd events (LeafEnter,LeafLeave,FileType),workspacenavviewtypesoptiondocs/features/workspace-navigation.md: added “Plugin view compatibility” section with key passthrough table and whitelist customizationdocs/guides/ecosystem-compatibility.md: added “Plugin leaf key passthrough” sectiondocs/configuration/lua-config.md: added table (array) syntax tip for string options with example
[0.38.0] - 2026-07-06
Added
- Custom surround pairs (
vim.obsidian.surround/surroundmap) — define custom single-character triggers that map to arbitrary delimiter strings, with fullys/ds/cssupport including multi-character delimiters (#36)vim.obsidian.surround.set("l", { left = "[[", right = "]]" })— register a custom pairvim.obsidian.surround.del("l")— remove a custom pairvim.obsidian.surround.add({ { "l", left = "[[", right = "]]" }, { "m", left = "$$", right = "$$" } })— batch registration- Vimrc:
surroundmap l [[ ]]/surroundunmap l - Reserved characters (`( ) [ ] { } < > b B r a t T f F ” ’ “) are rejected with a descriptive error
- Requires fork mode (bundled vim engine) — custom pairs are registered via
Vim.registerSurroundPair()on the codemirror-vim fork - Fork:
customSurroundPairsregistry,findSurroundingMultiChar()algorithm for multi-char delimiter matching,openWidth/closeWidthsupport indeleteSurroundPair/changeSurroundPair - Plugin:
src/lua/api.ts(vim.obsidian.surroundsub-table),src/vimrc/parser.ts+src/vimrc/loader.ts(surroundmap/surroundunmapcommands),src/main.ts(applyLuaSurroundPairslifecycle)
vim.obsidian.cursor.set()— structured cursor shape configuration — set per-mode cursor shapes via a Lua table instead of theguicursorformat stringvim.obsidian.cursor.set({ normal = "block", insert = "bar", operator_pending = "underline" })— partial tables allowed- Valid shapes:
block,bar,underline,hollow - Equivalent to
vim.opt.guicursorbut uses a table API - Plugin:
src/lua/api.ts(onCursorConfigcallback,vim.obsidian.cursorsub-table)
vim.obsidian.modeprompt.set()— batch mode prompt configuration — set status bar mode text for multiple modes in a single callvim.obsidian.modeprompt.set({ normal = "NOR", insert = "INS", visual_line = "V-LN" })— partial tables allowed- 11 mode keys supported with snake_case Lua names mapped to camelCase settings keys
- Equivalent to setting individual
vim.g.mode_prompt_*variables - Plugin:
src/lua/api.ts(onModePromptConfigcallback,vim.obsidian.modepromptsub-table)
vim.obsidian.leader.set()— leader binding convenience API — bind leader key sequences to Obsidian commands with automatic:obprefix, leader key prepend, and which-key label registrationvim.obsidian.leader.set("e", "file-explorer:reveal-active-file", { desc = "Reveal" })— single bindingvim.obsidian.leader.add({ { "ff", "switcher:open", desc = "Find file" } })— batch registrationdescoption auto-registers a which-key command label- For general-purpose keymaps or Lua callbacks, use
vim.keymap.setinstead - Plugin:
src/lua/api.ts(onLeaderBinding/onLeaderBindingDelcallbacks,vim.obsidian.leadersub-table)
Fixed
vim.g.mode_prompt_*read returns nil for settings-UI-set values — thegetModePromptcallback was defined in theVimApiCallbacksinterface but not wired up inloader.ts. Readingvim.g.mode_prompt_normalreturned nil unless the value was also set viavim.gin the same init.lua session. Fixed by implementing the callback in the loader.- Plugin:
src/lua/loader.ts(getModePromptcallback)
- Plugin:
Documentation
docs/configuration/lua-config.md: added 4 new Obsidian namespace sections — cursor shapes (vim.obsidian.cursor), mode prompts (vim.obsidian.modeprompt), custom surround pairs (vim.obsidian.surround), leader bindings (vim.obsidian.leader) — with API tables, examples, and cross-referencesKNOWN_LIMITATIONS.md: updated Lua supported APIs list to includevim.obsidian.cursor.set,vim.obsidian.modeprompt.set,vim.obsidian.surround.set/del/add,vim.obsidian.leader.set/del/add; updated surround section with custom pairs documentation and issue #36 reference
[0.37.0] - 2026-07-06
Added
vim.obsidian.whichkey.add()— batch which-key label configuration — define multiple group and command labels in a single call, similar to Neovim’s which-key.nvimwk.add()syntax (#27)vim.obsidian.whichkey.add({ { "<leader>f", group = "Find" }, { "<leader>w", desc = "Save" } })— each entry usesgroupfor prefix labels ordescfor individual binding labels- Per-entry
contextfield:"editor"(default) or"global"for non-editor which-key overlay modefield accepted but reserved for future mode-scoped label support- Entries without a key string or without
group/descare silently skipped - Shorthand:
local wk = vim.obsidian.whichkey; wk.add({ ... })for Neovim-familiar syntax - Plugin:
src/lua/api.ts(vim.obsidian.whichkey.add),src/lua/types.d.ts(luaL_lentype)
Changed
- Config file fallback chains — vimrc and Lua config files are now resolved via a fallback chain instead of a single hardcoded path. The plugin searches the vault root for the first matching file. Custom path overrides still take priority.
- Vimrc chain (8 candidates):
vimrc,.vimrc,init.vim,.init.vim,obsidian.vimrc,obsidian.vim,.obsidian.vimrc,.obsidian.vim - Lua chain (5 candidates):
init.lua,.init.lua,obsidian.init.lua,.obsidian.init.lua,obsidian.lua - Non-dotfile names (
vimrc,init.lua) are preferred — Obsidian Sync skips dotfiles, and the.obsidian.*naming relied on a linter workaround - Settings UI now shows “Currently using: {path}” (resolved path) or “File not found” for invalid custom paths
- Settings descriptions list the full fallback chain
- Backward compatible: existing
.obsidian.vimrcand.obsidian.init.luafiles still work (they appear later in the chain) - Plugin:
src/vimrc/loader.ts(resolveVimrcPath,VIMRC_FALLBACK_PATHS),src/lua/loader.ts(resolveLuaConfigPath,LUA_FALLBACK_PATHS),src/settings.ts(async path resolution display),styles.css(.vim-motions-config-path-active/.vim-motions-config-path-errorclasses)
- Vimrc chain (8 candidates):
Documentation
docs/configuration/vimrc.md: file location section rewritten with full fallback chain tabledocs/configuration/lua-config.md: file location section rewritten with full fallback chain table; addedvim.obsidian.whichkey.add()to API summary table and Obsidian namespace section withwk.add()exampledocs/configuration/settings.md: custom path setting descriptions updated with fallback chain listsdocs/configuration/which-key.md: added “Batch labels (add())” section with Neovim-stylewk.add()syntax,local wkshorthand tip, and reservedmodefield calloutdocs/guides/migrating-from-vimrc-support.md: custom vimrc path section updated with fallback chainKNOWN_LIMITATIONS.md: updated supported Lua APIs list to includevim.obsidian.whichkey.add()
[0.36.0] - 2026-07-06
Added
vim.obsidian.keymap— global (non-editor) keymaps from Lua — define key bindings for non-editor contexts (graph view, canvas, PDF viewer, file explorer) using a Neovim-style APIvim.obsidian.keymap.set(lhs, rhs, opts?)— create a global keymap with:obcommand <id>or:<ex-command>as RHSvim.obsidian.keymap.del(lhs)— remove a global keymapdescoption auto-creates a label in the global which-key popup- Lua global keymaps override vimrc
gmapon conflict (last-write-wins) - Survives settings changes and feature reloads via
luaGlobalMapspersistence arrays - Plugin:
src/lua/api.ts(LuaGlobalKeymaptype,onGlobalKeymap/onGlobalKeymapDelcallbacks)
vim.obsidian.whichkey— which-key labels from Lua — set group and command labels for the which-key popupvim.obsidian.whichkey.set_group(key, label, opts?)— name a which-key group by prefixvim.obsidian.whichkey.set_label(key, label, opts?)— label an individual which-key bindingcontextoption defaults to"editor"; use{ context = "global" }for non-editor which-key overlay- Previously only available via vimrc
whichkeygroup/whichkeylabeland Settings UI - Plugin:
src/lua/api.ts(onWhichKeyGroupLabel/onWhichKeyCommandLabelcallbacks)
vim.opt.guicursor— cursor shapes from Lua — set per-mode cursor shapes withoutvim.cmdpassthroughvim.opt.guicursor = "n:block,i:bar,v:block,r:underline,o:underline"— mode codes:n,i,v,r,o,a(all); shapes:block,bar,underline,hollow- Write-only (reading returns nil); invalid strings log a warning
- Previously only available via vimrc
set guicursor=...
- Lua standard library utilities (
vim.tbl_*,vim.split,vim.inspect,vim.json) — 22 Neovim-compatible utility functions for table manipulation, string operations, debugging, and JSON serialization- Table utilities (12):
vim.tbl_deep_extend,vim.tbl_extend,vim.tbl_contains(with predicate support),vim.tbl_keys,vim.tbl_values,vim.tbl_map,vim.tbl_filter,vim.tbl_count,vim.tbl_isempty,vim.tbl_get,vim.list_extend,vim.deepcopy - String utilities (6):
vim.split(with{plain, trimempty}options),vim.trim,vim.startswith,vim.endswith,vim.pesc,vim.stricmp vim.inspect(value)— human-readable table/value serialization for debugging init.lua configsvim.json.encode(value)/vim.json.decode(str)— JSON serialization bridged to JavaScript’sJSON.stringify/JSON.parse- Plugin:
src/lua/stdlib.ts
- Table utilities (12):
- Async primitives (
vim.schedule,vim.defer_fn,vim.uvtimers) — Neovim-compatible async APIs for deferred execution and timer managementvim.schedule(fn)— defer function to next event loop iteration (useful for breaking recursive autocmd loops)vim.schedule_wrap(fn)— returns a function that wrapsfnwithvim.schedule, passing all argumentsvim.defer_fn(fn, timeout)— defer function bytimeoutmilliseconds, returns cancellable handle withstop()/close()/is_closing()vim.uv.new_timer()— create timer withstart(delay, repeat, callback),stop(),close(),is_closing(),is_active()vim.uv.hrtime()— high-resolution time in nanosecondsvim.uv.now()— current time in millisecondsvim.loopalias forvim.uv(Neovim backward compatibility)- All timers cleaned up on plugin unload (no leaked timeouts)
- Plugin:
src/lua/timers.ts
- Buffer-local keymaps (
vim.keymap.set({ buffer = 0 })) — keymaps scoped to specific files, automatically swapped on editor/tab switchvim.keymap.set("n", "gd", handler, { buffer = 0 })— keymap active only in the current filevim.api.nvim_buf_set_keymap(0, mode, lhs, rhs, opts)/nvim_buf_del_keymap(0, mode, lhs)— low-level buffer keymap APIs- Combined with
BufEnterautocmd for per-filetype keymaps (e.g., markdown-only bindings) - Buffer identity uses vault-relative file path; only
buffer = 0(current file) is supported - Plugin:
src/lua/buffer.ts(BufferKeymapManager)
- Buffer content APIs (
nvim_buf_get_lines,nvim_buf_set_lines) — read and modify editor content from Lua callbacksvim.api.nvim_buf_get_lines(0, start, end, strict_indexing)— 0-based, end-exclusive,-1for EOFvim.api.nvim_buf_set_lines(0, start, end, strict_indexing, replacement)— empty table deletes linesvim.api.nvim_get_current_buf()— returns0(current buffer)vim.api.nvim_buf_get_name(0)— vault-relative file pathvim.api.nvim_buf_line_count(0)— total line countstrict_indexing = trueerrors on out-of-bounds;falseclamps silently
- 4 new autocmd events —
CursorMoved,CursorHold,BufWritePre,BufWritePost(total: 12 events)CursorMoved— fires after cursor moves (throttled viavim-command-doneevent)CursorHold— fires after cursor is idle forupdatetimems (default 4000, configurable viavim.opt.updatetime)BufWritePre/BufWritePost— fire before/after:w,:wq,:x,:wall,:updatewith vault-relative glob pattern supportupdatetimeoption added toKNOWN_SET_OPTIONSfor vimrc andvim.optconfiguration
vim.obsidiannamespace (vim.obalias) — Obsidian-specific APIs that don’t exist in Neovimvim.obsidian.vault_name(),vim.obsidian.app_version(),vim.obsidian.plugin_version()vim.obsidian.run_command(id)— execute any Obsidian command by IDvim.obsidian.list_commands()— table of{id, name}for all available commandsvim.obsidian.open_file(path)— open a vault filevim.obsidian.current_file()— table{path, name, extension, basename}or nilvim.obsidian.vault_path()— vault absolute path (desktop only)
- Sandboxed
vim.env— environment variable proxy with curated values and user-defined storagevim.env.HOME(vault path),vim.env.VIM("motions"),vim.env.TERM("obsidian"),vim.env.OBSIDIAN_VERSION- Custom variables:
vim.env.MY_VAR = "value"— stored in memory, not inprocess.env - Unknown keys return nil
vim.api.nvim_set_hl— highlight group → CSS bridge — customize plugin styling from Lua using Neovim’s highlight APIvim.api.nvim_set_hl(0, "EasyMotionTarget", { fg = "#ff5555", bold = true })— change EasyMotion label colorsvim.api.nvim_set_hl(0, "StatusLineNormal", { bg = "#282a36" })— change status bar mode colors- 13 plugin-defined highlight groups:
EasyMotionTarget,EasyMotionShade,HintTarget,StatusLineNormal/Insert/Visual/Replace/VLine/VBlock/Command/Search/Select/VReplace - User-defined groups generate
.vim-hl-GroupNameCSS classes - Supports:
fg,bg,sp,bold,italic,underline,undercurl,strikethrough,reverse,blend,link(group inheritance),default(don’t override),update(merge) vim.api.nvim_get_hl(0, { name = "group" })— query highlight attrsvim.api.nvim_create_namespace(name)— returns0(only global namespace supported)- Plugin:
src/lua/highlight.ts(HighlightManager)
- Enhanced
vim.notifywith log levels —vim.notify(msg, level)routes messages by severityvim.log.levels:TRACE(0),DEBUG(1),INFO(2),WARN(3),ERROR(4),OFF(5)ERROR/WARN→ Obsidian Notice + console;INFO→ Notice;DEBUG/TRACE→ console.debug onlyvim.notify_once(msg, level)— deduplicates by message content
Fixed
- Space-leader global keymaps not matching keyboard input —
replaceLeaderKeyconverted<leader>to raw" "(space character), butnormalizeKeyEventinGlobalKeyHandlerconverted spacebar to"<Space>". The key sequences never matched inGlobalMappingRegistry.resolve(). Fixed by addingnormalizeKeyString()to convert raw special characters to angle-bracket notation (" "→"<Space>") before storing keys in the registry. Affects both vimrcgmapand Luavim.obsidian.keymap.setwith space leader.- Plugin:
src/workspace/global-mapping-registry.ts(normalizeKeyString),src/main.ts(applyGlobalMaps,rebuildGlobalWhichKey)
- Plugin:
vim.g.mode_prompt_*reads returned nil after write — the__newindexhandler for mode_prompt keys calledonSettingOverridebut did not store the value in theglobalsMap. The__indexhandler’s fallback toglobals.get(key)returnedundefined. Fixed by also storing inglobalson write.- Plugin:
src/lua/api.ts(vim.g__newindexhandler)
- Plugin:
Changed
vim.apiexpanded from 6 to 16 functions —nvim_set_hl,nvim_get_hl,nvim_create_namespace,nvim_buf_get_lines,nvim_buf_set_lines,nvim_get_current_buf,nvim_buf_get_name,nvim_buf_line_count,nvim_buf_set_keymap,nvim_buf_del_keymapadded alongside existing autocmd/augroup/user command functions- Autocmd events expanded from 8 to 12 — added
CursorMoved,CursorHold,BufWritePre,BufWritePost vim.obsidiannamespace expanded — addedkeymapandwhichkeysub-namespaces for global keymaps and which-key labels.vim.obalias includes the new sub-namespaces.
Documentation
docs/configuration/lua-config.md: comprehensive Lua API reference expansion — added vim.opt table with defaults and valid ranges, keymapping mode reference, autocmd event data reference (per-eventev.datafields), highlight group CSS variable mapping, Lua sandbox reference (available/unavailable libraries, instruction limits),vim.fn.has()completeness statement, mode prompt customization section, global keymaps section (vim.obsidian.keymap), which-key labels section (vim.obsidian.whichkey),vim.opt.guicursoroption; fixedbufferoption row (was “Not supported”, now correctly documentsbuffer = 0/true); fixedos/debuglibrary availability claims (not loaded by plugin); addedvim.stricmp,vim.env.MYVIMRC,underdouble/underdotted/underdashedhighlight attributes, TextYankPostregnamefield, highlight group case-sensitivity callout, buffer-local keymap accumulation warning, underline style limitation calloutdocs/configuration/which-key.md: added Lua examples for group labels (vim.obsidian.whichkey.set_group) and global which-key labelsdocs/configuration/cursor-shapes.md: addedvim.opt.guicursorLua section, removed “not supported” workaround notedocs/configuration/status-bar.md: expanded Lua mode prompt examples to all 11 modesdocs/features/ex-commands.md: added Lua example for custom commands vianvim_create_user_commandKNOWN_LIMITATIONS.md: updated supported APIs list (addedvim.obsidian.keymap,vim.obsidian.whichkey,vim.opt.guicursor), correctedos/debuglibrary availability (not loaded by plugin sandbox)AGENTS.md: clarified fengari fork vs plugin library loading distinction (fork keepsos/debug, plugin does not load them)README.md: updated tagline and Lua configuration feature bullet with expanded API surface
[0.35.0] - 2026-07-05
Changed
- Fengari Lua runtime switched to browser-only fork — replaced upstream
fengari(v0.1.5) with a browser/Obsidian-only fork that strips all Node.js dependencies. Eliminates community scanner warnings for “Direct Filesystem Access” (require('fs')), “Shell Execution” (require('child_process')), and “System Identity Information” (process.env.USER/HOSTNAME) that originated from fengari’s bundled Node.js code paths (never executed at runtime but present in the bundle). (DIFFERENCES.md)- Removed from fork:
liolib.js(Luaiolibrary),loadlib.js(Luapackage/require()system), Node.js branches fromloslib.js/ldblib.js/lauxlib.js/lbaselib.js/luaconf.js - Removed npm dependencies:
readline-sync,tmp(keptsprintf-jsforstring.format) - Retained browser-safe
oslibrary functions:os.date,os.time,os.difftime,os.clock,os.setlocale - Retained
debuglibrary (minusdebug.debug()interactive REPL):debug.traceback,debug.getinfo,debug.sethook, etc. - Fixed crash-on-mobile bug: upstream’s unconditional
process.env.FENGARICONFaccess at module load time throwsReferenceErroron non-Electron platforms - Bundle impact: Fengari runtime reduced from +238KB to +201KB minified (-37KB / -15.5%), +179KB to +165KB gzipped (-14KB / -7.7%)
print()now always usesconsole.log(previously usedprocess.stdout.writein Electron)luaL_loadfilexstubbed to return error (plugin already disabledloadfile/dofileat Lua level)- Dependency pattern matches codemirror-vim fork:
"fengari": "https://github.com/saberzero1/fengari.git"inpackage.json
- Removed from fork:
[0.34.0] - 2026-07-05
Added
- Lua configuration support (
.obsidian.init.lua) — optional Neovim-style Lua configuration using a sandboxed Fengari Lua 5.3 runtime. Provides conditional logic, function-based keymaps, and familiarvim.keymap.set/vim.optsyntax. Disabled by default — enable in Settings → Vim Motions → Vimrc & key bindings → Enable Lua configuration. (#46)vim.opt.<name> = value/vim.o.<name>— set any plugin option (backed by the sameKNOWN_SET_OPTIONSmap as vimrcsetcommands)vim.g.mapleader/vim.g.<name>— set leader key and user variablesvim.keymap.set(mode, lhs, rhs, opts)— key mappings with string or function RHS,descfor which-key labels,noremap/remapcontrol, multi-mode supportvim.keymap.del(mode, lhs)— remove mappingsvim.cmd(string)— execute ex commands (deferred until first editor focus)vim.vault_name()— returns the current vault name for per-vault conditional configvim.notify(msg)— show an Obsidian notification from Luaprint(...)— outputs to developer console- Sandbox: 6 defense layers — selective library loading (no
io/os/debug/package), dangerous globals stripped (load/dofile/loadfile), nofengari-interop, instruction-count timeout vialua_sethook(1M instruction limit), custom environment table - Hybrid loading: settings and keymaps load immediately without an active editor;
vim.cmd()calls are queued and executed on first editor focus - Override hierarchy: init.lua loads after vimrc — Lua values override vimrc on conflict
- Settings:
configModedropdown (Lua + Vimrc / Lua only / Vimrc only / Settings only),luaConfigPath(custom file path) - Bundle impact: +238KB minified / +79KB gzipped (Fengari runtime)
- Plugin:
src/lua/engine.ts(sandbox + timeout),src/lua/api.ts(vim.* bridge),src/lua/loader.ts(hybrid file loading),src/lua/types.ts(Fengari type declarations) - 12 Neovim golden comparison test cases (
lua-keymapssuite), 17 e2e integration tests, 4 known deviations registered
vim.fn.*Neovim function subset — 27 functions from Neovim’svim.fnnamespace, scoped for Obsidian’s vault-centric environment- Config/detection (13):
has,expand,fnamemodify,exists,localtime,strftime,filereadable,isdirectory,glob,mode,line,col,getline - String manipulation (14):
tolower,toupper,trim,strlen,strwidth,stridx,strridx,strpart,substitute,nr2char,char2nr,split,join vim.fn.has(feature)— platform detection with 12 features:mac,linux,win32,unix,mobile,desktop,ios,android,obsidian,obsidian-X.Y,nvim(0),vim(0)vim.fn.expand('%')— vault-relative file path with modifiers (:t,:e,:r,:h,:p)vim.fn.fnamemodify(path, mods)— general-purpose path modifier with chainable modifiers (:t:r)vim.fn.filereadable(path)/vim.fn.isdirectory(path)— vault-scoped, path traversal blockedvim.fn.glob(pattern)— vault-scoped file matchingvim.fn.line('.')/vim.fn.col('.')/vim.fn.getline('.')— context-aware: return cursor position in function callbacks, return 0 at config-load timevim.fn.strftime(fmt)— full C89 strftime implementation (src/lua/strftime.ts)- Unsupported
vim.fn.*functions produce a helpful error listing available functions vim.fn.hostname()/vim.fn.getenv()intentionally skipped (system fingerprinting concern)- Plugin:
src/lua/fn.ts(VimFnCallbacks, function registry,__indexdispatch),src/lua/strftime.ts(pure strftime utility)
- Config/detection (13):
vim.api.nvim_create_user_command: define custom ex commands from Lua- String RHS:
vim.api.nvim_create_user_command("W", "w", {}): simple aliases - Function RHS:
vim.api.nvim_create_user_command("Today", function(opts) ... end, {}): Lua callback withopts.args vim.apichanged from error stub to partial namespace: unsupportedvim.api.*functions give a helpful error listingnvim_create_user_commandas available- Registered commands are immediately usable from the
:ex command line
- String RHS:
nvim_create_autocmd/nvim_create_augroup: Neovim-compatible autocommand system with 8 events- Events:
InsertEnter,InsertLeave,ModeChanged,BufEnter,BufLeave,FocusGained,FocusLost,TextYankPost - Augroups with
{ clear = true }for safe config reloads nvim_del_autocmd,nvim_del_augroup_by_name,nvim_clear_autocmdsfor management- ModeChanged supports
"old:new"pattern with*wildcard - BufEnter/BufLeave support vault-relative path glob patterns
- TextYankPost provides structured data: operator, regcontents, regtype, visual
- Non-nested guard prevents infinite autocmd loops
- Reentrancy protection: settings changes from callbacks defer reloadFeatures()
- Plugin:
src/lua/autocmd.ts(AutocmdManager class) - Fork:
vim-yanksignal added to yank/delete/change operators invim.js - 16 unit tests, 2 e2e tests
- Events:
- Unit test infrastructure — Vitest test runner for the Lua config modules
- 49 unit tests across 6 files (smoke, sandbox, timeout, api, fn, strftime)
- Runs in 250ms without Obsidian or browser
npm run test:unit/npm run test:unit:watchscripts- Obsidian module mocked via
test/unit/__mocks__/obsidian.ts - CI:
.github/workflows/lint.ymlnow runs unit tests on every push across all branches
Changed
- Consolidated configuration settings — replaced two independent toggles (
enableVimrc+enableLuaConfig) with a single Configuration mode dropdown (configMode):- Lua + Vimrc (default): both loaded, Lua overrides vimrc on conflict
- Lua only: only init.lua loaded
- Vimrc only: only .obsidian.vimrc loaded
- Settings only: neither config file loaded
- Notification logic consolidated: in Lua + Vimrc mode, only notifies when NEITHER file is found (no spam about missing vimrc when only using Lua, or vice versa)
- Automatic migration from old boolean settings on first load
- Custom path fields (init.lua path, vimrc path) remain independent and disable based on active mode
Documentation
docs/configuration/lua-config.md: full Lua configuration reference with supported APIs, allvim.optoptions,vim.fn.*function tables (has features, expand modifiers, fnamemodify modifiers, exists expressions), mapping examples, conditional config examples, loading order, unsupported API documentationdocs/configuration/settings.md: updated withconfigModedropdown replacing old toggles, added Lua column to all settings tablesdocs/configuration/index.md: reordered — Lua configuration presented as primary method, vimrc as alternativedocs/configuration/vimrc.md: added tip pointing to Lua configuration for advanced use casesdocs/configuration/which-key.md: added Luadescoption integration for which-key labelsdocs/configuration/cursor-shapes.md: addedvim.cmdworkaround note for guicursordocs/configuration/status-bar.md: added Lua equivalents for status bar settingsdocs/features/quality-of-life.md: added Lua examples alongside vimrcdocs/features/workspace-navigation.md: added Lua examples alongside vimrcdocs/getting-started/quickstart.md: reordered — Lua shown as recommended configuration pathdocs/reference/known-limitations.md: Lua configuration section with supported/unsupported APIs, hybrid loading, vim.fn subset, bundle sizeKNOWN_LIMITATIONS.md: Lua configuration section with full details
[0.33.0] - 2026-07-05
Fixed
- Obsidian commands only affect cursor line in visual-line mode (all invocation paths) — the previous fix (0.31.0, fork-side) only covered keyboard events that vim didn’t handle: it expanded the CM6 selection in the fork’s
handleKeyduring the bubble phase. However, Obsidian’sKeymapregisters its keydown listener onwindowin the capture phase (addEventListener("keydown", handler, true)), which fires before CM6’s bubble-phase handler — so commands triggered via Obsidian hotkeys executed with cursor-only selection before the fork could expand it. Additionally, commands invoked viaexecuteCommandById(command palette, toolbar buttons, other plugins) bypassed the DOM event path entirely. Spike test confirmed:editor:toggle-numbered-list,editor:toggle-bullet-list,editor:toggle-bold, andeditor:indent-listall affected only 1 line regardless of invocation method. Fixed by wrappingapp.commands.executeCommandviaaround()to temporarily expand the CM6 selection to the full linewise range fromvim.selbefore any Obsidian command executes, then restoring cursor-only after. Covers all invocation paths: hotkeys, command palette, toolbar, and programmaticexecuteCommandById. (#41)- Plugin:
src/vim/visual-line-command-fix.ts—installVisualLineCommandFix()wrapsapp.commands.executeCommandusing the existingaround()utility (safe for multi-plugin stacking); installed inonload(), cleaned up inonunload() - Spike test:
test/specs/spikes/spike23-visual-line-hotkey-commands.e2e.ts— 10 tests verifying direct command, hotkey, and selection state behavior
- Plugin:
[0.32.0] - 2026-07-05
Added
- Select mode (
gh/gH/g<C-h>) — Vim select mode where typing replaces the selection and enters insert mode.ghenters charwise,gHlinewise,g<C-h>blockwise.<C-g>toggles between visual and select mode.<BS>deletes the selection. Matches Neovim behavior. (#45)- Fork:
enterSelectMode,toggleSelectMode,preventReselectactions invim.js;selectModeflag on vim state;'select'context for keymap dispatch with visual fallback;gvpreserves and restores select mode vialastSelection - Fork:
:smap,:snoremap,:sunmap,:smapclearex commands for select-mode-specific mappings - Fork:
selectmodeoption (set selectmode=cmdmakesv/V/<C-v>enter select mode);keymodeloption (accepted, shifted cursor key behavior deferred) - Plugin: status bar shows
SELECT,data-vim-mode="select", powerline CSS with::aftertriangle, Style Settings entries - 16 fork browser tests, 5 Neovim golden test cases, 3 e2e tests
- Fork:
- Virtual Replace mode (
gR) — replace mode that operates on screen columns instead of byte positions. TAB-aware virtual column math with replace stack for<BS>restore.<Insert>toggles between virtual replace and insert mode. (#45)- Fork:
virtualReplaceCharandvirtualReplaceBackspaceadapter methods incm_adapter.ts;virtualReplaceflag andreplaceStackon vim state;{mode: "vreplace"}mode change event - Plugin: status bar shows
V-REPLACE,data-vim-mode="vreplace", powerline CSS, Style Settings entries - 10 fork browser tests, 3 Neovim golden test cases, 2 e2e tests
- Fork:
- Visual Line / Visual Block mode indicators — status bar now distinguishes
V-LINEandV-BLOCKfromVISUAL. Uses the fork’s existingsubModeevent field. (#45)- Plugin: mode-tracker maps
subMode: "linewise"→visualLine,"blockwise"→visualBlock;data-vim-mode="v-line"/"v-block"; powerline CSS + Style Settings entries - 3 e2e tests
- Plugin: mode-tracker maps
- Command-line and Search mode indicators — status bar shows
COMMANDwhen:prompt is open andSEARCHwhen/or?prompt is open. Detects dialog type via DOM text node inspection of the fork’s"dialog"event. (#45)- Plugin:
dialogHandlerin mode-tracker withpreDialogModetracking for restoration on dialog close;getDialogPrefix()walks DOM child nodes;data-vim-mode="command"/"search"; powerline CSS + Style Settings entries - 5 e2e tests (including rapid
:→Esc→/→Esccycling)
- Plugin:
- Insert-Normal mode indicator — status bar shows the configured insert-normal prompt (default
NORMAL) when<C-o>is pressed in insert mode, then returns toINSERTafter one command. (#45)- Plugin: mode-tracker detects
subMode.startsWith('ctrl-o')→insertNormal;data-vim-mode="insert-normal"; powerline CSS - 2 e2e tests
- Plugin: mode-tracker detects
- All 11 mode prompts configurable — mode prompt text for all modes (normal, insert, visual, v-line, v-block, replace, select, v-replace, command, search, insert-normal) is configurable via Settings UI and vimrc (
let g:mode_prompt_visual_line = "VL", etc.)- Plugin:
ModePromptsinterface expanded; settings UI entries for all modes; vimrcVIMRC_MODE_MAPwith snake_case → camelCase mapping;RELOAD_KEYSupdated
- Plugin:
- Configurable which-key popup delay — the delay before the which-key popup appears is now configurable via Settings → Vim Motions → Which-key hints → Which-key popup delay or
set whichkeydelay=<ms>(aliaswkd) in vimrc. Range 0–2000ms, default 500ms. Set to0for instant display. Once the popup is visible, subsequent keystrokes update it instantly — the delay only applies to the initial appearance. Single-key commands that resolve immediately never trigger the popup regardless of delay setting.src/settings.ts: addedwhichKeyDelay: numbertoVimMotionsSettings(default 500), added toRELOAD_KEYS, added number input control in “Which-key hints” groupsrc/vimrc/loader.ts: addedwhichkeydelay/wkdtoKNOWN_SET_OPTIONS(number, 0–2000)src/ui/which-key.ts: replaced hardcodedSHOW_DELAYwith configurableshowDelayconstructor parameter;onKeyPressGeneralupdates overlay immediately when already visible instead of restarting delay; extractedshowCompletionsIfPartial()helpersrc/ui/global-which-key.ts: same pattern — configurable delay, instant updates when overlay already visiblesrc/main.ts: passessettings.whichKeyDelayto bothWhichKeyOverlayandGlobalWhichKeyOverlayconstructors
Fixed
<C-o>in replace mode returns to insert instead of replace —oneNormalCommandnow saves the pre-Ctrl-O mode state and returns to the correct mode (insert, replace, or virtual replace) after the single normal command. Uses_suppressModeSignalto prevent a spurious{mode:"normal"}event, emitting{mode:"normal", subMode:"ctrl-o"|"ctrl-o-replace"|"ctrl-o-vreplace"}instead. (#45)- Fork:
insertModeReturnArgson vim state;_suppressModeSignalflag inexitInsertMode - 5 fork browser tests, 2 Neovim golden test cases, 2 e2e tests
- Fork:
Rmode<BS>does not restore original character — regular replace mode now maintains a replace stack (same mechanism as virtual replace).<BS>restores the original character under cursor, matching Neovim behavior. Previously,<BS>only moved the cursor left. (#45)- Fork:
handleReplaceModeInputpushes original chars toreplaceStackbefore overwriting; BS pops and restores with explicitsetCursorfor correct positioning - 4 fork browser tests
- Fork:
- Replace/vreplace character I/O only works through DOM events — unified replace mode character handling from
index.ts(DOM-only path) intovim.js(handleReplaceModeInput).Vim.handleKeyis now authoritative for all replace-mode operations — programmatic dispatch, macro replay, and dot-repeat work correctly through both paths. (#45)- Fork:
handleReplaceModeInputinvim.jscalled fromhandleKeyInsertModematch.type == 'none'branch;virtualReplaceChar/virtualReplaceBackspaceadapter methods; removed overwrite block and helpers fromindex.ts - 7 fork browser tests (overwrite, BS restore, dot-repeat, macro replay, Ctrl-H)
- Fork:
Documentation
docs/configuration/status-bar.md: lists all 11 mode indicators, alldata-vim-modeattribute values, all CSS variables, all vimrc directives, fork mode requirement calloutdocs/configuration/settings.md: all 11 mode prompt settings with vimrc equivalentsdocs/guides/style-settings.md: all 20 powerline CSS variables (bg + fg for 10 modes)docs/reference/keybindings.md: select mode (gh,gH,g<C-h>,<C-g>,gV) and virtual replace (gR) sectionsdocs/reference/known-limitations.md: select mode and virtual replace mode limitationsKNOWN_LIMITATIONS.md:selectmode=mouseCM6 limitation,selectmode=key/keymodel=startseldeferred, East Asian Width,gRnewline behaviorDIFFERENCES.md(fork): 7 new sections covering select mode, virtual replace, replace stack, unified char handling, Ctrl-O fix, mapping commands, type changes
[0.31.0] - 2026-07-04
Fixed
- Obsidian commands (Tab/indent, formatting toggles) only affect cursor line in visual-line mode — when vim didn’t handle a key in visual-line mode, the event propagated to Obsidian with a cursor-only CM6 selection. Obsidian’s commands (
editor:indent-list,editor:toggle-bold, etc.) only saw one line instead of the full visual selection. Fixed by temporarily expanding the CM6 selection to the full linewise range before the event propagates, then restoring cursor-only via microtask after Obsidian’s command executes. (#41)- Fork:
handleKeyinindex.tsnow expands CM6 selection on unhandled keys during visual-line mode and restores cursor-only viaPromise.resolve().then()
- Fork:
[0.30.0] - 2026-07-04
Added
- User-configurable global key mappings (
gmap/gnoremap/gunmap) — non-editor key bindings (graph view, canvas, PDF, reading mode, file explorer, empty workspace) can now be customized via.obsidian.vimrc. Previously, all non-editor bindings were hardcoded. The<leader>key is shared with editor mappings. (#43)gmap <leader>f :obcommand switcher:open— bind<leader>fto open the quick switcher in non-editor viewsgnoremap <leader>s :sidebar left— functionally identical togmap(accepted for vim syntax familiarity)gunmap H— remove the defaultH → previous tabbinding (key propagates to Obsidian)- Right-hand side supports
:obcommand <id>for Obsidian commands and:<ex-command> [args]for global ex commands - User bindings override defaults;
gunmapremoves any binding (user or default) - Count prefix support:
5jscrolls 5 lines,3gtgoes to tab 3 (matching existing behavior) - New files:
src/workspace/global-mapping-registry.ts(registry with prefix-matching resolver),src/workspace/global-defaults.ts(default binding table) - Refactored
src/workspace/global-key-handler.tsfrom 770-line hardcoded state machine to 255-line table-driven dispatch viaGlobalMappingRegistry - E2E tests:
test/specs/gmap.e2e.ts(12 tests),test/specs/gmap-vimrc.e2e.ts(9 tests)
- Global which-key overlay — non-editor key sequences now show a which-key popup after 500ms, displaying available completions. Pressing
<C-w>showsh/j/k/l/v/s/c/q/owindow commands. Controlled by the existingwhichKeyModesetting (off/leader/all).- New file:
src/ui/global-which-key.ts—GlobalWhichKeyOverlayclass, shares CSS with editor which-key - Reuses
vim-motions-which-keyCSS classes fromstyles.css(no CSS changes needed) - Popout window support via
Documentparameter tracking - Dismiss on sequence completion, timeout, or focus change to editor
- New file:
- Global which-key labels (
gwhichkeylabel/gwhichkeygroup) — label global bindings for the non-editor which-key overlay, independent from editor which-key labelsgwhichkeylabel <leader>f Open file— shows “Open file” instead of the raw command IDgwhichkeygroup <leader> +leader— groups<leader>*bindings under a named prefix
:gmapex command — lists all active global bindings with source (default/user) in a modal. Available in both editor and non-editor:command contexts.executeGlobalExCommandhelper — exported fromglobal-ex-command.tsfor programmatic ex command dispatch without opening the modal UI
Documentation
docs/configuration/vimrc.md: addedgmap/gnoremap/gunmap/gwhichkeylabel/gwhichkeygroupto supported commands table, added “Global key mappings” section with full syntax and examplesdocs/features/workspace-navigation.md: added “Customizing global bindings” sectiondocs/configuration/which-key.md: updated modes to note non-editor overlay support, added “Global (non-editor) labels” sectiondocs/reference/keybindings.md: added:gmapex command, added customization note to non-editor bindings section
[0.29.0] - 2026-07-03
Fixed
- Visual-line cursor lands inside widget decorations in Live Preview — when entering visual-line mode (
V) at a non-zero column (e.g., cursor onain- a) and moving down to a line with a checkbox (- [ ] d), the cursor positionsel.head.chwas preserved from the starting column. In Live Preview,[ ]is replaced by a checkbox widget viaDecoration.replace; placing the cursor inside this replaced range caused the visual-line highlight to disappear. Fixed by always using column 0 for the cursor-only CM6 selection in visual-line mode, matching Neovim’s behavior. (#41)- Fork:
updateCmSelectioninvim.jsnow usescm.setCursor(sel.head.line, 0)instead ofcm.setCursor(sel.head.line, sel.head.ch) - 2 new Neovim golden comparison test cases:
Vfrom mid-column +j+dwith checkbox content,Vfrom mid-column +2j+ycursor at col 0
- Fork:
Documentation
- Added Quartz-powered documentation site at saberzero1.github.io/motions with full feature reference, getting started guide, and changelog.
[0.28.0] - 2026-07-03
Fixed
- Async motion callback exits visual-line mode — when an EasyMotion async motion resolved in visual-line mode, the
.then()callback calledupdateCmSelection(cm)outside of acm.operation()context. With cursor-only CM6 selection,handleExternalSelectiondetectedvisualMode && !somethingSelected()and exited visual mode. Fixed by wrapping the callback’supdateCmSelectioncall incm.operation()withisVimOp = true, matching the protection used by all other vim operation entry points. (#41)- Fork: async motion visual mode branch in
vim.jsnow wrapsupdateCmSelectionincm.operation()withisVimOp = true - Test:
easymotion-visual.e2e.tsupdated to verify visual-line easymotion via register content (yank +getRegisterContent) instead ofgetSelection(), which returns empty with cursor-only CM6 selection
- Fork: async motion visual mode branch in
- Visual line selection overlap in Live Preview — visual-line mode (
V) rendered both the plugin’s custom full-line highlight decoration and the native CM6::selectionCSS simultaneously, causing a visible double-highlight. Fixed by adding a.cm-vimVisualLineclass to the editor scrollDOM when in visual-line mode and extending the::selectiontransparency rule to suppress native selection rendering in that mode. Charwise (v) and blockwise (Ctrl-V) visual modes are unaffected. (#41) - Visual-line cursor displacement over collapsed markup in Live Preview — navigating with
V+j/kon lines containing collapsed markup ([[wikilinks]],[text](url)) caused Obsidian to uncollapse the hidden content, reflowing the line and making the cursor appear to need extra steps. Root cause:updateCmSelectionset a spanning CM6EditorSelectionrange across the full line content; Obsidian’s Live Preview detects selection overlap withDecoration.replaceranges and reveals them. Fixed by setting a cursor-only CM6 selection (atsel.headposition) in visual-line mode — thelinewiseVisualHighlightViewPlugin already provides the visual highlight independently fromvim.sel, and operators (y/d/c) recompute their own selection at dispatch time. (#41)- Fork:
updateCmSelectioninvim.jsnow setscm.setCursor(sel.head)instead of a spanning range whenvim.visualLineis true - Fork:
joinLinesaction invim.jsnow reads fromvim.selinstead ofcm.getCursor('anchor'/'head')in visual mode, fixingV+Jregression from cursor-only selection - Fork:
replaceaction invim.jsnow reads fromvim.selinstead ofcm.getCursor('start'/'end')in visual mode, with line boundary expansion for visual-line; removed unusedselectionsvariable - Fork:
index.tsadds Ctrl+C special-case that copies linewise text fromvim.selwhensomethingSelected()returns false in visual-line mode - Fork:
index.tsadds.cm-vimVisualLineclass toggle inupdateClass() - Fork:
block-cursor.tsextends::selectionsuppression CSS selector to include.cm-vimVisualLine - Plugin:
styles.cssoverrides.cm-vim-linewise-selectionwithvar(--text-selection)for theme alignment (already present from 0.27.0) - 6 new Neovim golden comparison test cases:
V+j+ycursor position,V+2j+dmulti-line delete,Vkupward selection,Vjkround-trip,v→Vtransition,V→vtransition - 7 new e2e tests: visual-line yank with markup content, multi-line yank register verification,
gvafter visual-line yank,v→VandV→vmode transitions
- Fork:
[0.27.1] - 2026-07-03
Fixed
- Custom vimrc path setting missing from Obsidian 1.13+ settings — the “Custom vimrc path” text input was present in the legacy
display()rendering but missing from thegetSettingDefinitions()declarative API. On Obsidian 1.13+, users could not see or configure the custom vimrc path in settings. Added thevimrcPathtext control to the “Vimrc & key bindings” group ingetSettingDefinitions(), withaliasesfor settings search discoverability and adisabledpredicate gated onenableVimrc. (#34)
[0.27.0] - 2026-07-03
Added
- Vimium-style hint actions in non-editor views — hint mode now supports multiple actions via a key-tree dispatch when a non-editor view (graph, PDF, canvas, etc.) is focused.
factivates (click/focus),Fopens in a new pane,yfyanks the target’s URL or text to clipboard,dfcloses the target tab or pane. Count prefix works:3factivates three targets sequentially. In editor context,<leader><leader>h(unchanged) triggers hints with Ctrl/Cmd modifier during label selection upgrading to open-in-new-pane.src/ui/hint-mode.ts: refactored into action-dispatch architecture withHintTargettype classification (link/pane/tab/button/input/generic), four action functions (hintActivate/hintOpenNew/hintYank/hintClose),createHintActions()factory, count support viarequestAnimationFramerecursion, modifier-based action upgrade,el.isConnectedvalidation, clipboard fallbacksrc/workspace/global-key-handler.ts: addedY_PENDING/D_PENDINGstates toSeqStateenum,hintActionsconstructor parameter,f/F/y/ddispatch in IDLE and COUNT states,handleYPending/handleDPendinghandlers,chordText()updatessrc/main.ts:registerHintMode()→registerHintActions(),hintModeAction→hintActionsfield, stale hotkey closure fix (indirection pattern),reloadFeatures()reset, three new Obsidian commands- New Obsidian commands:
vim-motions:hint-open-new-pane,vim-motions:hint-yank,vim-motions:hint-close - E2E tests: 10 new tests covering non-editor
f/F/yf/df, modifier upgrade, escape, invalid sequence reset, command registration
Fixed
- Global key handler intercepts navigation keys in Obsidian settings modal —
j/k/g/z/:and other navigation keys were consumed by GlobalKeyHandler when the settings modal was open. Navigation keys are now suppressed when.modal-containeris detected in the DOM viaisModalOpen(). Hint actions (f/F/yf/df) still work in modals — they use a separateshouldInterceptHints()gate that does not check for modals. - Hint mode labels re-trigger instead of selecting label characters — pressing
fto activate hint mode, then typing a label character that is alsof, would re-trigger hint mode via GlobalKeyHandler instead of being captured by the label selection handler. Fixed by adding anisHintModeActive()flag (exported fromhint-mode.ts) that makes GlobalKeyHandler bail entirely during label selection. - Settings toggles not responding to hint activation — Obsidian’s toggle controls (
.checkbox-container, a<label>element) requiredpointerdown/pointerupevents beforeclickto trigger the toggle handler. Added full pointer event sequence dispatch for generic element activation. - Settings dropdowns cycling to wrong element on Obsidian 1.13+ — Obsidian 1.13+ adds hidden
<select class="dropdown is-measuring">shadow copies of every dropdown for layout measurement. These shadow selects have only 1 option and are positioned at the same coordinates as the real dropdown, causing hint labels to sometimes target the measurement copy. Fixed by filtering out elements with theis-measuringclass during target discovery. - Settings controls require Escape before re-activating hints — after activating a toggle or cycling a dropdown in the settings modal, focus remained on the control element, preventing GlobalKeyHandler from intercepting
ffor the next hint activation. Fixed by blurring the activated element (and any focused child) after activation when inside a.modal-container. - Dropdowns only focus but don’t change value —
<select>elements cannot be programmatically opened in Chromium. Changed activation behavior to cycle to the next option value and dispatch achangeevent, giving immediate feedback instead of requiring manual Arrow key interaction. - Broadened form control selectors —
STANDARD_SELECTORSnow includesinput:not([type="hidden"]):not([disabled]),textarea:not([disabled]), andselect:not([disabled])to ensure all visible form controls (text inputs, search bars, dropdowns) receive hint labels regardless of their Obsidian-specific parent structure. Removed redundant Obsidian-specific selectors that were subsets of the broader standard selectors. Changed.setting-item-control .checkbox-containerto.checkbox-containerto match toggles rendered by Obsidian 1.13+‘s declarative settings API outside the traditional.setting-item-controlparent.
Changed
- Scrolloff cap raised from 20 to 9999 — the
scrolloffsetting now accepts values up to 9999 (previously capped at 20), enabling the standard Vim pattern ofset scrolloff=999to keep the cursor vertically centered while scrolling. The Settings UI control has been changed from a slider to a validated number input field. Affects all four validation points: Settings UI (structured + manual rendering), vimrcset scrolloff=N/set so=N, and the vimdefineOptioncallback. The underlying CSSscrollMarginsimplementation was already uncapped. (#40)src/settings.ts: structured definition changed fromtype: 'slider'totype: 'number'withmax: 9999; manual rendering changed from.addSlider()to.addText()withtype='number',min='0',max='9999', integer clamping, and fallback to default 5 on invalid inputsrc/vimrc/loader.ts:scrolloffandsooption definitions updated frommax: 20tomax: 9999src/vim/options.ts:defineOptioncallback validation updated fromn <= 20ton <= 9999
Documentation
KNOWN_LIMITATIONS.md: added “Hint mode actions” section documenting the vimium-style key-tree, context split, modifier upgrade, target classification, settings gating, modal behavior, clipboard fallback, and stale target handlingKNOWN_LIMITATIONS.md: updated “Global workspace navigation” supported keys to include hint actions (f/F/yf/df)KNOWN_LIMITATIONS.md: updated “Scrolloff line height assumption” section to document the raised cap and centered-cursor patternREADME.md: updated hint mode section with vimium-style actions, non-editor key table, and new Obsidian commandsREADME.md: updated workspace keyboard control table with hint action keysREADME.md: updated scrolloff range from 0–20 to 0–9999 in number options table and settings list; updated scrolloff description to mentionset scrolloff=999for centered cursor
[0.26.0] - 2026-07-02
Fixed
- Stale jumpList markers crash vim state on document switch —
gg,G, and other motions withtoJumplist: truethrewRangeError: Invalid position N in document of length Mwhen switching between documents of different lengths (especially with PDF++ plugin). The global jumpList storedMarkerobjects with absolute offsets from the previous (longer) document. WhenjumpList.add()calledcurMark.find()on a stale marker,posFromIndexpassed the old offset todoc.lineAt()without bounds checking, crashing throughprocessMotion→processCommand→ thecm.operation()try-catch, which wiped and re-initialized vim state. Subsequent keystrokes fell through to default CM6 text insertion. (#18)- Fork:
posFromIndexnow clamps offset to[0, doc.length], mirroringindexFromPosbounds checking - Fork:
Marker.find()catches exceptions and returnsnullfor stale markers (all callers already handlenull) - Fork:
Marker.update()catchesRangeErrorfrommapPos()when marker offset exceeds the changeset’s starting document length, settingoffset = null - Plugin:
reloadFeatures()now callsvim.resetKeymap()to matchonload()behavior, closing a defense gap where 33 settings-triggered reloads could corrupt the keymap without recovery - 5 new fork tests (posFromIndex clamping, negative offset, valid offset, marker doc-shrink, gg/G with stale jumpList)
- 3 new plugin e2e tests (gg after doc switch, G after doc switch, gg/G after reloadFeatures on shorter doc)
- Fork:
- Visual line mode (V) highlight doesn’t match Obsidian theme — the linewise selection highlight used hardcoded rgba colors via the fork’s
EditorView.baseTheme, which didn’t adapt to Obsidian themes. The fork’s&light/&darkCSS variants never activated because Obsidian doesn’t addcm-dark/cm-lightclasses to.cm-editor. Added a CSS override instyles.cssusingvar(--text-selection)(Obsidian’s accent-derived selection color) at specificity 0-3-0, which beats both the fork’s base theme (0-2-0) and Obsidian’s code block background (0-2-1) without!important. (#38) - Visual line mode highlight invisible inside code blocks — the linewise selection
Decoration.line()class competed with Obsidian’sHyperMD-codeblock-bgclass on the same.cm-lineelement. The code block background (applied at specificity 0-2-1 via.cm-s-obsidian div.HyperMD-codeblock-bg) won the specificity fight. Fixed by the same CSS override above — specificity 0-3-0 beats 0-2-1. (#38) - Visual block select (Ctrl-V) on EOL displaces cursor rightward —
makeCmSelectionin the fork’s block mode branch added+1totoChfor inclusive selection without per-line clamping. When$(end-of-line) settoChto the actual line length,toCh + 1pushed the cursor one position past the last character. Fixed by clampingtoChandfromChto each line’s length inside the per-line loop, since each line in a block selection has a different length. The$motion’sInfinityreturn forchis preserved upstream — clamping only happens at the selection-building stage. (#38) - Formatting mark transaction filter corrupts visual selections — the
EditorState.transactionFilterinformatting-mark-fix.tssnapped cursor positions past formatting marks (**,*,`,~~,==) for all selection changes, including visual mode selections. When extending a visual selection across formatted text in Live Preview, the filter’ssnapRangefunction modified the selection head to a formatting mark boundary, causing the selection to jump or collapse unexpectedly. Fixed by adding arange.emptyguard that skips snapping for non-empty (visual) selections — the formatting mark correction is only needed for normal-mode cursor movement. (#38)- Fork:
makeCmSelectionblock mode now clampstoCh/fromChper-line vialineLength(cm, top + i) - Plugin:
formatting-mark-fix.tsskipssnapRangewhenrange.emptyis false - Plugin:
styles.cssadds.cm-editor .cm-line.cm-vim-linewise-selectionoverride withvar(--text-selection)fallback chain
- Fork:
- Visual block
$delete cursor deviation —<C-v>jj$dleaves cursor atch:1instead of Neovim’sch:0after deleting to EOL. This is a pre-existing cursor-after-block-delete positioning issue in the fork (content is correct, only cursor position differs). Registered as a known deviation intest/neovim/deviations.ts.
Documentation
KNOWN_LIMITATIONS.md: updated “Formatting mark cursor correction in Live Preview” section to document the visual mode bypassKNOWN_LIMITATIONS.md: updated “Block visual mode” section test coverage count (13 → 15 golden tests)DIFFERENCES.md(fork): added “Block visual EOL cursor clamping” section documentingmakeCmSelectionper-line clampREADME.md: updated recommended setup to mention theme-aligned visual line highlighting
[0.25.0] - 2026-07-02
Fixed
- Vim engine settings changed via Settings UI not taking effect — changing clipboard, tabstop, shiftwidth, expandtab, insertmodeescape, insertmodeescapetimeout, or textwidth in Settings → Vim Motions → Vim engine only persisted the value to disk but did not push it to the vim engine via
vim.setOption(). The setting appeared to save but had no effect until Obsidian was reloaded. The same settings worked correctly when set via.obsidian.vimrcbecause the vimrc loader explicitly callsvim.setOption(). Fixed by addingvim.setOption()calls to each vim engine setting’sonChangehandler insrc/settings.ts. For clipboard and textwidth, the module-level state helpers (setClipboardOption,setTextwidth) are also called to match the vimrc loader’s behavior. (#39)
Added
- Style Settings integration — powerline status bar colors and jump label colors are now customizable via the Style Settings plugin. The
styles.cssfile includes a/* @settings */block exposing 12 color pickers with separate light/dark mode defaults: powerline background and text for each vim mode (normal, insert, visual, replace), EasyMotion label background/text, and hint mode label background/text. The plugin triggersparse-style-settingson load/unload so Style Settings discovers the configuration automatically. Users without Style Settings are unaffected — the existing CSS variable fallback chain (--vim-pl-*-bg→ Obsidian theme variable → hardcoded fallback) continues to work identically. (#37) - Global workspace navigation — workspace keyboard commands (
<C-w>h/j/k/l,gt/gT,H/L,:q, scroll keys, etc.) now work across ALL Obsidian views, not just markdown editors. When a non-editor view (PDF, graph, canvas, image, backlinks, etc.) is focused, a capture-phase keydown handler intercepts workspace-relevant keystrokes and dispatches them via Obsidian’s command system. When a CodeMirror editor is focused, codemirror-vim handles everything as before — no regression. (#35)- Navigation:
<C-w>h/j/k/l(focus pane),<C-w>v/s(split),<C-w>c/q(close),<C-w>o(close others),gt/gT(next/prev tab),Ngt(Nth tab),H/L(prev/next tab),Ctrl-o/Ctrl-i(history back/forward) - Scrolling:
j/k(line scroll),gg/G(top/bottom),Ctrl-d/u(half page),Ctrl-f/b(full page), with count prefix support (5j= 5 lines) - Ex command line:
:opens a standalone command modal with tab-completion for 34 globally-safe ex commands (:q,:wq,:e {file},:sp,:vs,:ob {cmd}, etc.) - Chord display: pending keystrokes (
<C-w>,g,3) shown in status bar viasetGlobalChord()onVimModeTracker - Sequence timeout: multi-key sequences reset after 1000ms (matches vim’s
timeoutlen) - Popout window support: handler installed on all windows via
workspace.on('window-open') - Input suppression: keys not intercepted in text inputs, contentEditable, modals, command palette, or IME composition
- Scroll target detection: DOM tree-walking finds the largest scrollable container in arbitrary views (same approach as obsidian-vim-keynav)
- New file:
src/workspace/global-key-handler.ts—GlobalKeyHandlerclass withshouldIntercept(),SequenceStateMachine, scroll target detection - New file:
src/ui/global-ex-command.ts—GlobalExCommandModalextending Obsidian’sSuggestModal src/vim/mode-tracker.ts: addedsetGlobalChord(text)method for non-editor chord displaysrc/workspace/navigation.ts: exportedexecuteCommand()for reuse by global handler- E2E test suite
test/specs/global-nav.e2e.tswith 15 tests covering navigation, scrolling, ex commands, input suppression, sequence timeout, and no-regression
- Navigation:
H/Ltab switching in non-editor views — repurposesH/L(screen top/bottom in editors) for previous/next tab navigation when a non-editor view is focused, matching obsidian-vim-keynav conventionsCtrl-o/Ctrl-ihistory navigation in non-editor views — maps toapp:go-back/app:go-forwardwhen no editor is focused (in editor context, codemirror-vim uses these for the within-file jumplist)- Custom vimrc file path — new setting to load vimrc from a custom vault path instead of the default
.obsidian.vimrc. Useful when using Obsidian Sync, which skips dotfiles. The setting provides file-suggest autocompletion filtered to*.vimrcfiles in the vault. Leave empty to use the default.obsidian.vimrc. Changing the path triggers a full vimrc reload. (#34)src/settings.ts: addedvimrcPath: stringtoVimMotionsSettingsinterface and defaults, addedvimrcPathtoRELOAD_KEYS, added file-suggest text input below the “Load .obsidian.vimrc” togglesrc/ui/vimrc-file-suggest.ts: new file —VimrcFileSuggestextends Obsidian’sAbstractInputSuggest<TFile>to autocomplete vault files ending in.vimrcsrc/vimrc/loader.ts:getVimrcPath(),loadVimrc(), andresolveLeaderKey()accept optionalcustomPathparametersrc/main.ts: passessettings.vimrcPathto loader functions- E2E test suite
test/specs/vimrc-custom-path.e2e.tswith 7 tests covering custom path loading, default fallback, non-existent path resilience, and non-dotfile path for Sync compatibility
Changed
<C-w>o,:only,:qa,:xallnow close ALL view types — previously filtered bygetViewType() === 'markdown', leaving PDFs/images/etc. open. Now closes all tabs regardless of view type, matching Neovim behavior. Same change applied tog<C-t>(goto Nth tab) which now counts all leaves, not just markdown.
Documentation
KNOWN_LIMITATIONS.md: added “Global workspace navigation” section documenting Ctrl-d/f/b Obsidian hotkey prerequisite and scroll target limitations; updated “Vimrc hot-reload” section to note that vim engine settings now hot-reload via Settings UIKNOWN_LIMITATIONS.md: updated “Vimrc hot-reload” section to document custom vimrc path behaviorREADME.md: updated workspace keyboard control section with global navigation commands, scrolling keys, and standalone ex command line; added hotkey unbinding note for Ctrl-d/f/b; updated Vim engine settings section to note immediate hot-reloadREADME.md: updated powerline status bar description to mention Style Settings support; updated label colors description to mention Style SettingsREADME.md: updated vimrc support section, settings list, and quality of life to document custom vimrc path settingstyles.css: added/* @settings */block with Style Settings variable bindings; powerline CSS variables moved from local definitions to inline fallbacks for Style Settings compatibility
[0.24.0] - 2026-07-01
Changed
- Formatting mark cursor fix rewritten — replaced
RangeSetBuilder.prototypemonkey-patching with a CM6EditorState.transactionFilterthat corrects cursor positioning near formatting marks in Live Preview. The new approach walks the Lezer syntax tree to identify formatting mark nodes and snaps cursor endpoints that land inside mark ranges to the nearest boundary. Includes end-of-line boundary handling to prevent cursor oscillation when formatting marks extend to the line end (e.g.**he**with no trailing content). This eliminates conflicts with obsidian-latex-suite (#32) and fixes formatting marks being visible in live preview (#33). The'always'formatting mark mode has been removed (users are migrated to'cursor').
Added
- Block visual insert (
I/A), change (c/C) —CTRL-Vblock visual mode now supportsI(insert at left column),A(append at right column),c(change block), andC(change to EOL) with multi-cursor editing on all selected lines. Text appears on all lines in real-time as you type (unlike Neovim, where text only appears on the primary cursor until<Esc>). Short lines that don’t reach the block column are skipped, matching Neovim behavior. Dot-repeat (.) works for block insert operations. Block visual delete (d), yank (y), paste (p/P), indent (>/<), replace (r), and case toggle (~) were already working.- Fork:
enterInsertModepreserveswasInVisualBlockbeforeexitVisualModeclears the flag - Fork:
selectForInsertskips lines shorter than the block column instead of clipping - Fork:
operators.changeadds avim.visualBlockpath for block change and block change-to-EOL - Fork:
exitInsertModepositions cursor at the block’s left column viablockInsertLeftinstead of the standardch - 1, matching Neovim’s cursor placement after blockA - Fork:
makeCmSelectionblock mode treatsfromCh === toCh(zero-width block) the same asfromCh < toCh, fixingCon zero-width blocks - Fork:
repeatInsertModeChangesusesblockInsertLeftfor cursor placement after dot-repeat instead of hardcoded+1
- Fork:
- Neovim golden comparison tests for block visual: 13 golden test cases in
test/specs/vim-builtin/visual-block-golden.e2e.tscovering insert, append, change, change-to-EOL, delete, case toggle, replace, short-line handling, block yank/paste, zero-width block C, zero-width block I, A cursor position, and upward selection - Spike test suite
test/specs/spikes/spike-block-insert.e2e.tswith 10 tests covering all block visual insert scenarios - Command index entries:
CTRL-V_I,CTRL-V_A,CTRL-V_c,CTRL-V_C,q,@,@@ - Neovim golden comparison tests for marks: 5 golden test cases in
test/specs/vim-builtin/marks-golden.e2e.tscoveringma/'a,`b,'.,'',`` - Neovim golden comparison tests for macros: 5 golden test cases in
test/specs/vim-builtin/macros-golden.e2e.tscoveringqa/@a,2@a,@@,3@a, insert replay - Expanded register golden tests: 3 new cases (
"Ayyappend,"0pnumbered register,"a/"bindependent) innormal-yank-putsuite - Expanded search/replace golden tests: 2 new cases (
:%sglobal,:2,3srange) inex-commands-builtinsuite - Formatting mark cursor golden tests: 3 new cases (
wthrough**,fpast**,ethrough backticks) innormal-motionssuite
Fixed
- Vimrc
whichkeygroup/whichkeylabelcommands crash on load —defineEx('whichkeygroup', 'wkg', ...)threwError: (Vim.defineEx) "wkg" is not a prefix of "whichkeygroup"becausedefineExrequires the short form to be an actual starting substring of the command name, not an arbitrary abbreviation. Same issue forwhichkeylabel/wkl. Fixed by changing the prefixes to valid substrings:whichkeygandwhichkeyl. User-facing vimrc syntax (whichkeygroup,whichkeylabel) is unchanged. Theset whichkeygrouping/set wkgoption alias (handled by a separateKNOWN_SET_OPTIONSpath) was already correct and unaffected. (#31) - Block visual mode deviations removed — all
CTRL-Vblock visual deviations intest/neovim/deviations.tshave been removed. Block insert/change now matches Neovim output with zero deviations: cursor position afterAexit is correct, short lines are skipped, and zero-width blocks work for all operators. - Golden recording infrastructure —
test/neovim/record-golden.tsnow sends<Esc><Esc>before each test case to reset Neovim to normal mode, preventing stale visual/insert mode state from leaking between test cases. This fixed 5 pre-existing incorrect golden values ing-commands.json(3 mode corrections) andvisual-mode.json(1 mode correction, 1 cursor + mode correction). - Search dispatch in test wrapper —
test/neovim/test-wrapper.tsnow detects/pattern\nand?pattern\nsearch sequences and dispatches the search + post-keys separately with a settle pause, improving reliability for search-dependent golden tests.
Documentation
KNOWN_LIMITATIONS.md: added “Block visual mode (CTRL-V) insert not supported (Fixed)” sectionDIFFERENCES.md(fork): added “Block visual insert (I/A), change (c/C)” section documenting all 6 fork changesREADME.md: added block visual insert/change to recommended setup section
[0.23.0] - 2026-07-01
Added
- Declarative settings API (
getSettingDefinitions) — implemented Obsidian’s 1.13.0+ declarative settings API with a version guard. On Obsidian 1.13.0+, plugin settings appear in Obsidian’s global settings search and use the new declarative rendering pipeline. On older versions, the existing imperativedisplay()method continues to work unchanged. NominAppVersionbump required.getSettingDefinitions()returns all settings organized into groups (Vim features, Vim engine, Jump navigation, Status bar, Mode prompts, Cursor shapes, Vimrc & key bindings, Leader key bindings, Which-key hints, Which-key group/command labels, Advanced)getControlValue()/setControlValue()overrides handle dot-notation keys for nested settings (modePrompts.normal,cursorShapes.insert), clear vimrc overrides on user change, and triggerreloadFeatures()for settings that require it- Vimrc-overridden settings are disabled via
disabled: () => isOverridden(key)predicates - Complex sections (leader bindings, which-key group/command labels, hotkey recorder) use
rendercallbacks delegating to the existing imperative rendering methods styles.css: added.vim-motions-hiddenutility class for render callback placeholder rows
Documentation
README.md: updated Settings section to note settings search compatibility on Obsidian 1.13.0+
[0.22.0] - 2026-06-30
Added
- Mobile support — the plugin is no longer desktop-only. Changed
isDesktopOnlytofalseinmanifest.json. EasyMotion and hint mode are disabled on mobile viaPlatform.isMobileguards because they depend onactiveDocument/activeWindow(desktop-only Obsidian globals). All other features (core vim, text objects, navigation, workspace commands, vimrc, status bar, tables, surround) work on mobile. (#30)src/main.ts: addedPlatform.isMobileguards to skip EasyMotion and hint mode registration on mobile (inonload,reloadFeatures, andreregisterLeaderFeatures)eslint.config.mts: added@codemirror/*and@lezer/*toimport/no-nodejs-modulesallow list —eslint-plugin-obsidianmdenables this rule whenisDesktopOnly: false
Fixed
- EasyMotion big-WORD regex crashes on iOS < 16.4 —
BIG_WORD_START_REused a lookbehind assertion ((?<=\s|^)\S) which is not supported on iOS versions before 16.4. Rewritten as a two-pass scanner: first checks start-of-line for non-whitespace, then finds\s\Stransitions mid-line. Theobsidianmd/regex-lookbehindlint rule (enabled whenisDesktopOnly: false) caught this. (#30) import/no-nodejs-modulesfalse positives on@codemirror/*imports —eslint-plugin-obsidianmdenables this rule whenisDesktopOnly: falseinmanifest.json. The existingimport/core-modulessetting does not affect this rule’s allow list. Added explicitallowentries for all@codemirror/*and@lezer/*packages to the rule configuration.- Configurable insert mode escape timeout —
set insertmodeescapetimeout=N(aliasimet, range 100–5000ms, default: 1000ms) controls how long the plugin waits between keystrokes when matching theinsertmodeescapesequence (e.g.jk). Matches Neovim’stimeoutlendefault of 1000ms. Previously hardcoded at 200ms — too tight for normal typing. Configurable via vimrc, Settings UI (Settings → Vim Motions → Vim engine → Insert mode escape timeout), or runtimeVim.setOption('insertmodeescapetimeout', 500). (#31) - Vimrc ↔ Settings parity — all plugin settings are now configurable via
.obsidian.vimrcin addition to the Settings UI. When vimrc is enabled (the default), vimrc values override the corresponding Settings UI values. Settings overridden by vimrc are shown as disabled controls in the settings tab with a note indicating the vimrc directive that set them (e.g., “Set by vimrc:set scrolloff=10”).- Boolean feature toggles via
set/set no:textobjects,navigation,hardwrap,listcontinuation,tablenav,workspacenav,easymotion,easymotiondimming,hintmode,statusbar,chorddisplay,powerline - Number options via
set <option>=<value>:scrolloff(0–9999),scanlimit(5–200),labelfontsize(10–20) - String options:
easymotionlabels,hintlabels - Enum options:
tablewidget(off/cursor/always),whichkey(off/leader/all),whichkeygrouping(flat/grouped) - Mode prompt customization via
let g:mode_prompt_normal = "N"(and insert/visual/replace) - Which-key group labels via
whichkeygroup <leader>t Table— name key prefix groups in the which-key popup - Which-key command labels via
whichkeylabel <leader>w Save file— describe individual bindings in the which-key popup - Reverse-direction settings — clipboard, tabstop, shiftwidth, expandtab, insertmodeescape, and textwidth now have Settings UI controls (previously vimrc-only)
- Priority rule: vimrc values override Settings UI values when
enableVimrcis true. Overrides are in-memory only — the on-disk settings file always reflects UI-set values. Changing an overridden setting in the UI clears the override for the current session. - List merge: which-key group labels and command labels from vimrc are merged with labels configured in Settings. Vimrc entries appear as read-only rows; the “Add” button remains active for user additions. Vimrc wins on conflict.
sourcedirective fix: settings andguicursorin sourced vimrc files now propagate correctly (pre-existing bug whereonCursorShapeChangewas not passed to recursiveloadVimrcFilecalls)vimrcLoadingflag fix: the flag is now reset tofalseafter successful vimrc load, enabling runtime:setcommands to trigger immediatereloadFeatures()
- Boolean feature toggles via
- Vimrc
setcommand routing — all knownsetoptions are now handled directly in the vimrc loader via aKNOWN_SET_OPTIONSmapping table, callingonSettingOverridedirectly instead of relying ondefineOptioncallback dispatch throughvim.handleEx. This ensures reliable settings override regardless of codemirror-vim initialization order. Unknown options fall through tohandleExfor forward compatibility. - Spurious
defineOptioncallback prevention —registerVimOptionsnow uses aregisteredflag to preventdefineOptioncallbacks from firing during initial option registration (codemirror-vim callssetOption(name, defaultValue)internally duringdefineOption). Without this guard, every option with a truthy default would spuriously populatevimrcOverridesand triggerreloadFeaturesduring plugin startup. - E2E test suite
test/specs/vimrc-settings.e2e.tswith 11 tests covering boolean/number/string/enum option overrides, mode prompts, which-key labels, override tracking, and combined overrides set insertmodeescape=jknot working (frame-perfect timing required) — theInsertEscapeHandlerlistened tovim-keypressevents, which only fire for keys processed by codemirror-vim as vim commands. In insert mode, regular character keys bypass vim entirely and go through CM6’s text input pipeline — the handler never saw them. Rewrote to use DOMkeydownevents on the editor element, correctly intercepting keystrokes in insert mode. Also fixed theinsertmodeescapevim option not storing its value forgetOption()retrieval (callback returnedundefinedinstead of the stored value). (#31)dknot deleting in operator-pending mode —dk(delete current and previous line) was a no-op becausetableAwareMoveUpwas registered withcontext: 'normal', causing it to be filtered out in operator-pending mode. CM Vim’s keymap search then failed to fall through to the defaultkmotion. Removed the context restriction since the motion already handles operator-pending mode internally via itshasOperatorcheck.- Cursor snaps to formatting mark boundary in Live Preview — placing the cursor inside formatted text (
*italic*,**bold**,`code`,~~strike~~,==highlight==) would snap to the delimiter boundary instead of the intended position. Obsidian’s Live Preview usesDecoration.replace({})to hide formatting marks on inactive lines, creating zero-width gaps that cause CM6’s position mapping to collapse. Originally fixed by interceptingDecoration.replace({})viaRangeSetBuilder.prototype.addpatching. Later replaced with a CM6EditorState.transactionFilterapproach (see [Unreleased] section) due to conflicts with obsidian-latex-suite. %bracket matching skips brackets in strings and comments — the fork’sscanForBracketfallback now callsgetTokenTypeAt()for each bracket candidate and skips brackets inside"string"or"comment"tokens. Previously, positional stack counting would match a bracket inside a string literal. Note: in Markdown mode, Lezer does not classify double-quoted text as string tokens, so this primarily benefits languages with proper syntax trees.<</>>indent respectsshiftwidthandexpandtab— the fork’s indent operator now reads the vim optionsshiftwidthandexpandtab(viagetOption()) before falling back to CM6’stabSizeandindentWithTabs. Whenset shiftwidth=2orset expandtabis set in.obsidian.vimrc, the indent operator uses those values for both visual-block and line-by-line indentation.Vlinewise visual cursor at end of line instead of column 0 — linewise visual mode (V,Vj, etc.) now positions the cursor at column 0 of the head line, matching Neovim. The fork’smakeCmSelectionwas settinghead.ch = lineLength(line)for display, which placed the cursor at the end of the line. AViewPluginwithDecoration.linenow provides the full-line visual highlight independently of the CM6 selection head position.- Vimrc map re-application — vimrc key mappings are now re-applied 200ms after initial load as a safety net against CM Vim initialization timing. If the initial
applyVimrcMapscall runs before the CM6 vim extension has fully settled, the delayed retry ensures mappings take effect.
Changed
minAppVersionbumped to 1.2.3 — required forsetDisabled()API on settings controls (used to disable vimrc-overridden settings in the UI). Obsidian 1.2.3 was released March 2023.
Documentation
KNOWN_LIMITATIONS.md: replaced “Desktop only” section with “Mobile support” section documentingPlatform.isMobileguards and feature-by-platform compatibility matrixREADME.md: updated Requirements from “Desktop only” to “Desktop and mobile” with link to known limitationsKNOWN_LIMITATIONS.md: added “Insert mode escape” section documenting thekeydown-based handler, configurable timeout, and thevim-keypressevent limitation; updatedvi*single-character status to fixed via formatting mark cursor correction; updated%+ strings to note Lezer limitation in Markdown; updated<<unindent entry to note fork fix; removedVlinewise cursor deviation; updatednmap L $section with investigation findings; added “Formatting mark cursor correction” sectionREADME.md: addedinsertmodeescapetimeoutto number options table and vimrc example; added insert mode escape timeout to settings listDIFFERENCES.md(fork): added sections forscanForBracketstring/comment awareness, indent operatorshiftwidth/expandtabsupport, linewise visual cursor positioning with decoration-based highlight
[0.21.2] - 2026-06-29
Fixed
- Plugin fails to load when built-in Vim mode is enabled — three fork-only API methods were called unconditionally, but do not exist on Obsidian’s built-in Vim API. When built-in Vim mode is enabled (or when another plugin pre-installs
window.CodeMirrorAdapter.Vimwith the built-in API),getVimApi()returns the built-in Vim object and the calls throwTypeError: … is not a function. Addedtypeofguards to all three call sites and marked the methods as optional in theVimApitype definition. (#29)vim.resetKeymap()inonload()— prevented the plugin from loading entirelyvimApi.clearInputState(cm, 'pane-switch')in theactive-leaf-changehandler — crashed on every tab switch when a partial key buffer was pendingthis.vim.removeMapCommand(reg.keys)inVimRegistration.removeRegistration()— crashed during plugin unload or feature toggle when cleaning upmapCommandregistrations
[0.21.1] - 2026-06-29
Fixed
- Space-as-leader key mappings not matching in codemirror-vim —
Vim.map(' j', 'gj')andVim.mapCommand(' w', ...)stored literal space in the keymap (' j'), butvimKeyFromEventproduces'<Space>'on key press. ThecommandMatchstring comparison never found a match, so leader-prefixed sequences silently failed. The fork now normalizes literal spaces to<Space>in_mapCommand(bothkeysandtoKeys),unmap(), andremoveMapCommand(). Existing angle-bracket groups (<C-Space>,<S-Space>) are preserved. This is the root-cause fix for the space-as-leader issue — the 0.21.0 plugin-side fix (unmapDefaultBindingcentralization) was necessary but not sufficient without this keymap normalization. (#21) - Vimrc map commands registered twice —
nmap,nnoremap, and other map commands in.obsidian.vimrcwere processed once correctly viadeferredMaps(the plugin’s own parser) and then a second time viavim.handleEx()(codemirror-vim’s ex command parser). ThehandleExpath splits arguments on whitespace, sonmap <leader>j gjwith space as leader becamenmap j gj— a barej → gjmapping without the leader prefix. This double-registration was masked for non-space leaders (comma, backslash) because whitespace splitting doesn’t affect those characters. Addedcontinueafter thedeferredMaps.push()block, matching the pattern used by all other handled command types (let,source,set). (#21)
Documentation
DIFFERENCES.md(fork): added “Key string normalization formap/mapCommand” section documentingnormalizeKeyStringand the_mapCommand/unmap/removeMapCommandnormalization pointsKNOWN_LIMITATIONS.md: updated “EasyMotion leader key conflict” fixed section with fork-side key normalization details
[0.21.0] - 2026-06-29
Added
- Smart list continuation on
o/O— pressingoorOon a Markdown list line now automatically continues the list marker on the new line. Supports unordered lists (-,*,+), ordered lists (1.,1)), task lists (- [ ],- [x]), ordered task lists (1. [ ]), custom checkbox states (- [!],- [?],- [/], etc.), indented lists, blockquote lists (> -), and nested blockquotes (> > -). Ordered lists increment the number foro(below) and keep the same number forO(above). Checked tasks always continue with an unchecked[ ]. Lines inside fenced code blocks are excluded. Controlled by Settings → Vim Motions → Smart list continuation on o/O (on by default). Disable for plain Neovim behavior.- Fork: added
getAction(name)API to thevimApiobject for action introspection, enabling the save/restore pattern for built-in action overrides - Plugin: added
defineActionOverridemethod toVimRegistrationthat captures the original action before overriding and restores it on plugin unload — ensuringo/Orevert to default vim behavior when the plugin is disabled
- Fork: added
- Fork test count: 1690 (up from 1686, 4 new
getActionAPI tests) - E2E test suite
test/specs/open-line-list.e2e.tswith 35 tests covering all list types, indentation levels, blockquotes, nested blockquotes, code block exclusion, undo, and edge cases
Fixed
Oon first line after frontmatter behaves likeo— pressingOon the first content line below YAML frontmatter inserted the new line into the frontmatter region (swallowed by Obsidian’s properties UI) instead of above the current line. Fixed in both the fork and the plugin:- Fork:
newLineAndEnterInsertModeinvim.jscomparedinsertAt.line === cm.firstLine()— always false when frontmatter is present. Now scans past----delimited frontmatter to find the first editable line and usesinsertAt.line <= firstEditableas the boundary check. The insertion point uses{ line: insertAt.line, ch: 0 }instead of hardcodedfirstLine(), so it works for all line types (plain text, headings, etc.) with or without frontmatter. - Plugin: the smart list continuation override in
open-line.tshad the samecurLine === cm.firstLine()issue. AddedfirstEditableLine()helper with the same frontmatter scan, changed the boundary check tocurLine <= firstEditableLine(cm), and updated the insertion point to{ line: curLine, ch: 0 }.
- Fork:
- E2E regression tests for
o/Owith frontmatter:Oon unordered/ordered/task list after frontmatter inserts above,oafter frontmatter inserts below,Oon non-list line after frontmatter inserts above,oon non-list line after frontmatter inserts below,Oon second line after frontmatter uses normal insertion path gkon wrapped line after frontmatter jumps straight to properties — when the first line below the frontmatter wraps across multiple display lines,gknow correctly navigates through the wrapped display lines before entering the properties panel. Previously, thestuckAtBoundarycheck in the fork’sfindPosVtreated display-line movement within a wrapped line as “stuck” (same document line) and immediately firedfocusBefore. The check now also verifies that the cursor offset truly didn’t change (range.head === startOffset), distinguishing “cursor moved to a higher display line within a wrapped line” from “cursor is truly stuck at the frontmatter boundary.” (#25)let mapleader = " "(space) not working as leader key — space as leader now works regardless of which features are enabled. The default<Space>→lbinding in codemirror-vim’s keymap consumed the space keystroke before leader-prefixed sequences could accumulate. Previously,unmapDefaultBinding(leader)was only called insideregisterEasyMotion(), so the fix only applied when EasyMotion was enabled. The plugin now unmaps the leader key’s default binding centrally — after vimrc loading, inreregisterLeaderFeatures(), and inreloadFeatures()— so any key used as leader (space, comma, semicolon, etc.) works for all leader-dependent features (table manipulation, hint mode, settings leader bindings) even when EasyMotion is disabled. (#21)- Mislabeled “space as leader” e2e test — the
describe('space as leader')test block was loadinglet mapleader = ","instead oflet mapleader = " ", making it a duplicate of the comma test rather than a true space leader test. Fixed to use space, providing actual cross-platform regression coverage. - E2E regression tests for
gkwrapped-line frontmatter edge case:gknavigates display lines on wrapped first content line,gkenters properties on non-wrapping first content line,kenters properties from first content line
Changed
- Settings tab reorganized — settings are now grouped under section headings for easier navigation: Vim features (text objects, structural navigation, hard-wrap, smart list continuation, table navigation, table widget mode, workspace navigation), Jump navigation (EasyMotion, hint mode, shared label font size), Status bar (mode indicator, chord display, powerline, mode prompts), Cursor shapes, Vimrc & key bindings (vimrc toggle, leader key bindings), Which-key hints (mode, grouping, group labels), Advanced (scrolloff, multi-line scan range). Previously, settings appeared as an undifferentiated list with only a few headings.
- EasyMotion label characters — now exposed as a dedicated text field in the Jump navigation settings section. Previously only configurable by knowing the default value.
Documentation
KNOWN_LIMITATIONS.md: added “Smart list continuation and frontmatter” section documenting theOboundary fixDIFFERENCES.md(fork): added “Frontmatter-awareO(open line above)” section documenting thenewLineAndEnterInsertModefixREADME.md: updated smart list continuation description to mention frontmatter awarenessREADME.md: updated settings list to reflect new section grouping and orderingKNOWN_LIMITATIONS.md: updated “Properties navigation” section with wrapped-linestuckAtBoundaryedge case fixDIFFERENCES.md(fork): updated “Properties navigation” section withrange.head === startOffsetguard
[0.20.0] - 2026-06-29
Fixed
let mapleader = ","(comma) and other keys with default Vim bindings not working as leader for EasyMotion —unmapDefaultBindingnow passes{ includeDefaults: true }tovim.unmap(), so built-in codemirror-vim bindings (e.g.,→repeatLastCharacterSearch,;→ forward repeat) are actually removed before registering EasyMotionmapCommandmulti-key sequences. Previously,vim.unmap()silently skipped_isDefaultkeymap entries, meaning the default single-key binding consumed the first keystroke before the multi-key sequence (e.g.,,w) could accumulate. Space as leader was unaffected because the default<Space>binding uses angle-bracket notation which doesn’t collide with literal space incommandMatch. (#6)gg/Gand other keymaps intermittently stop working — comprehensive vim state hardening across the codemirror-vim fork and plugin to prevent keymaps from breaking until app reload. Root causes identified and fixed: stale normal-mode key prefix state persisting across focus changes, global singleton keymap corruption viaunmap()removing default entries, incompleteleaveVimMode()cleanup leaking insert-mode listeners, and async motion race conditions. (#18)- Fork: blur handler resets partial key prefixes — the CM6 ViewPlugin now registers a
blurlistener oncontentDOMthat callsclearInputState()when the editor loses focus in normal mode. A stale prefix likegno longer persists across tab switches or modal opens, preventing the next keystroke from being silently swallowed. - Fork:
leaveVimMode()cleanup hardened — now removes insert-modechange/keydownlisteners if the editor was destroyed while in insert mode, clears the globallastInsertModeKeyTimer, clearsvirtualPrompt, and resetsinputStatebefore nullingcm.state.vim. - Fork: default keymaps protected from
unmap()— default keymap entries are tagged with_isDefaultand a frozen snapshot is stored at module init.unmap()now skips default entries unless explicitly requested. NewVim.resetKeymap()API restores defaults from the snapshot while preserving user mappings.mapclear()updated to use the_isDefaultflag instead of fragile index-based partitioning. - Fork: async motion generation tracking —
_commandGenerationcounter on vim state prevents stale async motion callbacks from executing after a newer command has already run. Protects EasyMotion operator-pending mode (d+ easymotion) from race conditions. - Plugin: pane-switch state reset —
active-leaf-changehandler now clears pending vim input state on all editors when switching panes, preventing partial commands from leaking across editors. - Plugin:
resetKeymap()on load — callsVim.resetKeymap()during pluginonload()to ensure a clean keymap baseline on plugin enable/reload, recovering from any prior corruption in the same app session.
- Fork: blur handler resets partial key prefixes — the CM6 ViewPlugin now registers a
Added
- Which-key leader grouping — leader key bindings in the which-key overlay are now grouped by prefix key, matching Neovim’s which-key plugin behavior. When grouping is enabled (default), pressing the leader key shows collapsed groups (e.g.
t→Table (+11),\→EasyMotion (+17)) instead of listing every binding individually. Pressing a group key drills down to show only bindings within that group. Configurable via Settings → Vim Motions → Which-key leader grouping (grouped / flat). (#27)- Groups are sorted first in the overlay, followed by ungrouped single-key bindings
- Group rows are visually distinct (accent color, italic) via the
.vim-motions-which-key-groupCSS class - Grouping applies to all completions in “all partial keys” mode, not just leader-scoped bindings — any multi-key prefix (
g,z,[,], custom mappings) can be grouped - Drill-down works in both “leader key only” and “all partial keys” which-key modes
- Which-key group labels — configurable names for key groups in the which-key overlay. Prefix keys can be labeled (e.g.
\t→Table,gr→LSP) instead of showing the generic+N keystext. Built-in features register default labels (Table, EasyMotion) that can be overridden. Labels support<leader>token expansion (e.g.<leader>tresolves to the actual leader key +t). Configurable via Settings → Vim Motions → Which-key group labels. - E2E test suite
test/specs/vim-state-hardening.e2e.tswith 7 tests: blur prefix recovery,gg/Gafter plugin reload, keymap protection viaunmap(),resetKeymap()recovery after force-unmap,leaveVimModecleanup from insert mode - Fork unit tests: 10 new tests for async motion generation tracking (superseded motion discarded, superseded delete discarded), keymap protection (
unmapskips defaults,unmapremoves user mapping preserving default,unmap ggpreserves default,resetKeymaprestores after force-unmap,resetKeymappreserves user mappings,mapclearpreserves defaults),leaveVimModecleanup (clears input state, cleanup from insert mode) - Fork test count: 1672 (up from 1660)
Documentation
DIFFERENCES.md(fork): added sections for blur handler,leaveVimModecleanup hardening, default keymap protection (_isDefaulttagging,resetKeymap(),mapclear()update), async motion generation tracking,clearInputStateAPI exposureKNOWN_LIMITATIONS.md: added “Vim state hardening” section documenting the multi-layered defense against intermittent keymap breakageREADME.md: added “Improved vim state reliability” bullet to recommended setup section
[0.19.0] - 2026-06-27
Fixed
k/gkdo not enter frontmatter navigation — bothkandgknow enter the properties panel when the cursor is at the top of a note. Two fixes: (1) the fork’smoveByDisplayLineswas missing thefocusBeforecheck thatmoveByLinesalready had, and (2) the fork’sfindPosVfrontmatter detection only triggered when the cursor moved into the frontmatter region (pos.line < start.line), but when the properties widget replaced the frontmatter lines, the cursor couldn’t move up at all — now also triggers at the boundary (pos.line === start.line). Additionally, the plugin’stableAwareMoveUpmotion (which overrideskfor table separator skipping) bypassedfindPosVentirely — it now delegates tofindPosVwhen the target line is inside the frontmatter. (#25)gk/gjover headings resets cursor to column 0 —gk(andgj) no longer jumps to the beginning of the line when crossing Obsidian headings in live preview. Headings are rendered with larger fonts, making them visually taller. The fork’sfindPosVwidget-detection heuristic falsely treated the multi-line jump caused by the heading’s height as a skipped replaced widget (e.g. MathJax) and overrode the cursor position. The heuristic now checks for actual replaced/widget decorations (dec.point === true) before activating, and aposAtCoordsfallback corrects cases wheremoveVerticallymisresolves the goalColumn on decorated lines. (#26)
Added
gD— open link in new tab —gDopens the link under the cursor in a new tab, using the same bracket-aware link detection asgd. External URLs open in the browser. (#23)<C-w>gd/<C-w>gD— open link in split —<C-w>gdopens the link under the cursor in a horizontal split,<C-w>gDin a vertical split. Follows the Neovim<C-w>s/<C-w>vconvention (lowercase = horizontal, uppercase = vertical). (#23)- E2E tests for
gD,<C-w>gd,<C-w>gD: link-on-wikilink navigation (new tab, horizontal split, vertical split), no-op outside links, leaf count verification - E2E tests for
gk/gjover headings: cursor horizontal position preserved across single and multiple headings, symmetry betweengkandgj
Documentation
KNOWN_LIMITATIONS.md: updated “Properties navigation” section withk/gkfrontmatter fix andtableAwareMoveUpinteractionKNOWN_LIMITATIONS.md: addedgkfrontmatter entry to behavioral deviations tableDIFFERENCES.md(fork): updated “Properties navigation” section with boundary detection and dual-casefocusBeforelogicKNOWN_LIMITATIONS.md: updated “Visual line navigation and replaced widget decorations” section with heading-aware fix andposAtCoordsfallbackKNOWN_LIMITATIONS.md: updatedgj/gkwidgets behavioral deviation entry with heading decoration handlingREADME.md: addedgD,<C-w>gd,<C-w>gDto workspace keyboard control table
[0.18.0] - 2026-06-27
Fixed
- EasyMotion dimming not visible — the shade overlay (
.vim-motions-easymotion-shade) was invisible because it was a child of the zero-size absolutely-positioned wrapper div. The shade is now appended directly toscrollDOMas a sibling of the wrapper, so itsright: 0; bottom: 0resolves against the full editor dimensions. (#6) - EasyMotion labels overlapping on dense text — labels for adjacent targets (e.g.,
<leader><leader>won closely spaced words) now stack vertically instead of rendering on top of each other.renderLabels()tracks placed label bounding boxes and offsets new labels below any overlap. (#6) - EasyMotion labels on hidden text in Live Preview — word-start targets inside hidden markdown syntax (e.g., the URL portion of
[text](url)) no longer receive labels.filterVisibleTargets()deduplicates targets whosecoordsAtPos()resolves to the same pixel position, which occurs when multiple document offsets map to the boundary of a replaced decoration. (#6) - EasyMotion dimming setting required app reload — toggling Settings → Vim Motions → EasyMotion dimming now takes effect immediately. The
dimmingparameter was changed from a capturedbooleanto a() => booleangetter, so the shade state is read at motion invocation time instead of registration time.
Added
- Label font size setting — configurable font size for EasyMotion and hint mode labels via Settings → Vim Motions → Label font size (10–20px slider, default: 14). EasyMotion collision detection scales proportionally with the configured size.
- Label color customization via CSS — label colors are now overridable via CSS custom properties. EasyMotion:
--vim-motions-em-bg,--vim-motions-em-fg. Hint mode:--vim-motions-hint-bg,--vim-motions-hint-fg. All default to--text-accent/--text-on-accent.
[0.17.0] - 2026-06-27
Fixed
- Visual mode cursor displaced at end-of-line (regression) — exiting charwise visual mode (
v$<Esc>,vlll<Esc>) at end-of-line left the cursor one position past the last character. The fork’sexitVisualMode()calledclipCursorToContent()whilevim.visualModewas stilltrue, which allowed the cursor to land at the linebreak position; after clearing the flag, the cursor remained displaced. Fixed by clearing visual flags beforesetCursor. Also fixed a latent JS loose equality bug inmeasureCursor()wherefalse != "\n"evaluated tofalsedue to type coercion. (#15) - Leader key mappings not working via vimrc —
let mapleader = ","(or space, or any custom leader) in.obsidian.vimrcnow correctly re-registers EasyMotion, hint mode, table manipulation, and settings leader bindings with the new leader key. Previously, the initial backslash-leadermapCommandentries persisted in the keymap becauseVim.unmap()could not removemapCommand-created entries, andunmapDefaultBindingskipped non-special keys like comma. The fork now providesVim.removeMapCommand(keys)for clean removal, andVimRegistrationuses scoped leader binding tracking to selectively unregister stale bindings when the leader changes. (#21, #6) <C-w>workspace commands not working —<C-w>v,<C-w>h/j/k/l,<C-w>s,<C-w>c/q, and<C-w>onow work correctly when Obsidian’s default Ctrl+W hotkey is unbound. The fork’smatchCommandhad anidleentry for<C-w>in normal mode that consumed the key as a no-op before the second keystroke could arrive, preventing multi-key<C-w>Xsequences from matching. The fork now deprioritizesidlefull matches when more-specific partial matches exist (e.g.<C-w>v,<C-w>hregistered viamapCommand). Theidleentry still fires when no sub-commands are registered, preventing the keystroke from propagating to the browser. (#20)
Added
- Fork regression test
exit_visual_mode_cursor_clippingcoveringvlll<Esc>,vll<Esc>, andv$<Esc>cursor positioning - E2E tests for leader key mapping behavior: comma and space leader key mappings execute correctly via
Vim.handleKey, leader keys do not insert literal characters in normal mode, EasyMotion overlay appears with custom leader and old leader bindings are cleaned up - E2E tests for
<C-w>workspace commands:<C-w>v/<C-w>sverify leaf count increases (split created),<C-w>cverifies leaf count decreases (tab closed),<C-w>overifies other tabs closed,<C-w>h/j/k/lverify focus changes after split,<C-w>followed by invalid suffix (x) verifies the suffix does not execute as a standalone command, insert-mode<C-w>non-regression verifies delete-word still works
Documentation
KNOWN_LIMITATIONS.md: updated “Visual mode cursor displaced at end-of-line” section withexitVisualModeroot cause andmeasureCursorcoercion fixDIFFERENCES.md(fork): updated “Visual mode cursor positioning at EOL” section withexitVisualModeordering fix and strict equality fixKNOWN_LIMITATIONS.md: expanded “EasyMotion leader key conflict” fixed section with leader re-registration andremoveMapCommanddetailsKNOWN_LIMITATIONS.md: updated “<C-w>prefix conflict” section — removed codemirror-vim limitation framing, kept user-action requirement (unbind Obsidian’s Ctrl+W hotkey)DIFFERENCES.md(fork): added “removeMapCommandAPI” section documenting the new keymap removal methodDIFFERENCES.md(fork): added “Idle key deprioritization for multi-key sequences” section documenting thematchCommandfix
[0.16.0] - 2026-06-27
Added
- Cursor-aware table editing in Live Preview — replaced the table cell bridge approach with a custom table rendering system. Tables display as themed HTML when the cursor is outside and switch to raw Markdown when editing. All vim motions, operators, and text objects work naturally on table content. (#19)
- Custom
TableRenderWidgetrenders markdown tables as HTML using Obsidian’s CSS classes (cm-embed-block,markdown-rendered,table-wrapper,table-cell-wrapper) for full theme compatibility StateFieldprovidesDecoration.replacefor tables the cursor is NOT in; removes decoration when cursor enters- Table widget suppressor patches
RangeSetBuilder.prototype.addto suppress Obsidian’s interactive table widget - Default mode: “Cursor-aware” — rendered table when cursor is outside, raw Markdown when editing
- “Always raw” mode keeps tables as plain Markdown at all times
- “Off” mode restores Obsidian’s default interactive table editor
- Three-way setting: Settings → Vim Motions → Table widget in live preview
- Supports alignment markers (
:---,---:,:---:) in rendered tables
- Custom
- Vertical table cell navigation —
]r/[rmoves to the same column in the next/previous row, skipping separator rows - Table cell text objects —
i|/a|for operating on table cells with standard vim operators:i|: content between surrounding pipes (likei()a|: content plus the trailing pipe- Works with
d,c,y,v:di|deletes cell content,ci|changes it,yi|yanks it
- Table realignment —
:tablerealign(short::tablerea) ex command and<Leader>trmapping. Computes column widths across all rows, pads cells uniformly, and respects:---/---:/:---:alignment markers in separator rows. - Table auto-format on
|— CM6inputHandlerextension that realigns table columns when|is typed in insert mode. Typing||on a new line within a table generates a separator row (|---|---|). - Table manipulation keybindings —
<Leader>tprefix commands mapped to Obsidian’s built-in table commands, inspired by vim-table-mode:<Leader>tm— insert table<Leader>to/tO— add row below/above<Leader>tJ/tK— move row down/up<Leader>tdd— delete row<Leader>tiL/tiH— add column right/left<Leader>tL/tH— move column right/left<Leader>tdc— delete column<Leader>tr— realign table
- Table ex commands — 15 ex commands for table manipulation:
:tableinsert,:tablerowafter,:tablerowbefore,:tablerowup,:tablerowdown,:tablerowdelete,:tablecolafter,:tablecolbefore,:tablecolleft,:tablecolright,:tablecoldelete,:tablealignleft,:tablealigncenter,:tablealignright,:tablerealign - Internalized monkey-around —
src/util/around.tsprovides safe prototype patching with automatic removal, replacing the externalmonkey-arounddependency - E2E test suite expansion: 28 tests in
table-cell-bridge.e2e.ts(cursor-aware rendering, widget suppression,j/knavigation through tables, separator row traversal, post-edit navigation, theme class verification, alignment rendering), 24 tests intables.e2e.ts(cell navigation, vertical navigation, text objects, realignment)
Fixed
- Cursor stuck on table separator after insert-mode edit — after editing a table cell in insert mode, Obsidian’s async table handler repositions the cursor, preventing
kfrom crossing the separator row (|---|---|). Fixed with a customtableAwareMoveUpmotion that skips separator rows when moving up after a table edit. The motion detects the snap-back pattern and compensates by jumping two lines (over the separator) instead of one. Operator-pending context (dk) is excluded from the skip to preserve correct delete ranges. - Cursor-aware table rendering — the “Cursor-aware” mode now uses a custom read-only
TableRenderWidgetinstead of Obsidian’s interactive table widget. Tables render as themed HTML when the cursor is outside, with no async cursor snap-backs or state corruption.
Changed
- Replaced
TableCellBridgeapproach (per-cell vim bridge) with cursor-aware table rendering. The bridge approach required maintaining vim state across Obsidian’s cell-scoped editors; the new system suppresses Obsidian’s widget and provides its own themed read-only widget via aStateField. tableWidgetModesetting default:'cursor'(cursor-aware rendering)- Legacy
suppressTableWidget: booleansetting migrated:true→'always',false→'off'
Documentation
KNOWN_LIMITATIONS.md: updated table navigation section with new features (vertical nav, text objects, realignment, auto-format); documented cursor-aware mode architectureREADME.md: updated table navigation description for cursor-aware rendering; addedi|/a|to text objects table,]r/[rto navigation, table text objects section, auto-format docs,<Leader>trand:tablerealignto commands
[0.15.0] - 2026-06-26
Fixed
- Bundled fork not recognized by other plugins — ecosystem plugins that check
window.CodeMirrorAdapter.Vim(e.g. Outliner, obsidian-vimrc-support) could miss the bundled fork due to plugin load order or Obsidian overwriting the property after the bridge was installed. The bridge now uses a property descriptor (getter) instead of a plain assignment, so reads always return the fork’s Vim singleton regardless of timing. The bridge is also installed beforeregisterEditorExtension()for earlier availability, and properly cleaned up on plugin unload. (#17) Vim.enterInsertMode(cm)missing from fork API — ecosystem plugins (Outliner, obsidian-lineage) callVim.enterInsertMode(cm)to transition the editor into insert mode after custom actions. Obsidian’s built-invim.jsexposes this method but upstream@replit/codemirror-vimdoes not. The fork now exportsenterInsertMode(cm)on theVimsingleton, matching Obsidian’s API surface. Without this, plugins using the bundled fork would getTypeError: vim.enterInsertMode is not a function. (#17)
Changed
- Fork test count — 1630 fork tests passing (up from 1628). Added
api_enterInsertModetest verifying the new public API method.
Documentation
DIFFERENCES.md(fork): added “enterInsertModeAPI exposure” section documenting the Obsidian-specific API addition
[0.14.0] - 2026-06-26
Added
- Which-key for all partial keys — the which-key overlay now triggers on any partial key sequence (operators like
d,c,yand prefix keys likeg,z,[,]), not just the leader key. After a 500ms delay, a multi-column panel at the bottom of the editor shows available continuations. Configurable via Settings → Vim Motions → Which-key hints with three modes: off, leader key only, all partial keys (default: off).- Operator-pending mode (
d …) shows grouped next-key options: single-key motions directly (w,j,$), multi-key prefixes collapsed (i→ +N text objects,a→ +N text objects) - Partial prefix keys (
g …,z …) showgetCompletions()results from the fork’s keymap introspection API - Special keys (
<Left>,<C-n>, etc.) and insert-only entries are filtered out - Leader bindings from settings and vimrc are shown with friendly command names
- Overlay positioned at bottom of editor pane (not viewport), max 40% height, multi-column grid layout
- Operator-pending mode (
- E2E test suite
test/specs/which-key.e2e.tswith 31 tests covering all three modes (off/leader/all), settings hot-reload, leader registry integration, and fork API integration (getKeymap/getCompletions)
Changed
- Which-key setting —
enableWhichKeyboolean replaced withwhichKeyModedropdown ('off'|'leader'|'all'). Default changed from implicit leader-only to explicit'off'. - Which-key overlay rewritten —
WhichKeyOverlayclass generalized from leader-only to support any partial key sequence. UsesgetInputState()for operator-pending detection andvim.statusfor partial key chord display. DOM attachment changed fromeditorEl.parentElementtoview.contentElfor reliable positioning. VimStatetype fix —modefield changed from required'normal' | 'insert' | 'visual' | 'replace'to optionalstringto match runtime behavior (the field is only set by the CM6 ViewPlugin’s mode-change handler, not by the initial vim state).- Plugin deviations reduced —
test/neovim/deviations.tsreduced from 20 to 10 entries. 10 deviations removed after verifying the fork now matches Neovim:)sentence motion,di(multiline,dbcross-line,dwempty line,d2wcross-line,dgeempty lines,diwword boundary,da"trailing space,:joincursor,:globalcursor. - Fork test count — 17 new fork-level tests for async motion dispatch (6),
getKeymap()API (5), andgetCompletions()API (6). Total: 1628 fork tests passing. - Fork golden comparison — re-recorded 756 golden cases from Neovim 0.12.2 with per-step state capture. 476 pass, 0 unexpected diffs, 280 known deviations (down from 284). Fixed 3 duplicate test name collisions and empty
:sflag behavior.
Fixed
da"trailing space —da"onsay "hello world" endnow producessay end(single space) instead ofsay end(double space). The fork’sfindBeginningAndEndnow consumes adjacent whitespace after inclusive quote expansion, matching Neovim.:joincursor position —:joinex command now positions cursor at column 0 of the joined line, matching Neovim. Previously placed cursor at the join point.:globalcursor position —:g/pattern/dnow positions cursor at the last matched line after execution, matching Neovim. Non-destructive:gcommands leave cursor where the last sub-command placed it.- Empty
:sflag behavior —:swith no arguments no longer preserves the/gflag from the previous substitution. Only the first match on the line is replaced, matching Neovim. %string-awareness — updatedKNOWN_LIMITATIONS.mdto reflect that%is only partially fixed: the forward-seek check works, butfindMatchingBracketstill does positional counting without string awareness.- Which-key graceful degradation —
getInputState(),getKeymap(), andgetCompletions()calls in the which-key overlay now checktypeofbefore invocation, preventing errors when built-in vim mode is active (these APIs are fork-only). - Cursor shape settings in built-in mode — cursor shape dropdowns are now disabled with an explanatory message when Obsidian’s built-in vim mode is active (cursor shapes require bundled fork mode).
- g-commands golden data — corrected incorrect Neovim recordings for
g$(cursor ch:11→10, mode visual→normal) andguu(content unchanged→lowercased). Full vim-builtin e2e suite now passes 16/16.
Documentation
DIFFERENCES.md(fork): added “Keymap introspection API” section documentinggetKeymap()andgetCompletions()DIFFERENCES.md(fork): updated “Empty :s flag preservation” → “Empty :s uses default flags”, addedda"whitespace,:joincursor,:globalcursor sections, updated golden comparison statsKNOWN_LIMITATIONS.md: “Which-key overlay scope” section rewritten to reflect the new all-keys modeKNOWN_LIMITATIONS.md: updated%+ strings entry to “Partially fixed” with explanation of remainingfindMatchingBracketlimitationKNOWN_LIMITATIONS.md: addedda",:join,:global,:sempty entries to behavioral deviations tableKNOWN_LIMITATIONS.md: correctedvi*single-char status from “Fixed” to “Not fixed”AGENTS.md: updated fork test count (1421→1628) and golden comparison statsREADME.md: which-key description updated and settings list updated with new dropdown
[0.13.0] - 2026-06-26
Fixed
- Visual mode cursor displaced at end-of-line — in charwise visual mode (
v$,vlto EOL), the block cursor no longer renders one character past the visible line content. The fork’smeasureCursor()now uses the vim state (vim.visualLine,vim.visualBlock) to only apply the EOL cursor adjustment in charwise visual mode, preserving linewise (V) and blockwise (<C-v>) rendering. Verified against Neovim 0.12.2 golden comparison. (#15) set clipboard=unnamednot syncing to system clipboard —set clipboard=unnamed(orunnamedplus) in.obsidian.vimrcnow actually syncs yank, delete, and change operations with the system clipboard. Previously, the option was parsed and stored but never acted upon — only explicit"+yregister yanks reached the clipboard. Paste (p/P) also reads from the system clipboard when the option is set. (#16)
Added
- Surround operator (vim-surround) — complete vim-surround implementation with all standard features. Requires bundled fork mode. (#9)
- Core:
ds{target}(delete),cs{target}{replacement}(change),ys{motion}{replacement}(add),yss{replacement}(entire line), visualS{replacement}(selection) - Tag surround:
dst(delete surrounding tag),cst{replacement}(change tag),ysiw<tag>(surround with tag),cs"<tag>(delimiter to tag), visualS<tag>(selection with tag). Regex tag fallback for Markdown mode. - Function wrapping:
ysiwf+ name + Enter →name(text),ysiwFfor spaced variant →name( text ) - Newline variants:
cS,yS,ySS,gS— delimiters on separate lines with content indented one level deeper - Count support:
2ds)deletes 2nd-level surrounding bracket,2ysiw*repeats delimiter for Markdown bold (**word**),2ds*unbolds,2cs*~changes bold to strikethrough. Works with any quote-type delimiter (*,~,=,$). - Insert mode:
<C-G>s{char}inserts open delimiter, type content, close delimiter appended on Esc.<C-G>S{char}for newline variant. - Dot-repeat (
.) works for all surround commands including tags, functions, and multi-char delimiters - All bracket/quote targets with space rules, aliases (
b→),B→},r→],a→>), andt(tag) target - 1585 fork tests passing
- Core:
- E2E test suite
test/specs/surround.e2e.tswith 66 tests covering ds/cs/ys/yss/visual S, tags (dst/cst/ysiw), function wrapping (f/F), newline variants (cS/yS/ySS/gS), count support (2ds/2cs), Markdown pairs (2ysiw*/2ds*/2cs*~), insert mode surround ( s), dot-repeat, and edge cases
Documentation
DIFFERENCES.md(fork): added surround operators section with architecture (pendingInput buffer, tag finding, newline variants, count support, dot-repeat, insert mode surround, char-repeat for Markdown pairs)KNOWN_LIMITATIONS.md: “Surround operator scope” section — complete feature set documented with breaking changesREADME.md: surround keybinding table with all features (tags, functions, newlines, counts, Markdown pairs, insert mode)test/neovim-command-index.yaml: added 46-entry surround section (100% tested)
[0.12.0] - 2026-06-25
Fixed
- Visual line navigation skips block MathJax in live preview —
gj/gkandj/know navigate into rendered MathJax$$blocks line by line instead of skipping over them. The fork’sfindPosVdetects whenmoveVerticallyjumps over multiple document lines (indicating a replaced widget decoration) and steps one document line instead, allowing the cursor to enter the widget’s source range. Folded ranges are excluded from correction. (#14) da$on block math$$...$$deletes partially —da$on$$ a + b = c $$now correctly deletes the entire expression (producing empty string) instead of leaving$$. The$text object now uses smart disambiguation (same pattern asi*/a*): tries$$as delimiter first, falls back to$for inline math.di$on block math correctly produces$$$$.)sentence motion at end of text —)at the end of the last sentence no longer moves the cursor backward to the period; it stays in place, matching Neovim- Dot-repeat of
cw+ typed text —.aftercwcorrectly replays the inserted text (was a test infrastructure issue, not a vim engine bug) - Search
n/Nwrap-around —nafter/search correctly wraps to the first match when reaching the end of the document (was a test infrastructure issue) - Chord display not clearing on Escape — pending keystrokes (e.g.
d) in the status bar now clear when Escape is pressed. The mode tracker now listens tovim-command-donein addition tovim-keypressandvim-mode-change, catching the case where Escape cancels a partial command without changing mode or firing a keypress event. (#2) - Cursor text invisible in light mode — the character under the block cursor now uses
--text-on-accent(Obsidian’s contrast color) instead of the syntax-highlighted color. Previously, colored text (headings, links) under the cursor was the same hue as the cursor background, making it unreadable in light themes. (#12)
Added
- Per-mode cursor shapes — configurable cursor shape per Vim mode: block, bar, underline, or hollow. Defaults match Neovim (
guicursor): block for normal/visual, bar for insert, underline for replace/operator-pending. Configurable via Settings → Vim Motions → Cursor shapes or vimrcset guicursor=n:block,i:bar,v:hollow,r:underline,o:underline. Requires bundled fork mode. (#13) - E2E test suite
test/specs/widget-navigation.e2e.tswith 6 regression tests for gj/gk/j/k navigation through rendered MathJax$$blocks in live preview
Changed
i$/a$text objects now usecreateSmartDollarTextObject(tries$$first, falls back to$), matching the same disambiguation pattern asi*/a*withcreateSmartAsteriskTextObject
[0.11.0] - 2026-06-25
Fixed
- Visual selection highlight — visual mode selection is now visible when using the bundled fork. The fork toggles a
.cm-vimVisualclass and scopes its::selection { transparent }rule to non-visual modes only. (#10) - Properties navigation — pressing
kat the top of the document now navigates into the properties (YAML frontmatter) panel, matching built-in vim behavior. The fork’sfindPosVadapter detects whenmoveVerticallylands the cursor inside the frontmatter region and provides afocusBeforecallback that focuses the “Add property” button. (#11) - Latex Suite compatibility — bundled vim extension now registered at
Prec.highestso its keydown handler fires before Latex Suite’s handlers, preventing duplicate key consumption in large math blocks. (#11) - Empty
:sflag handling —:swith no arguments now uses default flags (no/g), replacing only the first match on the line, matching Neovim - Octal increment disabled — numbers with leading zeros (e.g.
007) now increment as decimal (008) instead of octal (010), matching Neovim’s defaultnrformats - Per-step golden comparison infrastructure — fork’s Neovim comparison now captures state after each key step (1504 steps at 100% coverage), revealing 23 previously hidden behavioral differences
- Golden recorder reliability —
redrawaftersetCursorprevents stale Neovim state; 80×24 viewport simulation viaset columns=80 lines=24enables accurate display-line motion recording zc/zofold commands — fold/unfold now use CM6’sfoldCode/unfoldCodedirectly instead of Obsidian’s incrementaleditor:fold-more/editor:fold-lesscommands, which operated globally by heading level rather than at the cursor position.zausestoggleFoldfor robust cursor-based toggling. (#8)
Changed
- Bundled vim extension registered at
Prec.highestfor correct key handler ordering with third-party plugins
[0.10.0] - 2026-06-25
Added
- Bundled codemirror-vim fork — when Obsidian’s built-in vim mode is disabled, the plugin provides a forked
@replit/codemirror-vimas a CM6 extension with Neovim-parity behavioral fixes. Awindow.CodeMirrorAdapter.Vimbridge ensures ecosystem plugins (obsidian-vimrc-support, vim-im-control, etc.) work transparently. - Async motion support — the fork’s
defineMotionnow accepts async functions returningPromise<Pos>, enabling EasyMotion to work natively as a motion instead of an action. Operator-pending (d/c/y+ easymotion) and visual mode (v+ easymotion) work through the standard vim dispatch. - Neovim golden comparison infrastructure in fork — 496/688 tests passing against headless Neovim, with per-step extraction, golden recording, and automated comparison (
npx tsx test/neovim/compare.ts). - E2E tests for operator-pending easymotion (
d/c/y+ easymotion w) - E2E tests for multiline bracket text objects (
di{/di[/di<across lines, same-line verification) - E2E tests for
%string-awareness,db/d2wcross-line whitespace,ddcursor column preservation,Jtrailing whitespace - Expected-failure test cases for 6 remaining fixable deviations (dw cursor, d2w scope, dge empty, db cross-line, % quoted brackets, N after search)
- Full vim-easymotion default motion set — all 17 default-mapped motions: find (
f,F,s,t,T), word (w,b,e,ge,W,B,E,gE), line (j,k), search (n,N) - Bidirectional easymotion variants —
easyMotionBdWord,easyMotionBdEndWord,easyMotionBdWORD,easyMotionBdEndWORD,easyMotionBdLine,easyMotionBdTillavailable as named actions for vimrc remapping - Repeat last easymotion motion —
easyMotionRepeataction replays the most recent easymotion jump - 2-character combo labels — SCTree algorithm assigns single-char labels to nearby targets and 2-char labels to distant targets when there are more targets than label characters (>26). Backspace resets after typing the first char of a 2-char label.
- Text dimming — non-target text is dimmed when easymotion is active, making labels more visible. Controlled by Settings → Vim Motions → EasyMotion dimming (on by default).
- Visual mode support — all easymotion motions work in visual mode.
v+ easymotion extends the character selection to the target,V+ easymotion extends the line selection. Uses CM6dispatch({ selection })to manipulate the selection range directly. - EasyMotion dimming setting —
easyMotionDimmingtoggle in settings UI - Spike test
test/specs/spikes/spike19-easymotion-visual.e2e.tsinvestigating CM Vim visual mode and operator-pending feasibility (6 questions answered) - E2E test file
test/specs/easymotion-comprehensive.e2e.tswith 22 tests covering cursor landing (word, char, line, ge/gE), 2-char labels, dimming, repeat, visual mode, and edge cases (empty document, single word, empty lines, non-existent char) - E2E test file
test/specs/easymotion-visual.e2e.tswith 4 tests covering visual mode overlay, charwise selection, linewise selection, and escape preservation - CSS classes:
.vim-motions-easymotion-shade(dimming overlay),.vim-motions-easymotion-label-firstand.vim-motions-easymotion-label-second(2-char label styling)
Fixed
ddcursor column preservation — cursor now stays at its original column after linewise delete (matching Neovim), instead of moving to first non-blank characterJtrailing whitespace — join now strips trailing whitespace from the current line before adding the join space, preventing double spacesdi{/di[/di<multiline — inner bracket text objects on multiline brackets now preserve the bracket lines (producinga{\n}binstead ofa{}b), matching Neovimdj/dkat document boundary —djon the last line anddkon the first line are now no-ops (matching Neovim), instead of deleting the content:scursor positioning — cursor after substitute now goes to first non-blank of the last affected line instead of column 0%string-awareness —%now aborts (no movement) when the first bracket candidate found via forward-seeking is inside a string token, matching Neovimdb/d2wcross-line whitespace — when a delete crosses a line boundary, the whitespace-only prefix before the cursor is now included in the deletion, matching Neovimdgeat document start —geat the start of the document is now a no-op instead of deleting the character under cursordgeon empty lines —dgeon double-empty-lines now deletes both lines (matching Neovim) instead of leaving one]ptab remainder —]pwithindentWithTabsnow preserves remainder spaces when indent doesn’t divide evenly by tabSize- EasyMotion visual mode — async motions now properly update visual selection head/anchor instead of just moving cursor
- EasyMotion escape dismissal — Escape overlay dismissal in e2e tests now uses real DOM events (
browser.keys) instead ofVim.handleKeywhich bypasses the DOM listener - Hint mode escape dismissal — same fix as EasyMotion
- Workspace test isolation — workspace tests now use
beforeEachwithloadSingleFileWorkspace()to prevent cascading failures fromgdnavigation - Settings reload Y/Q test — uses
Vim.handleKeyinstead ofbrowser.keysto avoid DOM event routing issues afterreloadFeatures() - Vim cursor styling — fork’s hardcoded
#ff9696cursor color replaced with Obsidian CSS variables (--interactive-accent,--text-on-accent) directly in the fork’sblock-cursor.tswith fallbacks for non-Obsidian environments - Settings notice — when Obsidian’s built-in Vim mode is enabled, the plugin settings tab shows a callout-style warning recommending to disable it, with an explanation of the fork’s benefits
dwon empty line cursor — cursor afterdwon an empty line before a whitespace-only line now positions atch:1instead ofch:0- Ambient type declarations —
src/types/codemirror-vim.d.tsprovides fallback types forvim(),getCM(), andVimwhen the fork’s build artifacts are unavailable (e.g. in the community scanner’s sandboxed environment)
Changed
- Recommended setup: disabling Obsidian’s built-in vim mode is now the recommended configuration. The plugin’s bundled fork provides Neovim-correct behavior, async motion support, and theme-aligned cursor styling that are not available with the built-in vim engine.
- EasyMotion architecture — EasyMotion motions are now registered via
defineMotion(async, returningPromise<Pos>) instead ofdefineAction. The capture-phase operator-pending interceptor (src/easymotion/operator-pending.ts) has been removed — operator-pending and visual mode work natively through the fork’s async motion dispatch. - EasyMotion module refactored from single
easymotion.ts(243 lines) into 6 focused files:register.ts(data-driven registration),targets.ts(direction-aware target finding),labels.ts(SCTree algorithm),overlay.ts(DOM rendering with dimming and re-render support),keypress.ts(key capture with 2-char narrowing),types.ts(interfaces) <leader><leader>w,<leader><leader>j,<leader><leader>fare now forward-only, matching vim-easymotion parity. Previously these scanned the entire visible viewport regardless of cursor position.registerEasyMotion()now accepts adimmingparameter and uses a data-drivenEASYMOTION_DEFSarray for registration instead of per-motion imperative codeshowOverlay()returns anOverlayHandlewithupdateLabels()for dynamic re-rendering during 2-char label narrowingwaitForLabel()replaceswaitForKey()as the primary label capture function, supporting multi-char labels, backspace reset, and narrowing callbacks- Removed
test/specs/easymotion-motions.e2e.ts— superseded byeasymotion-comprehensive.e2e.tswith correct async test patterns for char-input motions - Fork dependency —
@replit/codemirror-vimnow referenceshttps://github.com/saberzero1/codemirror-vim.gitinstead of a local file path, enabling CI/scanner environments to install without local checkouts reportUnusedDisableDirectivesset tooffin eslint config to avoid conflicts between local and scanner lint rule sets- Added
Obsidianto sentence-case brands list in eslint config
Documentation
KNOWN_LIMITATIONS.md: EasyMotion operator-pending rewritten — now uses async motions natively instead of capture-phase interceptorKNOWN_LIMITATIONS.md: added 8 behavioral deviation entries for fork fixes (ddcursor,Jwhitespace,di{}multiline,dj/dkboundary,:scursor,%strings,dbcross-line,dwcursor)KNOWN_LIMITATIONS.md: added “DOM keyboard events not routed after settings reload” and “EasyMotion visual mode label selection via DOM events” sectionsAGENTS.md: added codemirror-vim fork section with dual-vim architecture documentationREADME.md: added “Recommended setup” section explaining benefits of disabling built-in vimDIFFERENCES.md(fork): comprehensive rewrite documenting all behavioral fixes and infrastructure changesDIFFERENCES.md(fork): added widget-aware vertical navigation and per-mode cursor shapes sectionsKNOWN_LIMITATIONS.md: added “Visual line navigation and replaced widget decorations” sectionKNOWN_LIMITATIONS.md: added “Smart dollar disambiguation” section for$$vs$text object matching
[0.9.0] - 2026-06-23
Added
- Configurable multi-line scan limit — multi-line text objects (
i*,a*,i$, etc.) now have a configurable scan range via Settings → Vim Motions → Multi-line text object scan range (5–200 lines, default: 20). Users working with long-form documents can increase the limit to match delimiters spanning more than 40 lines. - Code block exclusion in delimiter scanning — the multi-line delimiter scanner now skips lines inside fenced code blocks (
```fences). Delimiters like**inside code blocks are no longer matched as text object boundaries. - E2E test for delimiter scanning across code block boundaries (
di*should not match delimiters inside fenced code blocks). - E2E test for
vi*on single-character content (*x*), documenting the codemirror-vim visual mode limitation.
Fixed
- Scrolloff dynamic line height — scrolloff margins now use
EditorView.defaultLineHeightto measure the actual line height instead of assuming 22px. The margin adapts automatically when the user changes font size or line height via CSS/themes. adjustRangeForVisualModeno longer produces zero-width selections for single-character text object ranges — the −1 head compensation is skipped when the range is exactly 1 character wide. (The underlying codemirror-vimmakeCmSelectionbug still preventsvi*on*x*from selecting correctly, butdi*on*x*now works as expected.)
Changed
getTextwidth()now reads directly from the plugin’s internaltextwidthValueinstead of queryingvimApiRef.getOption('textwidth'), avoiding a dual-source ambiguity where CM Vim’s internal option state could return a stale default (80).- Vimrc loader skips
vim.handleEx()forset textwidth=Nlines and handles them entirely viasetTextwidth()+vim.setOption(), preventing CM Vim’s Ex handler from interfering with the plugin’s textwidth state. syncTextwidthFromVim()removed — the function read CM Vim’sgetOption('textwidth')which returned the stale default (80) during theactive-leaf-changelifecycle, overwriting the correct vimrc-set value.findFenceLines()andfindContainingBlock()exported fromsrc/text-objects/code-block.tsfor reuse in delimiter scanning.MULTILINE_SCAN_LIMITconstant removed fromdelimiter.ts— scan limit is now passed as a parameter through the text object factory chain (createMultiLineDelimiterTextObject,createSmartAsteriskTextObject,registerTextObjects).
Documentation
KNOWN_LIMITATIONS.md: “Scrolloff line height assumption” marked as fixed.KNOWN_LIMITATIONS.md: “Multi-line delimiter scan limit” updated to note the limit is now configurable via settings.KNOWN_LIMITATIONS.md: “Multi-line delimiter nesting” updated to note fenced code blocks are now excluded from the scan.KNOWN_LIMITATIONS.md: “Visual mode on single-character text objects” updated from “Under investigation” to “Confirmed codemirror-vim limitation” with detailed root cause.KNOWN_LIMITATIONS.md: “set textwidthvia vimrc” root cause refined — identified CM Vim’sdefineOptioncallback resetting the value during editor initialization.KNOWN_LIMITATIONS.md: “dGleaves trailing newline” updated from “Skipped test, pending fix” to “Unfixable from plugin code” with investigation findings.KNOWN_LIMITATIONS.md: “Dot-repeat ofcw” and “n/Nsearch wrap-around” updated from “pending fix” to “Confirmed codemirror-vim bug, not a test timing issue.”
[0.8.0] - 2026-06-23
Added
- Vim chord display — pending keystrokes (e.g.
2d,gq,<C-w>h) are shown in the status bar as you type a multi-key command, clearing when the command completes or is cancelled. Reads codemirror-vim’s internalvim.statusstring directly, avoiding event-ordering issues with manual keystroke accumulation in the CM6 adapter. Togglable via Settings → Vim Motions → Vim chord display (on by default). (#2) - Customizable mode prompts — per-mode status bar text is configurable via four text fields in Settings → Vim Motions → Vim mode display prompt (normal, insert, visual, replace). Defaults to
NORMAL/INSERT/VISUAL/REPLACE. Supports emoji (e.g.🟢for normal). (#3) - Powerline-style status bar — optional colored mode indicator with per-mode background colors (gruvbox-inspired: green/normal, teal/insert, amber/visual, red/replace) and a CSS border-triangle separator. No special font required — uses pure CSS. Togglable via Settings → Vim Motions → Powerline-style status bar (off by default). Colors are overridable via CSS custom properties (
--vim-pl-normal-bg,--vim-pl-normal-fg, etc.). - Left-aligned status bar — the vim mode indicator and chord display are always positioned at the leftmost edge of the status bar via DOM reordering and
margin-right: auto, matching the convention established by obsidian-vimrc-support. ModePromptsinterface andDEFAULT_MODE_PROMPTSconstant exported fromsettings.ts.VimModeTrackerOptionsextended withpowerlineandmodePromptsfields.- CSS classes:
vim-motions-chord,vim-motions-powerline,vim-motions-statusbar-end. - Hint mode expanded into a full vimium-style UI navigation system (#7):
- Smart label length: single-character labels (from home row) when 9 or fewer targets, two-character labels for more.
- Configurable hint characters: new
hintModeLabelssetting controls the character pool for hint labels (default:asdfghjkl). - Independent settings toggle:
enableHintModesetting allows toggling hint mode on/off independently from workspace navigation. - Obsidian command: registered as
vim-motions:show-hint-labels— triggerable from command palette, assignable via Settings → Hotkeys, and usable without an open note. - Global hotkey: press-to-record hotkey setting that works even when modals (settings, command palette) have focus. Uses capture-phase DOM listeners that bypass Obsidian’s scope system.
- Multi-window support: global hotkey listener registered on workspace popout windows via
window-openevent. - Editor pane navigation:
.workspace-leaf-contentis now a hint target. Selecting it callssetActiveLeaf()with focus and activates the editor, matching click-to-focus behavior. - Smarter element activation:
contenteditableelements receive.focus(), internal links useapp.workspace.openLinkText(), Ctrl/Cmd+click opens in new pane viaMouseEventdispatch. - Backspace reset: pressing Backspace after typing a wrong first character undims all labels and allows re-selection.
- First-char mismatch dismissal: pressing a character that matches no label immediately dismisses the overlay instead of waiting for a second character.
- Auto refocus: after hint mode completes, the active editor is refocused (150ms delay) so
<leader><leader>hworks for the next invocation.
- Hint mode target selectors expanded from 9 to 24, covering: checkboxes, ribbon icons, callout folds, settings navigation items, settings controls (buttons, toggles, dropdowns), tab close buttons, search inputs, editor panes, internal links in live preview, and modal close buttons.
- Selectors grouped by stability: standard HTML selectors (stable across Obsidian versions) and Obsidian-internal selectors (may change between versions).
generateHintLabels(),HOME_ROW,ALL_KEYS, andTARGET_SELECTORexported fromhint-mode.tsfor testability.- E2E test suite
test/specs/hint-mode.e2e.tswith 13 tests across two tiers:- Tier 1 (baseline): overlay appearance, label rendering, Escape dismissal, first-char dimming, label completion, unmatched-char dismissal, Backspace reset.
- Tier 2 (behavior contracts): home-row first characters, no duplicate labels, consistent label length, visibility filtering, pointer-events CSS, Obsidian command registration.
formatHotkey()utility insettings.tsfor displaying serialized hotkey strings in human-readable form.- CSS class
.vim-motions-hotkey-displayfor the hotkey display in settings.
Changed
- Hint mode registration extracted from
registerWorkspaceNavigation()into a standaloneregisterHintMode()private method on the plugin, following the same pattern asregisterEasyMotion(). createHintModeAction()now accepts an optionalhintCharsparameter for configurable hint character pools.isVisible()now checks against scrollable ancestor containers (not just the viewport) — elements scrolled out of view insideoverflow: hidden/scroll/autoparents are excluded.showHints()refactored to usegetHintPosition()which places.workspace-leaf-contentlabels at the editor/preview content area (8px inset) rather than the top-left of the leaf container.waitForHintKey()now returnsHintResultwithctrlKey/metaKeymodifier state for new-pane activation support.activateElement()replaces the previous bare.click()with context-aware activation (focus, link resolution, modifier-based new-pane,setActiveLeaf).- Pop-out window compatibility:
window.innerHeight/scrollX/scrollYreplaced withactiveWindow.*equivalents throughout hint mode. - Hotkey recorder uses
e.codeas fallback whene.keyreports'Unidentified'(common for Ctrl+Space on Linux with input methods).
Fixed
- Hint mode now works when no note is open (via the Obsidian command path).
- Hint mode global hotkey now fires even when a modal (settings, command palette) has focus — uses capture-phase
keydownlisteners on the main window’s document that bypass Obsidian’s scope system. - Selecting a
.workspace-leaf-contenthint now properly focuses the editor pane viaapp.workspace.setActiveLeaf()instead of a bare.click()that Obsidian didn’t treat as a pane activation. - Settings controls (toggles, buttons, dropdowns, navigation items) are now targetable via hint mode.
- Tab close buttons (
.workspace-tab-header-inner-close-button) are now targetable via hint mode. - Elements inside scrollable containers (e.g., settings content area) that are scrolled out of view no longer receive hint labels.
[0.7.0] - 2026-06-22
Fixed
- EasyMotion (
<leader><leader>w/j/f) and hint mode (<leader><leader>h) now work with any leader key, including space (let mapleader = " ") and comma. Previously, leader keys with default Vim bindings (space → forward char, comma → reverse repeat find) were consumed immediately by codemirror-vim before the multi-key sequence could accumulate. Fixed by unmapping the leader key’s conflicting default binding before registering EasyMotionmapCommandentries. (#6) - Vimrc
let mapleader = " "(space) now correctly sets the leader key. The parser previously split the line by whitespace, losing the space inside quotes. Added regex-first parsing forletto preserve quoted values containing whitespace. - Vimrc loading no longer falsely reports “loaded but contained no commands” when the editor isn’t ready.
loadVimrcnow distinguishes “editor not available” (ready: false, retries on next event) from “file parsed with 0 commands” (ready: true). Includes a retry loop (up to 10 attempts, 100ms apart) to handle the race betweenactive-leaf-changeand editor initialization. - Leader-dependent features (EasyMotion, hint mode) are re-registered after vimrc loading resolves the leader key, ensuring they use the user’s configured leader instead of the default backslash.
- Visual mode selection on markdown text objects (
vi*,va*,vi$,va$,vi~,va~,vi=,va=,vi_,va_,vi`,va`,vil,val,viC,vaC,viB,vaB,vio,vao,vit,vat) now selects the correct range — previously selected one character too far to the right. Operators (d,y,c) were unaffected. Root cause: codemirror-vim’smakeCmSelectionadds +1 to the head position in visual mode, and built-in text objects compensate via an internalexpandSelectionhelper, but customdefineMotiontext objects bypassed that path. (#4) ]bwith a single buffer no longer opens a stale file from a previous session’s recent-files list.vgq(visual modegq) no longer triggers macro recording. Thevim-keypresshandler for macro recording previously intercepted theqkeystroke ingqas a macro-record toggle. Fixed by restricting macro recording to normal mode only (matching Vim behavior), tracking previous keypress to detectg-prefixed operator sequences, and cancelling pending record state on mode changes. (#5)
Added
VimRegistration.unmapDefaultBinding(key)— removes a key’s default codemirror-vim binding (e.g.<Space>→l) somapCommandmulti-key sequences starting with that key can accumulate in the input buffer.VimrcLoadResult.readyfield — distinguishes “editor not available” from “file parsed successfully”, enabling reliable retry logic for vimrc loading.- E2E tests for EasyMotion with space and comma as leader keys, verifying the
unmap+mapCommandapproach works for keys with default Vim bindings. - E2E test for EasyMotion surviving settings hot-reload (disable → re-enable cycle).
getSelection()test helper for asserting exact visual mode selections.loadSingleFileWorkspace()test helper usingobsidianPage.loadWorkspaceLayout()to set up deterministic single-file workspace state with an empty recent-files list.- 14 new E2E tests verifying exact visual mode selection for all delimiter-based text objects (
*,$,~,=,_,`), plus regression guards for operator mode. - E2E tests for
gqin visual mode (wrap + no macro recording),gqqmacro non-interference, and standaloneqmacro recording start/stop. - 3 Neovim golden comparison cases for
gqoperators (gqq,Vgq,gqj) added to theg-commandssuite with content deviation registered (Markdown-aware wrapping differs from Neovim’s plain-textgq).
Changed
registerEasyMotion()now callsreg.unmapDefaultBinding(leader)before registeringmapCommandentries, allowing any single-character leader key to work.registerWorkspaceNavigation()hint mode binding uses the resolved leader key fromLeaderRegistry(same approach as EasyMotion).createHintModeAction()return type narrowed fromActionFnto() => void(the function ignores all parameters).VimrcLoadResultgainsready: booleanfield;loadVimrc()returnsready: falsewhen the editor adapter is unavailable.- Vimrc
active-leaf-changecallback retriesloadVimrcup to 10 times when the editor isn’t ready, then re-registers leader-dependent features after successful load. KNOWN_LIMITATIONS.md: “EasyMotion leader key conflict withmapCommand” marked as fixed; added vimrc parser space-handling context.KNOWN_LIMITATIONS.md: added “Visual mode on single-character text objects” section documenting a codemirror-vim edge case wherevi*on*x*(1-char inner content) does not select correctly.
[0.6.0] - 2026-06-21
Fixed
Neovim deviation closure
di*/da*with cursor on delimiter now correctly no-ops — previously the delimiter scanner treated the delimiter position as “inside”, operating on the text. Matches Neovim behavior.diB/daBon nested blockquotes (>>) now correctly scopes to the innermost nesting level — previously deleted all blockquote content regardless of depth.P(paste before cursor) now places cursor on the last pasted character, matching Neovim — previously CM Vim placed cursor one position further.- Rewrote
gP/gpto use direct register-reading implementation instead of delegating throughVim.handleKey, avoiding re-entrancy issues with the newPoverride.
Neovim test infrastructure
- Ex commands (
:s,:sort,:d,:yank,:join,:noh,:undo,:redo,:global) now work correctly in Neovim golden comparison tests — addeddispatchVimKeysrouting that detects Ex command sequences and dispatches them viaVim.handleEx()instead of character-by-character key input.
Changed
test/neovim/deviations.tsreduced from 28 to 19 entries (9 removed, 3 new cursor-position deviations added for Ex commands where content is correct but cursor placement differs from Neovim).KNOWN_LIMITATIONS.mdbehavioral deviations table expanded with 5 entries for confirmed upstream constraints (dG,>>,V+>,d0,<<) that cannot be intercepted viamapCommanddue to codemirror-vim’s operator-pending dispatch architecture.
Added
Neovim golden comparison testing
- Neovim-backed golden comparison system for Tier 1 Vim behavior tests, inspired by Zed editor’s
NeovimBackedTestContext. Sends identical keystrokes to both Obsidian and a headless Neovim instance, compares resulting editor state (content, cursor, mode). test/neovim/client.ts— Neovim RPC client wrapping the officialneovimnpm package. Spawnsnvim --embed --headless, providessetContent(),setCursor(),input(),getContent(),getCursor(),getMode(),getRegister().test/neovim/compare.ts— state comparison helpers:getObsidianState(),getNeovimState(),compareStates().test/neovim/golden.ts— golden file read/write infrastructure withloadGoldenFile(),saveGoldenFile(),findGoldenCase().test/neovim/deviations.ts— known deviation registry tracking behavioral differences from Neovim.isKnownDeviation()silently allows expected behavioral differences during golden comparison.test/neovim/test-wrapper.ts—testWithNeovim()function: the primary test format for Tier 1 tests. Operates in playback mode (golden files, no Neovim needed) or compare mode (NEOVIM_COMPARE=1, live Neovim).test/neovim/test-definitions.ts— 199 test case definitions across 16 suites covering motions, operators, text objects, editing, yank/put, insert entry, visual mode, g-commands, bracket commands, insert mode, scroll (Ctrl-A/X), and Ex commands.test/neovim/record-golden.ts— standalone script to record golden files from Neovim without running Obsidian. Usage:npm run test:neovim-record.test/neovim/smoke.ts— Neovim client smoke test. Usage:npm run test:neovim-smoke.- 16 golden files in
test/neovim/golden-data/recorded against Neovim 0.12.2. - npm scripts:
test:neovim-smoke,test:neovim-record,test:neovim-compare.
Edge-case test expansion
- 110 new edge-case tests translated from Neovim’s legacy test suite (
test/old/testdir/), replit/codemirror-vim (test/vim_test.js), and VSCodeVim (test/motion.test.ts). - Word motion edge cases:
w/b/e/geacross empty lines, at document boundaries, with punctuation, count clipping, line wrapping. - Operator edge cases:
dwat end of line,ddon last/only line,d2w/2dd,D,dk,djon last line,de/db,dG/dgg,dfx/dtx,cwvsce,cc/C/2cc. - Text object edge cases:
iw/awon whitespace,iW/aWwith mixed punctuation, nestedi(/i{/i[,di(across lines,d2awwith count,i"with escaped quotes. - Character search edge cases:
f/tnot crossing line boundaries,2t/2Fcounts,;aftert,,reversal. - Visual mode edge cases:
viw,v3l+d,gvreselect,V+ylinewise, visual at document boundaries. - Yank/register edge cases:
yy/ywlinewise flag,y$without newline, numbered register rotation,"Ayyappend,".last inserted text. - Repeat edge cases:
.afterdw/>>/cw+text,3.with count. - Search edge cases:
*/#wrap-around. - Mark edge cases: mark persistence after edit,
'.jump to last change.
Fixed
test/coverage-report.ts— replaced broken regex YAML parser with proper YAML parsing via theyamlpackage, fixingnpm run test:coveragewhich previously reported 0/0 on the multi-line manifest format.
Changed
- Replaced
js-yamldependency withyaml— better maintained, YAML 1.2 spec-compliant, ships its own types. - All 16 Tier 1 test files (
test/specs/vim-builtin/*.e2e.ts) now usetestWithNeovim()as the primary test format alongside existingit()blocks. Neovim lifecycle hooks (startNvim/stopNvim) added to top-levelbefore/after. test/helpers.ts— addedvimRawKeys()for raw byte key sequences (supports\x1bfor Escape,\x01-\x1afor Ctrl keys,\nfor Enter).
Documentation
- README: added “Testing strategy” section describing the Neovim golden comparison system, test types (
[nvim]/[obsidian]/Tier 2), and available test commands. KNOWN_LIMITATIONS.md: added “Test-discovered behavioral discrepancies” section documenting 6 bugs found during edge-case test translation (dGtrailing newline,iBnesting,di*on delimiter, dot-repeat ofcw,)cursor off-by-one,n/Nwrap-around).
[0.5.1] - 2026-06-19
Fixed
.obsidian.vimrcis now also loaded on startup, instead of only on leaf change.
[0.5.0] - 2026-06-18
Added
New Vim commands
Q— replay last recorded macro (Neovim default, maps to@@)Y— yank to end of line (Neovim default, maps toy$; overrides CM Vim’syybehavior)ga— show character info under cursor (codepoint, hex, octal) via Noticegp— paste and move cursor past pasted textgn/gN— select next/previous search match (CM Vim native, now tested)g;/g,— jump to older/newer change position (changelist navigation)zO/zC/zA— recursive fold open/close/toggle (maps to Obsidian’s fold commands)it/at— HTML/XML tag text objects, implemented via raw text scanning since CM Vim’s built-inexpandToTagis inactive in Markdown mode. Supports single-line, multiline, and nested tags.<C-v>— visual block mode (CM Vim native, now tested)
New Ex commands
:e {file}/:edit {file}— open file by name in vault:e!/:edit!— revert current file to saved version:enew— create new untitled note:saveas {file}— save current buffer as new file:update/:up— save current file (alias for:w):x/:xit— write-if-modified and close:xa/:xall— write-if-modified all and close all:find {file}/:fin— find and open file by partial name match:read {file}/:r— insert file contents at cursor position:b {name}/:buffer {name}— switch to tab matching name:bf/:bfirst— go to first tab:bl/:blast— go to last tab:bw/:bwipeout— close current tab:sp/:split— horizontal split:vs/:vsplit— vertical split:new— horizontal split with new note:vnew— vertical split with new note:tabnew/:tabedit— open new tab (optionally with file):tabclose/:tabc— close current tab:tabonly/:tabo— close all other tabs:tabfirst/:tabrewind— go to first tab:tablast/:tabl— go to last tab:version/:ve— show plugin version:delmarks {marks}— delete specified marks:changes— show change list in modal
Test infrastructure
- Shared test helpers module (
test/helpers.ts) withsetupEditor,getCursorPos,getEditorValue,getRegisterContent,getVimMode,vimKeys, and timing constants unsupported()anddeviation()test helpers for documenting known limitations and behavioral differences in test reports- Neovim command index manifest (
test/neovim-command-index.yaml) tracking 227 commands with tier classification, test status, and test file references - Coverage report script (
test/coverage-report.ts) — run vianpm run test:coverage - 16 new test files in
test/specs/vim-builtin/covering normal mode motions, search, editing, yank/put, insert entry, scroll, marks/jumps, g-commands, z-commands, bracket commands, text objects, operators, visual mode, insert mode, and Ex commands - 7 spike tests for register access, paste marks, editor extensions, tag text objects, CM Vim Ex command probing, Ex command conflict checking, and vimrc mapping diagnostics
- Comprehensive E2E test coverage for
<C-w>h/j/k/lpane focus,H/M/Lscreen-relative motions,?backward search,zO/zC/zArecursive folds, and all new Ex commands - E2E test for scrolloff hot-reload: verifies scroll margins update when
scrolloffLineschanges - E2E test for
Y/Qindependence from workspace navigation: verifiesYstill yanks to end of line when workspace nav is disabled - GitHub issue templates (bug report, feature request) with required KNOWN_LIMITATIONS.md checklist
Fixed
- Scrolloff now works correctly — previously used CSS
scroll-paddingwhich CodeMirror 6 ignores (it uses manual scroll calculations, notElement.scrollIntoView). Replaced withEditorView.scrollMarginsfacet, which CM6 respects when scrolling the cursor into view - Scrolloff setting now applies immediately when changed in settings — previously required a plugin reload because the slider’s
onChangehandler did not triggerreloadFeatures()andreloadFeatures()itself had no scrolloff handling - Removed deprecated
setDynamicTooltip()call on scrolloff slider — the value is now always shown inline by Obsidian Y(y$) andQ(@@) Neovim default remaps now work regardless of the “Workspace navigation” toggle — previously these were registered insideregisterWorkspaceNavigation()and would stop working when workspace nav was disabled- Vimrc loader now shows a Notice on load: reports the number of commands applied on success, warns when the file is not found, and warns when the file contains no commands
- Vimrc commands are now processed through codemirror-vim’s Ex command handler (
handleEx) instead of the programmatic API, matching obsidian-vimrc-support’s approach for improved compatibility - ESLint
import/no-extraneous-dependencieserror on@codemirror/view— addedimport/core-modulessetting andpeerDependenciesfor@codemirror/*packages provided by Obsidian at runtime - Removed unused variables:
totalLinesintag.ts,openEndIndex/closeStartIndexintag.ts,activeincommands.ts,newLeafincommands.ts
Changed
- Scrolloff implementation rewritten from CSS
scroll-paddinginline styles toEditorView.scrollMarginsextension registered viaregisterEditorExtension. TheScrolloffManagerclass no longer manages event listeners or DOM manipulation — it updates a shared margin variable read by the CM6 facet callback. - Refactored 8 existing test files to use shared helpers from
test/helpers.tsinstead of locally definedgetEditorValue,getCursorLine, andvimKeysfunctions - Test-vault
hotkeys.jsonnow unbinds Obsidian shortcuts that conflict with Vim commands (Ctrl+W,Ctrl+N,Ctrl+P,Ctrl+S,Ctrl+O) - Tag text objects (
it/at) changed fromunsupportedskip to working plugin-implemented text objects ChangeListclass gainsgetEntries()andgetIndex()public accessors for the:changesEx commandYandQNeovim default remaps moved fromregisterWorkspaceNavigation()to the always-on initialization path inonload()andreloadFeatures()- Vimrc loader’s
loadVimrc()now returns aVimrcLoadResultwithfound,commandCount,path, andmapsfields - Vimrc loader refactored to use
vim.handleEx()for command application instead of directvim.map()/vim.setOption()API calls, improving compatibility with obsidian-vimrc-support configurations - Vimrc loader now collects parsed map commands as
DeferredMapentries and re-applies them viavim.map()/vim.noremap()on subsequentactive-leaf-changeevents, attempting to restore mappings that CM Vim may lose during editor reinitialization - Vimrc loader intercepts
set textwidth=N/set tw=Nlines and directly updates the plugin’s internaltextwidthValue, bypassing CM Vim’s option callback chain getTextwidth()now reads from CM Vim’s option viaVim.getOption('textwidth')as a fallback when the plugin’s internal value hasn’t been updated- Vimrc loading deferred to first
active-leaf-changeevent to guarantee editor availability, matching obsidian-vimrc-support’s loading strategy
Documentation
- README: added
:wa/:wallto Ex commands table,g<C-t>to workspace keybindings table - README: corrected
set textwidth=Nclaim — now notes the known limitation and provides the runtime workaround via developer console KNOWN_LIMITATIONS.mdexpanded with comprehensive “Neovim Ex commands not applicable in Obsidian” section covering 30+ commands across 8 categories (shell, quickfix, tags, scripting, diff, etc.) with specific reasoningKNOWN_LIMITATIONS.mdexpanded with “Behavioral deviations” section documenting 6 commands that work differently from Neovim (Y,Q,:wall,gf,zO/zC/zA,it/at)KNOWN_LIMITATIONS.md: added “nmap L $does not work via vimrc” section with full diagnostic findingsKNOWN_LIMITATIONS.md: added “set textwidthvia vimrc does not affectgq” section with root cause analysisKNOWN_LIMITATIONS.md: replaced “Scrolloff cleanup on disable” section with “Scrolloff line height assumption” (22px hardcoded)
[0.4.0] - 2026-06-14
Changed
- Lowered minimum Obsidian version from 1.13.0 to 1.1.1 — audited all Obsidian API usage and confirmed no API newer than 0.13.8 is required. Users on Obsidian 1.1.1 and later can now use the plugin.
- Replaced Obsidian’s
setCssPropsprototype augmentation with standardel.style.setProperty()calls in EasyMotion and hint mode. Removes dependency on an undocumented global API whose introduction version is unknown, improving backward compatibility. - Prefixed all plugin-owned CSS custom properties with
--vim-motions-to avoid collisions with other plugins or themes:--em-left→--vim-motions-em-left--em-top→--vim-motions-em-top--hint-left→--vim-motions-hint-left--hint-top→--vim-motions-hint-top--hint-opacity→ replaced with.is-dimmedCSS class (avoids inline style assignment)
Added
- E2E tests for blockquote text objects (
iB/aB) and callout text objects (io/ao) - E2E tests for buffer navigation (
]b/[b) - E2E tests for EasyMotion interaction (overlay appearance, dismissal, line/char label variants)
- E2E tests for workspace operations: splits (
<C-w>v/<C-w>s), folds (zc/zo/zM/zR), tab navigation (gT), file switcher (gf), rename (grn), backlinks (grr), document stats (g<C-g>) - E2E tests for ex commands with effect verification:
:q,:wq,:bp,:only,:back,:forward,:explorer,:ls - E2E tests for quality-of-life features: status bar mode display (NORMAL/INSERT/VISUAL), which-key overlay, ex command suggest
- E2E tests for settings hot-reload: toggling text objects, navigation, status bar, and EasyMotion on/off
- E2E tests for operator edge cases: bullet/numbered/nested list prefix preservation in
gq,gqj(two-line wrap),gqip(paragraph reflow) - E2E tests for text object edge cases: empty delimiters (
****,~~~~,====), visual mode selection (vi*), yank (yi*) - E2E tests for navigation edge cases: heading levels
]3/]4/[3, ordered list navigation, last-heading boundary, cross-line link jumps
[0.3.0] - 2026-06-14
Fixed
gdon wiki links with display names ([[file|display name]]) now correctly navigates to the file instead of creating a new file with the display name in the pathgdon wiki links with heading fragments ([[file#heading|display]]) correctly preserves the heading target- EasyMotion keybindings (
<leader><leader>w/j/f) now work — previously registered as literal<leader>strings inmapCommandwhich could never match typed input - Hint mode (
<leader><leader>h) same fix as EasyMotion - Leader key bindings configured via settings UI or
.obsidian.vimrcnow work when workspace navigation is disabled —:obex command is registered unconditionally instead of only when workspace nav is on - Leader key bindings no longer silently fail when obsidian-vimrc-support is installed — removed unnecessary guard that skipped
:obregistration - Leader key bindings survive settings hot-reload —
:obis re-registered inreloadFeatures()so it isn’t left as a noop after toggling any setting - Which-key overlay now dismisses when a key is pressed after it appears — previously
show()resetpendingLeaderstate, preventing dismissal - Which-key overlay no longer leaks
active-leaf-changeevent listeners on destroy ExCommandSuggestis rebuilt after settings hot-reload so the completion list stays current
Added
]c/[cas alternative keybindings for table cell navigation, for keyboards where|requires AltGr or modifier keys- EasyMotion and hint mode bindings now appear in the which-key overlay
- Which-key overlay rebuilds after settings hot-reload
Changed
- Plugin initialization order restructured: leader key resolution (vimrc loading) now happens before feature registration, so EasyMotion and hint mode receive the correct leader key
registerObCommandextracted as a standalone function, called unconditionally in bothonload()andreloadFeatures()LeaderBindingnow trackssource('builtin'or'user') to support selective clearing during hot-reloadLeaderRegistrygainsclearBuiltinBindings()for clean re-registration duringreloadFeatures()registerEasyMotion()andregisterWorkspaceNavigation()acceptLeaderRegistryparameter
[0.2.0] - 2026-06-13
Fixed
- Vimrc path now uses
Vault.configDirinstead of hardcoded.obsidian, supporting custom config directories - Setting descriptions use dynamic config directory path
:obwith no arguments now opens a searchable modal listing all command IDs instead of logging to the developer console- Coexistence E2E test now opens a file before assertions, fixing CI race condition
- Removed deprecated
setDynamicTooltip()call on scrolloff slider
[0.1.0] - 2026-06-13
Added
Markdown text objects
i*/a*— inside/around bold (**...**) or italic (*...*), with smart disambiguationi_/a_— inside/around italic (_..._)i`/a`— inside/around inline codei$/a$— inside/around math ($...$)i~/a~— inside/around strikethrough (~~...~~)i=/a=— inside/around highlight (==...==)il/al— inside/around links ([[wikilink]]or[text](url))iC/aC— inside/around fenced code blocksiB/aB— inside/around blockquotesio/ao— inside/around callouts- All delimiter-based text objects work across multiple lines (20-line scan limit)
Structural navigation
]h/[h— next/previous heading (any level)]1–]6/[1–[6— next/previous heading by specific level]l/[l— next/previous list item (same indent level)]n/[n— next/previous link]b/[b— next/previous open buffer (tab), with fallback to recent files]|/[|— next/previous table cell
Operators
gq— hard-wrap text at textwidth (default 80) with Markdown-aware prefix preservation (blockquotes, lists, nested structures)gw— same asgqbut keeps cursor at original position
Workspace navigation
<C-w>h/j/k/l— focus pane left/down/up/right<C-w>v/<C-w>s— split vertical/horizontal<C-w>c/<C-w>q— close current tab<C-w>o— close all other tabsgt/gT— next/previous tabgd— go to definition (follow link under cursor)gx— open URL under cursor in browsergf— open file switcher (quick open)gO— document outline navigator (searchable heading list)grn— rename current notegrr— show backlinks to current notegra— context-aware actions for cursor positiong<C-g>— show document statistics (words, lines, characters)za/zc/zo— toggle/close/open fold at cursorzM/zR— fold all / unfold all
Ex commands
:w/:write— save current file:q/:quit— close current tab:wq— save and close:bn/:bp— next/previous tab:bd/:bc— close current tab:only— close all other tabs:qa/:quitall— close all tabs:wa/:wall— save all:ob {command-id}— execute any Obsidian command by ID:ob— list all available command IDs:sidebar left/:sidebar right— toggle sidebar:explorer— reveal active file in file explorer:buffers/:ls— show all open buffers in a modal:backlinks— show backlinks to current note in a modal:grep {pattern}— search vault for text, show results in a modal:back/:forward— navigate back/forward in history:reg/:registers— show register contents in a modal:marks— show marks and their positions in a modal
EasyMotion / Hop
<leader><leader>w— label every word start in the viewport<leader><leader>j— label every non-empty line<leader><leader>f{char}— label every occurrence of a character<leader><leader>h— hint mode (Vimium-style labels for clickable UI elements)
Quality of life
- Vim mode status bar showing NORMAL / INSERT / VISUAL / REPLACE
- Macro recording indicator showing RECORDING @{register} in status bar
- Which-key hints overlay when leader key is pressed
- Ex command tab completion via Tab key
- Scrolloff (configurable visible lines above/below cursor)
- Configurable insert escape sequence (e.g.,
jkto exit insert mode viaset insertmodeescape=jk) - Settings hot-reload (toggle features without restarting Obsidian)
Vimrc loader
- Built-in
.obsidian.vimrcsupport compatible with obsidian-vimrc-support syntax - Supported commands:
map,nmap,imap,vmap,noremap,nnoremap,inoremap,vnoremap,unmap,set,let mapleader,exmap,obcommand,source - Supported
setoptions:clipboard,tabstop/ts,textwidth/tw,shiftwidth/sw,expandtab/et,insertmodeescape/ime - Leader key replacement in mappings (
<leader>token) - Leader key propagation to sourced files
Settings
- Independent toggles for all feature groups
- Leader key bindings table (add/remove key-to-command mappings without editing vimrc)
- Scrolloff slider (0–20 lines)
- EasyMotion label character customization