Vim Motions

by Emile Bangma
5
4
3
2
1
Score: 58/100

Description

Enhances Obsidian's built-in Vim mode with Markdown-aware text objects, structural navigation, workspace keyboard control, and a polished Neovim-native experience.

Reviews

No reviews yet.

Stats

82
stars
8,880
downloads
10
forks
79
days
1
days
4
days
6
total PRs
1
open PRs
4
closed PRs
1
merged PRs
180
total issues
6
open issues
174
closed issues
1,155
commits

Latest Version

4 days ago

Changelog

Added

  • :stopinsert (:stopi), which docs/guides/plugin-integration.md has documented in three places without it existing. handleEx('stopinsert') reported unknownCommand: true and the notice Not an editor command ":stopinsert", while :startinsert resolved normally — so the published Better Paste recipes, whose whole point is returning to normal mode after handing insert mode to another plugin, silently left the editor in insert mode. Measured against Neovim 0.12.5 before implementing: :stopinsert is indistinguishable from <Esc> on the way out (A x on hello leaves hellox with the cursor on column 5 either way), and it is a no-op outside insert mode. It deliberately does not route through doKeyToKey(cm, '<Esc>') the way :startinsert routes through i/A: in normal mode that would run the fork's idle-normal Escape path and fire the host's _idleEscapeCallback, which dismisses popovers and blurs non-workspace editors, so a command Vim defines as doing nothing would have visible side effects. It calls exitInsertMode behind an insertMode guard instead.
    • Fork: src/vim.js (defaultExCommandMap entry, exCommands.stopinsert)

Fixed

  • A repeated tabstop inside Markdown emphasis lost both cursors and the whole snippet on the first keystroke in Live Preview — the follow-up half of #198. With the reported body $1 *a$2* *b$2* $0, Tab placed cursors correctly at both $2 occurrences, but typing z left them at 5 and 7 instead of 4 and 9, outside the emphasis and outside the snippet's own field ranges — so CodeMirror dropped the session (selectionInsideField returns false) and a second keystroke produced *az*y *ybz* where *azy* *bzy* was wanted. A repeated tabstop is not an extension: the LSP snippet specification requires the occurrences to be linked ("typing in one will update others too"), VS Code and CodeMirror realise that as simultaneous selections, and Neovim's own vim.snippet as one cursor plus mirrored ranges. Two measurements locate the defect outside the snippet machinery — the same body with no markup ($1 a$2 b$2 $0) is correct through every keystroke in Live Preview, and the emphasis body is correct in Source mode. The guard shipped in 1.3.0 covers the tabstop jump; the second exposure is the first edit at a tabstop, where the document change closes the jump's window and Obsidian's snap arrives on transactions after it. Against a multi-range selection the snap does not offset each range past its marker as it does for one cursor — instrumentation recorded it collapsing 4:4,9:9 to 8:8 and then rebuilding it as 4:4,7:7, across two separate dispatches — so the window is now held for the whole macrotask rather than being consumed by the first drop. Arming also covers any document change that leaves the selection inside the active field, and a transaction that re-sets the selection it already has no longer closes the window, because closing it there leaves the snap behind it unguarded.
    • Plugin: src/snippets/live-preview-guard.ts (isTabstopEdit arming, isMarkerSnap extracted, the window no longer consumed by a dropped snap or by a selection-preserving transaction)
  • <Esc> ignored user mappings entirely, so :imap <Esc> … and :vmap <Esc> … did nothing. handleEsc() ran before matchCommand() and exited insert or visual mode unconditionally, so the mapping table was never consulted for that one key. Neovim honours it — measured on 0.12.5, inoremap <Esc> XY then a <Esc> yields aXYbc, where the fork produced abc; xnoremap <Esc> ll leaves visual mode active, where the fork returned to normal. Only full matches win: with inoremap <Esc>q ZZ and nothing bound to bare <Esc>, Neovim still exits insert mode, and honouring partials here would be worse than a deviation — the fork's insert-mode partial branch returns consumed without arming insertModeEscKeysTimeout for a non-character key, so a user who bound <Esc>q would have no way out of insert mode at all. A mapping already being expanded is skipped via keyToKeyStack, which is what keeps the recursive imap <Esc> <Esc> falling back to the built-in exit instead of resolving to a no-op. <C-[> now follows the <Esc> mapping and <C-c> still does not, matching Neovim, because <C-[> is Escape — both send 0x1b — while <C-c> is a distinct key; the two adjacent keyToKey entries therefore differ by a single noremap property. That property is the whole mechanism: commandMatches already skips user entries during a noremap expansion through startIndex, so an explicit if (noremap) return false in the new resolver was removed after no test could distinguish it.
    • Fork: src/vim.js (userEscMappingClaims() consulted by handleEsc(); noremap: false on the <C-[> and <C-Esc> entries)

Tests

  • Four scenarios for repeated tabstops, two of which are controls that must stay green. Red first, against a rebuilt bundle: keeps both cursors inside their emphasis after typing returned [{4,4},{9,9}] → [{5,5},{7,7}], and keeps the repeated tabstop live for a second keystroke returned *az*y *ybz* against *azy* *bzy*. Both halves of the fix were then sabotaged separately and each failed differently, which is what shows they are not one change: reverting the edit-arming reproduced the original failure exactly, while keeping the arming and consuming the window on the first dropped snap failed partially — cursor one correct at 4:4, cursor two still wrong at 7:7, text *azy* *ybz* — the signature of a two-transaction cascade with only the first blocked. The markup-free body and the Source-mode case are the controls: were repeated tabstops simply unsupported, both would fail too, and neither moved at any point. (#198)
    • Tests: test/specs/snippets/snippet-live-preview-tabstop.e2e.ts (four scenarios, plus a multi-range selection reader — getCursorPos reports only the main range, so the previous assertions could not have seen a second cursor at all)
  • Twelve scenarios for :stopinsert and user <Esc> mappings, five of which are controls that were green before the change and needed their own sabotage. Red first: 7 failed, 5 passed — the four :stopinsert scenarios on unknownCommand: true, inoremap <Esc> XY at abc against aXYbc, <C-[> likewise, and vnoremap <Esc> ll at normal against visual. Each of the five pre-existing passes was then broken deliberately and failed alone: dropping the keyToKeyStack check failed only the recursive imap <Esc> <Esc> (insert, expected normal); matching partials as well as full matches failed only the <Esc>q scenario (insert, expected normal); adding noremap: false to the <C-c> entries failed only the <C-c> scenario (aXYbc, expected abc); dropping :stopinsert's insertMode guard failed only the normal-mode no-op (cursor ch: 1, expected ch: 2); and making the resolver claim every Escape failed 8 of the 12, including both no-mapping controls. A sixth sabotage found dead code rather than a gap: removing if (noremap) return false left all 12 green, because commandMatches already enforces it, so the line was deleted and the suite re-run at 12 passing. The visual scenario asserts against the same keys typed directly instead of a literal column — CM6 reports an exclusive selection head (3) where Neovim reports an inclusive one (2), and a literal there encodes the coordinate convention rather than the mapping; the first draft failed on exactly that and the baseline measurement is what distinguished it from a real defect.
    • Tests: test/specs/vim-builtin/insert-escape-mapping.e2e.ts (new), test/specs/vim-builtin/insert-escape-mapping-negative-controls.md (new, including the Neovim oracle table), test/specs/lua-doc-examples.e2e.ts (one scenario for the published vim.cmd("stopinsert") recipe)
  • A vim.schedule(stopinsert) scenario was written, found vacuous, and deleted rather than kept. It wrapped the deferred half of the same published recipe and passed against the unfixed build, which is the signal that it proved nothing. A mode timeline located why: with no fix present it read normal at settle, normal at +200 ms and insert at +1200 ms, while normal! a on its own read insert at all three — so the waitUntil(mode === 'normal') was satisfied on its first poll, before the leader mapping had fired at all. The classic shape of a test passing because its subject never ran. It cannot be made honest without an observable window between the callback returning and the scheduled tick, and widening the defer to manufacture one would test vim.defer_fn instead of the documented vim.schedule. The direct scenario is genuinely red-first (insert against normal on the unfixed build) and covers :stopinsert reached through vim.cmd from a Lua callback, which is the substance.

Documentation

  • AGENTS.md: a bare npx wdio run does not rebuild main.js, so a source change under test is silently absent and a negative-control sabotage reports green — the plugin-side twin of the cm-buildhelper trap already recorded for the fork.
  • CONTRIBUTING.md: the guard's two exposures, and why a dropped snap does not close its window.
  • KNOWN_LIMITATIONS.md: a new ### Tabstop placement limitations entry for tabstops inside a table in Live Preview, and the repeated-tabstop support statement including where Neovim's presentation differs. Also a new ### set tablewidget=raw does not accept typed text inside a table in Live Preview entry under the table-widget section, recording a previously unreported defect found while checking whether raw was a workaround: with the cursor inside a cell, a typed character lands at the end of the document. raw mode is CSS-only, so Obsidian's table decoration stays in the CodeMirror state. Measured with a control matrix — native in Live Preview and raw in Source mode are both correct on the identical fixture and cursor, which isolates the failure to raw in Live Preview. No snippet is involved.
  • docs/features/snippets.md: repeated tabstops documented as linked occurrences, with the table caveat.
  • docs/features/ex-commands.md and docs/reference/keybindings.md: :stopinsert/:stopi and :startinsert/:start documented, including that :stopinsert is a no-op outside insert mode and lands the cursor where <Esc> would.
  • docs/configuration/remapping.md and docs/configuration/vimrc.md: new sections on remapping <Esc>, covering the three behaviours a user has to know before binding it — exact matches only, <C-[> following the mapping while <C-c> stays a dependable escape hatch, and a recursive mapping falling back to the built-in exit.
  • KNOWN_LIMITATIONS.md: a new ## Only an exact Escape mapping overrides the built-in mode exit entry, placed beside the existing noremap mapping limitation. The heading spells out "Escape" rather than <Esc> so the deep link from docs/configuration/vimrc.md has a plain-text anchor — an anchor carrying angle brackets has no working precedent in docs/, and the one existing link to a heading with backticks and slashes (ex-commands#ob--obcommand--execute-obsidian-commands) uses the slugified form instead. It records that the full-match-only rule is a safety property as well as a parity one: the engine's insert-mode partial branch consumes the key without arming insertModeEscKeysTimeout for a non-character key, so honouring a partial would swallow <Esc> indefinitely and leave no way out of insert mode.
  • AGENTS.md: user <Esc> mappings and the <C-[>-versus-<C-c> asymmetry recorded in the fork description, and a note that a bad probe key can look like a missing feature — Ctrl+L is Obsidian's own editor:toggle-checklist-status hotkey and is consumed at window capture, which is why an insert-mode function keymap bound to it appears never to fire while the same mapping on <A-y> fires normally.

Full Changelog: https://github.com/saberzero1/motions/compare/1.3.1...1.4.0

README file from

Github

Vim Motions

A polished, Neovim-native experience inside Obsidian. Vim Motions adds what's missing from Obsidian's built-in Vim mode: Markdown-aware text objects, structural navigation, hard-wrap formatting, workspace keyboard control, EasyMotion, Lua configuration with vim.keymap.set / vim.opt / vim.fn / vim.api / vim.tbl_* / autocommands / timers / highlight groups, and a built-in .obsidian.vimrc loader.

Full documentation →

Features

  • Markdown text objects — operate on bold, italic, code, math, links, blockquotes, code blocks, callouts, tags, table cells, subwords, numbers, quotes, wikilinks, URLs, arguments, and indentation with d, c, y, v
  • Structural navigation — jump between headings, lists, links, and buffers with ]h, ]l, ]n, ]b
  • Lua configuration — .obsidian.init.lua with conditional logic, function keymaps, vim.v predefined variables (count, count1, register, operator, searchforward, maxcol, constants), { expr = true } expression mappings, vim.fn.* (92 real implementations with async callbacks, including byte/character/display-column helpers, line byte offsets, deletebufline, and CM6 viewport geometry), vim.api.* (69 real nvim_* implementations: buffer, cursor, marks, keymaps, options, commands, highlights, namespaces, extmarks, autocommands, mode query, vvars, byte offsets, and synthetic current-window calls/dimensions/identity), vim.o/vim.go global option fallbacks, vim.iter (26 methods; rpop, count, and size are extensions), vim.on_key (pre-mapping physical-key observation), Neovim key-byte conversion via nvim_replace_termcodes and decoding in nvim_feedkeys, vim.tbl_*, vim.snippet.*, vim.json, vim.inspect, vim.regex (Vim patterns), vim.validate (full Neovim spec), vim.version (parse, compare, range), vim.keycode, vim.schedule/vim.defer_fn/vim.uv timers, autocommands (19 events, mode events fire per-view across all editors), vim.obsidian namespace (including vim.obsidian.im for input method control), buffer-local keymaps, async file reading (vim.ob.fs.read), async key input (vim.fn.getcharstr), async user prompts (vim.fn.input), regex buffer search (vim.fn.searchpos), extmarks (nvim_buf_set_extmark), multi-file configs via require() (with init.lua fallback, resolved synchronously from an in-memory snapshot so lazy require works inside keymap callbacks), plugin management with automatic GitHub fetching (vim.plugins.add, retaining Lua and .scm query files), collectgarbage() support, __gc userdata finalization, vim.o.operatorfunc support for g@ in bundled fork mode (also via vim.opt, vim.go, and global option APIs), vim.treesitter (backed by web-tree-sitter WASM — get_parser, get_node, query.parse/get/set/get_files, user/plugin/bundled queries with extension and inheritance modelines, Query:iter_captures/iter_matches, LanguageTree, 31 TSNode methods, 8 built-in predicates, 4 directives), five real string-coordinate helpers (str_byteindex, str_utfindex, str_utf_start, str_utf_end, str_utf_pos), and config hot-reload on save (.scm edits require a configuration reload). Coordinate correctness is scoped to 23 enumerated APIs, now including text, legacy positions and extmark columns. Interior-byte cursor/text/extmark writes normalize instead of preserving Neovim's byte remainders; text reads preserve exact bytes, and cursor goals use the fork's partial state. Other coordinate seams remain deferred. See known limitations.
  • Neovim backend with Obsidian bridge — optional desktop-only connection to a user-supplied Neovim 0.12+ binary over msgpack-RPC. Neovim receives ordinary Markdown-editor keys through nvim_input, owns text, mode, cursor, registers, undo, folds, dot-repeat, macros, persistent extmarks, and floating windows, and mirrors line events back into an input-inert CM6 editor. A cursor-positioned input outside CM6 owns native IME composition and forwards only committed text through nvim_input, preserving undo and dot-repeat. Visible extmarks and redraw-time fold state render as matching CM6 decorations and folds. Floats render as Obsidian overlays with their buffer content, extmarks, border presence, and z-index; row, column, width, and height use measured CM6 cell metrics, so terminal-grid placement on proportional Markdown typography is approximate. Registry-generated callbacks restore all built-in picker leader actions and picker ex commands, :Oil, every Harpoon action and ex callback, host-owned cross-note <C-o>/<C-i>, :marks/:delmarks/:jumps, workspace navigation, go-to-definition variants, and the three undo-tree sidebar commands. Structural heading, list, and link motions and Markdown text objects execute inside Neovim as buffer-local companion mappings backed by native treesitter, with bounded operator ranges, visual selections, registers, and count handling matching the fork; native gq/gw uses the mirrored buffer's configured textwidth and stock Markdown ftplugin. The sidebar reads Neovim's native undotree() data; fold and undo commands remain native Neovim operations. Oil's embedded editor is deliberately outside RPC key delegation, so all 16 Oil mappings continue through its bundled Vim engine without duplicating actions or leaking keys into Neovim. Slot strings, counts, picker queries, named sources, and resume state cross the general payloads; cross-note actions re-seed the active note and restore its stored cursor in CM6 and Neovim. Lowercase within-buffer mark motions remain Neovim-native; uppercase cross-file mark motions are deferred. Lowercase bridged commands use guarded command-line abbreviations and do not expand inside substitutions. Both Properties in document modes are supported: Source frontmatter stays navigable, while rendered frontmatter is protected by a closed Neovim fold and properties-widget input remains owned by Obsidian. The mirror is an acwrite buffer: :w uses Obsidian's active-editor save command, while :e and :e! re-seed from the current Obsidian document instead of reading the file behind Obsidian's back. Fold persistence and the i=/a= highlight object are unavailable under RPC; the bundled Markdown parser exposes no highlight node. M7 latency is certified: over 500 measured keystrokes per condition on a ~2000-line note, RPC measured p50 8.2 ms / p95 23.5 ms / p99 28.9 ms against the fork's 16.2 / 43.4 / 54.0 ms (deltas −8.0 / −19.9 / −25.1 ms), inside the ≤25 ms/≤60 ms budget. RPC leads at every percentile and in all four measured command classes, because a pipe round-trip to a native process costs less than running a full vim implementation in the renderer over a note that size. An optional configuration path can load a minimal Obsidian-specific init.lua under --clean; leaving it empty loads your normal Neovim setup. Multi-leaf buffer ownership is deferred. Because Neovim owns editor keys, editor keymaps from the plugin's own Lua and vimrc config do not fire in this mode; their equivalents belong in your Neovim config. Enabling it runs the binary and configuration you supply as arbitrary code. That code may load native libraries through LuaJIT FFI and read or write files outside the vault. No sandbox is provided. Vim Motions never downloads or installs Neovim itself. It can ask your Neovim to install or update the plugins it generates configuration for, but only when you press the button and confirm the preview.
  • Neovim external UI — RPC-mode errors, warnings, notifications, echoes, Lua prints, and shell output appear as severity-styled, duplicate-limited Obsidian Notices; routine undo and search messages remain silent. A byte-correct, nested external command line renders :, /, and ? input plus vim.ui.input and vim.ui.select prompts. The external popup menu shows command-line wildmenu and insert completion with live selection, and Neovim's own mode output owns the plugin status bar until RPC disconnects.
  • Built-in vimrc — .obsidian.vimrc loader with 100+ configurable settings, which-key support with Lucide icons, and hot-reload on save
  • Flash motions — enhanced f/F/t/T with labels on all visible matches (flash.nvim-inspired). Auto-jumps on single match, count prefix honored (3f{char} jumps to 3rd match without labels). Operator-pending (df, cf, yf), visual mode, multi-line search. Incremental s jump mode (type multiple chars to narrow, labels update live), post-commit //? search labels, clever-f repetition, label conflict skipping, [3/15] search match counter. Dynamically sized match highlights and labels positioned after matched text (flash.nvim parity)
  • EasyMotion / Hop — jump to any visible position with two keystrokes, with operator-pending support
  • Workspace keyboard control — navigate panes, tabs, and sidebar without a mouse (<C-w>, gt/gT/Ngt, :sp/:vs), including native File Explorer navigation with h/j/k/l. Built-in hotkey conflict detection with resolution wizard
  • Surround — add, change, or delete surrounding delimiters (vim-surround with Markdown support, including dsf/csf for function calls, dot-repeat for ys with text objects, insert-mode <C-G>s with both delimiters inserted up front and full dot-repeat support). Every pair is user-definable, including the built-in ones — rebind ( to drop the inner spaces, " to curly quotes, or t/f/< away from their tag and function prompts
  • Hard-wrap formatting — Markdown-aware gq/gw operators with prefix preservation
  • Replace-with-register — gr{motion} replaces text with register contents without clobbering the register (vim-ReplaceWithRegister parity)
  • Yank-ring paste cycling — cycle through numbered register history with <C-p>/<C-n> after pasting. Wraps around registers "1–"9. Cancels on any non-cycling command. Dot-repeat (.) replays the final cycled text (yanky.nvim parity).
  • Table editing — cell navigation, text objects, manipulation commands, manual realignment, and native table editor integration with vim-enabled per-cell editing, cross-cell h/j/k/l navigation, and optional table-nav overlay with direct table manipulation (o, dd, J/K, H/L, =). Three modes: native with nav overlay (default), native without overlay, or raw markdown
  • Oil explorer — oil.nvim-inspired file manager: edit directories as buffers, create/rename/delete files with vim commands, nested path creation (newfolder/notes.md creates both directory and file). Matching oil.nvim keybindings: <CR> opens in same leaf, <C-t> new tab, <C-s>/<C-h> vertical/horizontal split, <C-p> preview toggle, <C-c>/q close, gx open in default app, g. toggle hidden files (blocked when unsaved changes exist), visual mode multi-select (V + <CR> opens all selected files)
  • Telescope-style picker — fuzzy finder with 15 built-in sources (files, buffers, commands, headings, outline, grep, live grep, marks, registers, tags, backlinks, recent, harpoon, snippets, Neovim quickfix), preview pane, frecency scoring, bundled integrations for Omnisearch, Obsidian Tasks, and Dataview, and a provider API for external plugin integration
  • Snippets — VS Code-compatible snippet expansion with tabstop navigation, 37 variables (full VSCode spec + $VISUAL/$WORD vim aliases), choice nodes, context filtering. 60+ bundled Obsidian snippets. User-defined snippets via JSON files or LuaSnip-inspired Lua DSL with reactive f()/d() nodes
  • 100+ ex commands — :sp, :vs, :e, :grep, :ob, :Oil, :sidebar, :move, :copy, navigation/action aliases, and more
  • Vimium-style hints — navigate the entire Obsidian UI with keyboard hints (f, F, yf, df, gf for context menu)
  • Line numbers — configurable line number gutter with absolute, relative, and hybrid modes. Neovim-compatible statuscolumn API for custom gutter layouts (vim.opt.statuscolumn = "%s %l %r %C"). Cursor line highlight with Neovim's full cursorlineopt grammar (line, screenline, number, both, and comma lists — screenline highlights only the cursor's display row of a wrapped line), configurable number width, mobile-responsive gutter, and Obsidian's native line numbers suppressed when active
  • Marks — dedicated sign column gutter showing mark letters next to marked lines, configurable via signcolumn (auto/always/off), consistent font size regardless of content, gutter layout matching Neovim (sign column → line numbers → fold column), global mark persistence across files and sessions (A–Z), and a grouped marks picker with cross-file navigation
  • Harpoon — pin files to numbered slots for instant switching (<leader>1–<leader>9), cursor position tracking, persistence across sessions, auto-updating on file rename/delete
  • Fully remappable keybindings — every keybinding can be customized via Lua or vimrc across all contexts (editor, oil explorer, picker, workspace)
  • Folding — full Neovim-style fold commands: zf/zF (create), zd/zD (delete, recursive), zE (eliminate all), zo/zO/zc/zC/za/zA (open/close/toggle, with recursive variants), zm/zM/zr/zR (incremental and global level), zn/zN/zi (fold enable/disable/toggle), zv (reveal cursor), zx/zX (reapply fold level), zj/zk (fold motion navigation with hierarchical sibling-fold semantics), [z/]z (enclosing fold boundary navigation). Custom heading fold provider trims trailing blank lines for Neovim-accurate fold ranges. Custom fold providers for frontmatter and callouts, descriptive fold placeholder text, Neovim-compatible foldopen option (structural motions like ]h, %, / auto-unfold; j/k leave folds closed — configurable via set foldopen=…), cross-session fold persistence, set foldenable toggle, and optional fold column gutter (set foldcolumn) with click-to-fold
  • Input method switching — automatic IM switching for CJK users when entering/leaving insert mode. Supports macism, im-select, fcitx5-remote, ibus, and any external binary. Platform presets for one-click setup, per-view state across all editors (split panes, popovers, canvas cards) with session persistence, composition guard, :IMToggle/:IMStatus ex commands, Lua API (vim.obsidian.im). Desktop only.
  • Vim in text areas — focused <textarea> elements in modals and plugin UIs are replaced with a vim-enabled editor overlay. Starts in insert mode for transparent typing; press Escape for normal mode, second Escape returns to modal. Experimental, disabled by default. Desktop only.
  • Cross-note jump list — <C-o> and <C-i> navigate backward/forward through jump history across notes. Jumps recorded on gd, picker selection, harpoon, oil, EasyMotion, and 100+ other navigation paths. Persists across sessions. :jumps displays the list. set jumplist/set jumplistsize for configuration
  • Undo tree — undotree-style branching undo history visualization. g-/g+ navigate chronologically across all branches with buffer content restoration. :earlier/:later by count, time, or save point. :undolist modal. Sidebar view (:UndoTreeToggle) with tree rendering, keyboard nav, collapse/expand, diff preview. vim.fn.undotree() Lua API. Optional persistence (set undofile). 5 settings: enableUndoTree, undoTreeMaxNodes, undoTreePosition, undoTreeAutoOpen, undoFile
  • Animated cursor — canvas-based smooth cursor movement and smear-cursor.nvim-style spring-damper smear trail. Per-mode cursor shapes, configurable stiffness/damping/smoothness, prefers-reduced-motion support, cross-platform resilience (3-gear frame governor, heartbeat safety net, error recovery, visibility-change wakeup, scroll tracking, dirty-rect clearing, fractional DPI rounding), and full vimrc/Lua configuration (set smoothcursor / vim.opt.smoothcursor). Disabled by default. 8 settings: animatedCursor, smoothCursor, cursorSmoothness, smearTrail, smearStiffness, smearTrailingStiffness, smearDamping, smearMaxLength
  • Subword motions — spider.nvim-style w/b/e/ge override stopping at camelCase, snake_case, and kebab-case boundaries. Full Unicode support (Arabic, CJK, accented Latin, and other non-ASCII scripts). Opt-in setting.
  • Enhanced increment/decrement — dial.nvim-style <C-a>/<C-x> cycling hex colors, booleans, dates, CSS values, and checkboxes
  • Custom text objects — define delimiter-pair text objects from Lua via vim.textobject.add() + vim.gen_spec.pair()
  • External grep — optional ripgrep or GNU grep binary for native-speed vault search in the picker. Desktop only with in-memory fallback.
  • Quality of life: Neovim defaults (Y/Q/g&/gM/K/]<Space>/[<Space>/v_*/v_#/g<C-A>/g<C-X>), 12 configurable Neovim options (ignorecase, smartcase, hlsearch, incsearch, wrapscan, gdefault, startofline, whichwrap, virtualedit, joinspaces, shiftround, nrformats), vim toggle commands (toggle-vim-mode, enable-vim-mode, disable-vim-mode), yank highlight, smart list continuation, scrolloff, insert escape sequences, chord display, powerline status bar, Neovim option compatibility (every Neovim option recognized — typos produce warnings, irrelevant options are silently accepted), and settings hot-reload

Third-party Lua compatibility (bundled runtime only): this paragraph describes the bundled fengari Lua shim, which is what runs on mobile and in bundled-fork mode. It does not describe the Neovim backend — that runs your own Neovim, so LuaJIT FFI is available and flash.nvim works there. See Neovim plugin compatibility for the backend's own boundary, which is decided by whether a plugin draws with extmarks or with screen cells. API counts measure registered surface, not semantic correctness; repairing already-real handlers leaves those counts unchanged. mini.surround and mini.splitjoin remain audit-blocked after the coordinate fixes; their behavior suites remain gated. mini.splitjoin's string expression mapping requires unavailable Vimscript evaluation, an architectural constraint rather than a missing-function to-do. The built-in surround feature above is separate. Existing mini.comment tests cover specific operations and now pin commit 27a29d6b949b9497f80a0a03421e89fed71d8c37 for reproducibility. flash.nvim is blocked in the bundled runtime by unavailable LuaJIT FFI. See API status for the audited blockers.

Installation

From community directory

Search for "Vim Motions" in Settings → Community plugins → Browse.

Manual installation

  1. Download main.js, manifest.json, and styles.css from the latest release.
  2. Create a folder vim-motions in <your-vault>/.obsidian/plugins/.
  3. Copy the downloaded files into that folder.
  4. Restart Obsidian and enable the plugin in Settings → Community plugins.

Disable Obsidian's built-in Vim mode (Settings → Editor → Vim key bindings → off). Vim Motions provides its own enhanced vim engine — a fork of codemirror-vim — with Neovim-correct behavior, async motion support, correct cursor positioning in Live Preview, and theme-aligned styling.

The plugin also works with built-in vim mode enabled, but the fork provides a more accurate Vim experience. See the recommended setup guide for details.

Documentation

Full documentation: https://saberzero1.github.io/motions

Requirements

  • Obsidian v1.8.7 or later
  • Desktop or mobile (physical keyboard recommended on mobile)

Development

npm install       # Install dependencies
npm run dev       # Development build (watch mode)
npm run build:dev # Development build (one-shot, with __DEV__ assertions)
npm run build     # Production build
npm run lint      # Lint
npm run test:unit # Unit tests (Vitest)
npm run test:e2e  # E2E tests (requires nix develop)

See CONTRIBUTING.md for the full development guide, testing strategy, and contribution guidelines.

License

MIT — Emile Bangma