Emile Bangma176 downloadsEnhances Obsidian's Vim mode with Markdown-aware text objects, structural navigation, workspace keyboard control, and a polished Neovim-native experience.
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, and a built-in .obsidian.vimrc loader.
Operate on Markdown structures with standard Vim operators (d, c, y, v).
| Keybinding | Description |
|---|---|
i* / a* |
Inside/around bold (**...**) or italic (*...*) |
i_ / a_ |
Inside/around italic (_..._) |
i` / a` |
Inside/around inline code |
i$ / a$ |
Inside/around math ($...$ or $$...$$), with smart disambiguation |
i~ / a~ |
Inside/around ~~...~~) |
i= / a= |
Inside/around ==highlight== (==...==) |
il / al |
Inside/around links ([[wikilink]] or [text](url)) |
iC / aC |
Inside/around fenced code blocks |
iB / aB |
Inside/around blockquotes (>) |
io / ao |
Inside/around callouts (> [!type]) |
it / at |
Inside/around HTML/XML tags |
All delimiter-based text objects work across multiple lines (configurable scan range, default: 20 lines in each direction). Delimiters inside fenced code blocks are excluded from the scan.
Jump between document structures. Works with counts (e.g., 3]h jumps 3 headings) and operators (e.g., d]h deletes to the next heading).
| Keybinding | Description |
|---|---|
]h / [h |
Next/previous heading (any level) |
]1–]6 / [1–[6 |
Next/previous heading of specific level |
]l / [l |
Next/previous list item (same indent level) |
]n / [n |
Next/previous link |
]b / [b |
Next/previous open buffer (tab) |
gq / gw)Reformat paragraphs with Markdown-aware line wrapping — something Obsidian's built-in Vim mode does not support.
| Keybinding | Description |
|---|---|
gqq / gwq |
Reformat current line at textwidth (default 80) |
gqj / gwj |
Reformat current and next line |
gqip / gwip |
Reformat paragraph |
Visual gq / gw |
Reformat selected lines |
gq moves the cursor to the start of the formatted range. gw keeps the cursor at its original position. Both use the same wrapping engine.
The default wrap width is 80 columns. You can change it at runtime via Obsidian's developer console: CodeMirrorAdapter.Vim.setOption('textwidth', 100). Note: set textwidth=N in .obsidian.vimrc is parsed but does not currently propagate to the gq/gw operators due to a known limitation.
Behavior:
>) — wrapped lines keep the > prefix.- , * , + ) — wrapped lines are indented to align with the text.1. ) — same alignment behavior.> - text) — both prefixes are preserved.Navigate Markdown table cells without leaving Vim mode.
| Keybinding | Description |
|---|---|
]| or ]c |
Move to the next table cell |
[| or [c |
Move to the previous table cell |
Wraps to the next/previous row when reaching the end/start of a row. Skips separator rows (|---|---|).
Note: On keyboard layouts where
|requires AltGr or a modifier key (e.g. German, Dutch, Nordic), the]\|/[\|bindings may not work. Use]c/[cinstead — they do the same thing and work on all keyboard layouts.
Navigate Obsidian without a mouse, following Neovim window management conventions.
| Keybinding | Description |
|---|---|
<C-w>h/j/k/l |
Focus pane left/down/up/right |
<C-w>v |
Split vertical |
<C-w>s |
Split horizontal |
<C-w>c / <C-w>q |
Close current tab |
<C-w>o |
Close all other tabs |
gt / gT |
Next/previous tab |
g<C-t> |
Go to tab by number (e.g., 3g<C-t> goes to tab 3) |
gd |
Go to definition — open the link under the cursor |
gx |
Open URL under cursor in browser |
gf |
Open file switcher (quick open) |
grn |
Rename current note |
grr |
Show backlinks to current note |
gra |
Show context-aware actions for cursor position |
gO |
Open document outline (searchable heading list) |
g<C-g> |
Show document statistics (words, lines, characters) |
gp / gP |
Paste and move cursor past pasted text |
ga |
Show character info under cursor (codepoint, hex) |
g; / g, |
Jump to older/newer change position |
za |
Toggle fold at cursor |
zc / zo |
Fold / unfold at cursor |
zO / zC / zA |
Recursive fold open/close/toggle |
zM / zR |
Fold all / unfold all |
Note: The
<C-w>prefix may conflict with Obsidian's default "Close current tab" hotkey. To use<C-w>bindings, go to Settings → Hotkeys, search for "Close current tab", and remove or rebind the Ctrl+W hotkey. The close-tab functionality remains available via:qor:quit.
Add, change, or delete surrounding delimiters — brackets, quotes, tags, and more.
| Keybinding | Description |
|---|---|
ds{target} |
Delete surrounding (ds" on "hello" → hello) |
dst |
Delete surrounding tag |
cs{target}{replacement} |
Change surrounding (cs"' → 'hello') |
cst{replacement} |
Change surrounding tag |
ys{motion}{replacement} |
Add surround (ysiw) on hello → (hello)) |
ys{motion}<tag> |
Surround with HTML tag (ysiw<em> → <em>hello</em>) |
ysiwf + name + Enter |
Surround with function call (print(hello)) |
ysiwF + name + Enter |
Surround with spaced function call (print( hello )) |
yss{replacement} |
Surround entire line (yss" → "line content") |
cS / yS / ySS |
Newline surround variants (delimiters on separate lines) |
S{replacement} |
Surround visual selection (visual mode) |
S<tag> |
Surround selection with tag (visual mode) |
gS |
Newline surround selection (visual mode) |
2ds), 2cs) |
Count: delete/change 2nd-level surrounding bracket |
2ysiw* |
Count: repeat delimiter (**hello** for Markdown bold) |
2ds* |
Count: delete repeated delimiter (unbold **hello**) |
<C-G>s{char} |
Insert mode: type inside delimiters, close on Esc |
Targets: ", ', `, (, ), [, ], {, }, <, >, t (tag), b→), B→}, r→], a→>
Opening brackets (, [, { add inner spaces. Closing brackets ), ], }, > don't. < in replacement position triggers tag prompting (use > for angle brackets). f/F in replacement position triggers function wrapping. Count-prefix repeats the delimiter character for quotes (2ysiw* → **word**, 2ysiw~ → ~~word~~, 2ysiw= → ==word==). All surround commands support dot-repeat (.). Requires bundled fork mode.
| Command | Description |
|---|---|
:ob {command-id} |
Execute any Obsidian command by ID |
:ob |
List all available command IDs |
:sidebar left / :sidebar right |
Toggle left/right sidebar |
:explorer |
Reveal active file in file explorer |
:w / :write |
Save current file |
:update / :up |
Save current file (alias for :w) |
:q / :quit |
Close current tab |
:wq |
Save and close |
:x / :xit |
Write if modified and close |
:xa / :xall |
Write if modified all and close all |
: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 |
:find {file} / :fin |
Find and open file by partial name match |
:read {file} / :r |
Insert file contents at cursor position |
:bn / :bp |
Next / previous tab |
:b {name} / :buffer {name} |
Switch to tab matching name |
:bf / :bfirst |
Go to first tab |
:bl / :blast |
Go to last tab |
:bd / :bc |
Close current tab |
:bw / :bwipeout |
Close current tab |
:only |
Close all other tabs |
:qa |
Close all tabs |
: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 |
:buffers / :ls |
Show all open buffers in a modal |
:backlinks |
Show backlinks to the current note in a modal |
:grep {pattern} |
Search vault for text, show results in a modal |
:wa / :wall |
Save all open files |
:back / :forward |
Navigate back / forward in history |
:reg / :registers |
Show register contents in a modal |
:marks |
Show marks and their positions in a modal |
:delmarks {marks} |
Delete specified marks |
:changes |
Show change list in modal |
:version / :ve |
Show plugin version |
Jump to any visible position with two keystrokes.
Find motions:
| Keybinding | Description |
|---|---|
<leader><leader>f{char} |
Find {char} forward |
<leader><leader>F{char} |
Find {char} backward |
<leader><leader>s{char} |
Find {char} in both directions |
<leader><leader>t{char} |
Till before {char} forward |
<leader><leader>T{char} |
Till after {char} backward |
Word motions:
| Keybinding | Description |
|---|---|
<leader><leader>w |
Word start forward |
<leader><leader>b |
Word start backward |
<leader><leader>e |
End of word forward |
<leader><leader>ge |
End of word backward |
<leader><leader>W |
WORD start forward |
<leader><leader>B |
WORD start backward |
<leader><leader>E |
End of WORD forward |
<leader><leader>gE |
End of WORD backward |
Line motions:
| Keybinding | Description |
|---|---|
<leader><leader>j |
Line down |
<leader><leader>k |
Line up |
Search motions:
| Keybinding | Description |
|---|---|
<leader><leader>n |
Next search match forward |
<leader><leader>N |
Next search match backward |
All easymotion motions work in visual mode — v + easymotion extends the character selection, V + easymotion extends the line selection. Operator-pending mode (d + easymotion, c + easymotion, y + easymotion) works natively via the fork's async motion support.
All easymotion actions can be remapped in .obsidian.vimrc. Bidirectional variants (easyMotionBdWord, easyMotionBdFind, etc.) and repeat (easyMotionRepeat) are also available as named actions.
Navigate the entire Obsidian interface without a mouse. Press <leader><leader>h (or a configurable global hotkey) to label every clickable element on screen — buttons, tabs, sidebar items, settings controls, editor panes, links — then type the label to activate it.
setActiveLeaf for proper Obsidian focus)[[wikilinks]] and [markdown](links) are opened via Obsidian's link resolver, not raw clickvim-motions:show-hint-labels — assign a hotkey in Settings → Hotkeys or trigger from the command paletteY yanks to end of line (y$) and Q replays last recorded macro (@@), matching Neovim's defaults instead of CM Vim's legacy behavior.2d, gq) in the status bar as you type a multi-key command.--vim-pl-normal-bg, etc.).d shows available motions/text objects, g shows g-prefixed commands, etc.: command line.jk, jj, or any two-key sequence to exit insert mode via set insertmodeescape=jk in your vimrc..obsidian.vimrc — load key mappings and settings without needing obsidian-vimrc-support.Vim Motions has built-in support for .obsidian.vimrc files, compatible with obsidian-vimrc-support syntax. Place a .obsidian.vimrc file in your vault root:
" Example .obsidian.vimrc
let mapleader = " "
" Key mappings
nnoremap j gj
nnoremap k gk
nmap Y y$
" Leader key mappings
exmap saveFile obcommand editor:save-file
nmap <leader>w :saveFile
" Execute Obsidian commands
nmap <C-s> :saveFile
" Settings
set clipboard=unnamed
set tabstop=4
set textwidth=80
set shiftwidth=2
set expandtab
set insertmodeescape=jk
Supported commands: map, nmap, imap, vmap, noremap, nnoremap, inoremap, vnoremap, unmap, nunmap, iunmap, vunmap, set, let mapleader, exmap, obcommand, source.
Supported set options: clipboard (unnamed/unnamedplus — syncs yank/delete/paste with system clipboard), tabstop/ts, textwidth/tw, shiftwidth/sw, expandtab/et, insertmodeescape/ime. Use set noexpandtab to disable boolean options.
If obsidian-vimrc-support is also installed, Vim Motions skips its own :ob command registration to avoid conflicts.
All features can be toggled independently in Settings → Vim Motions. Changes take effect immediately without restarting.
gq/gw (on/off).obsidian.vimrc (on/off)Search for "Vim Motions" in Settings → Community plugins → Browse.
main.js, manifest.json, and styles.css from the latest release.vim-motions in <your-vault>/.obsidian/plugins/.Disable Obsidian's built-in Vim mode (Settings → Editor → Vim key bindings → off). When built-in vim is off, Vim Motions provides its own enhanced vim engine (a fork of codemirror-vim) with:
dd cursor positioning, J join whitespace, di{ multiline brackets, dj/dk at document boundaries, :s cursor, % string-awareness, db/d2w cross-line whitespace, and mored + easymotion, c + easymotion, y + easymotion)--interactive-accent)The plugin also works with built-in vim mode enabled — it extends whatever vim engine is active. But the fork provides a more accurate Vim experience.
# Install dependencies
npm install
# Development build (watch mode)
npm run dev
# Production build
npm run build
# Lint
npm run lint
# E2E tests (requires nix develop on NixOS, or system libraries for Electron)
npm run test:e2e
# Coverage report (command-level test status)
npm run test:coverage
The plugin uses a Neovim-backed golden comparison system inspired by Zed editor's Vim test architecture. Every Tier 1 Vim command (motions, operators, text objects, insert mode, visual mode) is tested against a real headless Neovim instance to verify behavioral parity.
How it works:
nvim --embed --headless and connected over msgpack-RPC.Test types:
[nvim] tests — Neovim-compared via golden files. No hand-written expected values; Neovim's output is the spec.[obsidian] tests — viewport-dependent behavior (H/M/L, scroll, folds) that headless Neovim cannot verify.Available commands:
# Run Neovim smoke test (verify client works)
npm run test:neovim-smoke
# Record golden files from Neovim (requires nvim binary)
npm run test:neovim-record
# Run tests with live Neovim comparison (requires nvim binary)
npm run test:neovim-compare
Intentional behavioral deviations from Neovim are documented in test/neovim/deviations.ts and KNOWN_LIMITATIONS.md. The deviation registry serves as the roadmap toward full Neovim parity — when it's empty (minus intentional overrides like Y→y$), the goal is achieved.
MIT — Emile Bangma