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]
Added
- 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
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
Documentation
docs/configuration/lua-config.md: added sections for table/string utilities, JSON, notifications, async/timers, buffer-local keymaps, buffer content, Obsidian namespace, environment variables, highlight groups; updated supported events table and unsupported APIs calloutKNOWN_LIMITATIONS.md: updated supported APIs list, autocmd event count (8 → 12), removed “not yet implemented” note for CursorHold/CursorMoved/BufWritePre/Post, updated intentionally skipped features tableAGENTS.md: added Plugin-side Lua API summary to fengari fork sectionREADME.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