confuserdotcom44 downloadsSpelling keys for Vim mode (]s [s z= zg zw), numbers that count what gj and gk move by on wrapped lines, and Ctrl-N / Ctrl-P in menus.
The Vim keys Obsidian forgot.
Fix typos with ]s and z=, count wrapped rows for gj, and drive every menu with Ctrl-N / Ctrl-P.
Spelling keys · Screen-row numbers · Ctrl-N / Ctrl-P · vimrc · Install
Three small additions to Obsidian's Vim mode, each with its own switch in the settings:
]s [s z= zg zw, like Vim's, on Obsidian's own spellchecker.gj and gk really move by on wrapped lines, also beside rendered maths.Requires Vim mode: Obsidian's built-in Settings → Editor → Vim key bindings, or the Vim Motions plugin. Desktop only (the spellchecker is Electron's).
▶ Watch the full-quality video
| Keys | What they do |
|---|---|
]s [s |
next / previous misspelled word; counts (3]s), operators (d]s) and visual mode work; wraps around the note like Vim |
z= |
fixes for the word under the cursor: 1–9 picks one, Ctrl-N / Ctrl-P + Enter too, Esc cancels |
3z= |
take fix 3 straight away (1z= = the best one) |
zg |
add the word to the dictionary |
zw zug |
take it back out |
$…$, $$…$$), code, frontmatter, links and their targets, URLs, file names, tags, comments, HTML, LaTeX commands, one-letter words and short ALL-CAPS acronyms are skipped.zg / zw write to the same dictionary as the editor's right-click menu.]s and z= only read caches.]s sees them: the whole note at once, never inside maths or code. It hides the built-in underline in the editor.z= ranks the fixesIt gathers every spelling one edit away (plus any accents: theoreme → théorème, aparaitre → apparaître) that the dictionary accepts, with Chromium's own suggestions, and ranks them by:
problemme) less than a random letter, and the key next door less than any other key (QWERTY, AZERTY or QWERTZ, read from the system);mond → monde before mont or gond;theorme → theorem, in a French one théorème;surjective, Cauchy) are known;du mond blanc → mont, le mond entier → monde;The typo's capitals are kept (Bonjuor → Bonjour).
Word lists takes files of your vault, one per line. Each file has one word per line, best followed by how common it is, like the frequency lists of FrequencyWords (content/2018/fr/fr_50k.txt: monde 277005). Name each file after its language: fr_50k.txt, en_50k.txt, french.txt (files of the same language are merged).
With lists of two or more languages:
z= ranks that language's words first and pushes the other language's down: an English paragraph never gets French fixes first, and a note can mix both;EN, FR); click it, or run Set the spelling language of this note, to set one for the whole note. That writes lang: en in the note's frontmatter (shown in the accent colour); "Detect per paragraph" removes it.Which words get underlined is still up to the system spellchecker, which accepts every language it has: a French word in an English note is not underlined, it is only never offered as a fix. On macOS the system checks every language you have, so it may take a word of another language as correct (helo, mond).
Learn from your notes (on by default) counts the words of your notes and which words follow which, and remembers the fixes you take, in the plugin's own data; the button next to it forgets the picks.
What z= knows (the word lists, your notes' words and word pairs) is compiled into one file, model.bin in the plugin folder: sorted tables and typed arrays, searched in place.
onload does no spelling work. Once Obsidian is idle, it reads model.bin and takes views on its bytes, without parsing lists or building tables: about 1 ms to decode, 8–16 ms with the disk read, measured on 150 000 words and 800 notes. Only the notes changed since the last session are read again.model.bin is a cache of this device: keep it out of sync (Syncthing, Obsidian Sync, git); each device builds its own.The three commands (Spelling suggestions, Add word to dictionary, Remove word from dictionary) also work from the command palette.
Notes wrap, so most people in Obsidian move with gj / gk (or map j / k to them): 5gj goes 5 screen rows down, and no line numbers say where that is. This draws a relative number on every screen row in the left margin:
gj (or gk) land there; it comes from running Vim's own vertical motion, so rows beside rendered inline maths are skipped and a rendered $$ block shows the count that reaches it, just like the cursor moves;set number relativenumber);gj stays smooth.The numbers sit in the margin left of the text: turn on Settings → Editor → Readable line length so there is room. Toggle screen-row numbers is a command, to bind to a key. Resize them with a CSS snippet:
body { --vim-extras-row-size: 19px; --vim-extras-row-cur-size: 21px; }
The quick switcher and the other suggesters already take Ctrl-N / Ctrl-P; menus (right-click menu, file menu, the z= list…) only took the arrow keys, and Ctrl-N fell through to Create new note. Now they move the selection. Needs Settings → Appearance → Native menus off: native menus are drawn by the system and never see the keys.
The spelling keys need counts, so they are plain Vim keys, not commands: don't map z= or ]s in your vimrc, a map would shadow them. To bind the row numbers toggle, with Vim Motions:
nnoremap <leader>tn :obcommand vim-extras:toggle-row-numbers<CR>
With Vimrc Support:
exmap rownumbers obcommand vim-extras:toggle-row-numbers
nmap <Space>tn :rownumbers<CR>
Until it is in the community plugins list, either:
confuserdotcom/obsidian-vim-extras, ormain.js, manifest.json and styles.css from the latest release into <vault>/.obsidian/plugins/vim-extras/, then enable Vim Extras in Settings → Community plugins.No build step: main.js is the plugin.
npm test # unit tests for the spelling keys
node e2e.mjs # real key presses in a running Obsidian (see the header of e2e.mjs)
node e2e-isolated.mjs # the same in a separate Obsidian with a throwaway vault (macOS)
tools/eval.js measures how often z= puts the right fix first, against releases 1.0.0 and 1.1.0, on real typos mined from a vault's git history (tools/mine-typos.py) and on synthetic ones:
node tools/eval.js <vault> tools/typos.json fr_50k.txt en_50k.txt
node --expose-gc tools/bench.js <vault> fr_50k.txt en_50k.txt # build, size, load, edit, save, z=
npm test also checks that the incremental path lands exactly where a full rebuild does (200 random runs of notes edited, deleted and renamed), and that the worker's source builds the very same bytes.
The plugin is free and stays free. If it saves you time, you can support it here.
If Vim Extras saves you a trip to the mouse, a ⭐ on GitHub helps other Vim users find it.
Bugs and ideas: open an issue.