Overview
Vim Motions provides an enhanced Vim experience for Obsidian, built on a specialized fork of codemirror-vim. It extends Obsidian’s editing capabilities with Markdown-aware text objects, structural navigation, and advanced workspace control while maintaining compatibility with the existing Vim ecosystem.
Dual-vim architecture
The plugin operates in two distinct modes to ensure flexibility and stability:
- Built-in vim mode: When Obsidian’s native Vim mode is enabled, the plugin uses the bundled
codemirror-viminstance viawindow.CodeMirrorAdapter.Vim. - Bundled fork mode: When native Vim is disabled, the plugin registers its own fork as a CodeMirror 6 extension. It installs a bridge at
window.CodeMirrorAdapter.Vimso other plugins (likeobsidian-vimrc-support) can still discover the Vim API.
The bundled fork is preferred for its advanced features, including async motion support for EasyMotion, Neovim-correct cursor positioning in Live Preview, and various behavioral fixes not present in the upstream library.
Module structure
The src/ directory is organized into focused modules:
main.ts: Entry point handling plugin lifecycle and feature orchestration.settings.ts: Defines the settings interface, UI tab, and default values.vim/: Core engine integration, including registration logic, API detection, and the bundled fork provider.easymotion/: Implementation of EasyMotion/Hop navigation.motions/: Structural navigation for headings, lists, links, and tables.text-objects/: Markdown-specific text objects (e.g., code blocks, callouts).operators/: Custom operators like hard-wrap (gq).actions/: Custom actions such as smart list continuation.workspace/: Global navigation, pane management, and Ex commands.ui/: UI components including WhichKey, hint mode, and command suggestions.vimrc/: Logic for loading and parsing.obsidian.vimrcfiles.
Lua runtime (src/lua/)
The plugin includes a sandboxed Lua 5.3 runtime via a browser-only fork of fengari. The src/lua/ directory is organized into focused modules:
engine.ts: Lua VM lifecycle — sandboxed state creation, instruction-limit timeout (config load: 1M instructions; runtime callbacks: 500K; snippet nodes: 100K),withInstructionGuardhelper, throttled error notices, code evaluation.api.ts: Registers thevim.*API surface —vim.opt,vim.g,vim.cmd,vim.keymap,vim.api(16nvim_*functions),vim.notify,vim.obsidian/vim.ob,vim.env,vim.log.levels.fn.ts: Registersvim.fn.*functions (26 functions) with callbacks bridging to Obsidian’s vault and editor APIs.stdlib.ts: Pure-Lua standard library utilities —vim.tbl_*(12 table functions),vim.split/vim.trim/vim.startswith/vim.endswith/vim.inspect, andvim.json(JS-bridged encode/decode).timers.ts: Async primitives —vim.schedule,vim.defer_fn,vim.uv/vim.looptimer subset. Managed byTimerManagerfor cleanup on plugin unload.autocmd.ts: Autocommand system —AutocmdManagerhandles 12 events (InsertEnter,CursorMoved,BufWritePre, etc.) with augroup management and pattern matching.buffer.ts: Buffer-local keymaps —BufferKeymapManagerstores per-file keymaps and swaps them on active leaf change.highlight.ts: Highlight group → CSS bridge —HighlightManagermapsnvim_set_hlcalls to CSS custom properties (plugin-defined groups) or dynamic CSS classes (user-defined groups).loader.ts: Orchestrates Lua config loading — reads.obsidian.init.lua, creates the sandboxed state, injects all API modules, evaluates user code, and returns collected keymaps/commands/settings.
The injection order matters: createSandboxedState() → injectVimApi() → injectVimFn() → injectStdlib() → injectTimers() → evalLua(userCode).
Feature registration pattern
Features are registered using the VimRegistration class. This provides a unified interface for defining motions, actions, operators, and Ex commands:
defineMotion(),defineAction(),defineOperator(),defineExCommand()mapCommand()for keybindings
This abstraction allows for clean “hot-reload” functionality. During a settings update or plugin reload, unregisterAll() is called to clear existing definitions before re-registering features based on the new configuration.
Vim API bridge
To maintain ecosystem compatibility, the plugin uses a getVimApi() helper that prioritizes the built-in API but falls back to the bundled fork.
When the fork is active, a bridge is installed using a property descriptor (getter) on window.CodeMirrorAdapter.Vim. This ensures the fork’s Vim singleton is always returned, even if other plugins attempt to overwrite the property, ensuring a consistent experience regardless of load order.
Settings hot-reload
The plugin supports dynamic configuration changes without requiring an Obsidian restart. When settings are modified, reloadFeatures() orchestrates a full teardown and setup cycle:
- Unregisters all custom Vim commands.
- Resets the internal keymap.
- Re-registers features and applies
.obsidian.vimrcmappings based on the updated settings.
Testing
Vim Motions uses a robust testing strategy centered on Neovim parity. Tier 1 commands are verified against a headless Neovim instance using the testWithNeovim() helper.
- Golden files: Recorded Neovim output used for CI comparison.
- Deviation registry: Tracks intentional differences where Obsidian’s behavior purposefully diverges from Neovim.
- E2E tests: WebDriverIO tests running against a real Obsidian instance.
For more details on the testing infrastructure, see AGENTS.md.