Learn Vim keybindings hands-on, inside Neovim.
vimro shows you a start text and a goal text. Edit the buffer until it matches the goal — any sequence of keys that gets you there counts. Hints and solutions stay hidden until you ask for them.
日本語版 README はこちら / Japanese README
- Drill-style problems: transform
startintogoal, cleared automatically on match - Any key sequence counts — only the final buffer state is checked
- Hints and solutions on demand (
?/s), never shown by default - Japanese / English UI, per-problem translations with fallback
- Progress saved locally; keystroke count shown on clear
- Problems are plain JSON files — easy to contribute
- Neovim 0.9+
vimro is installed with lazy.nvim. If you don't have it yet, follow the lazy.nvim installation guide first (Neovim distributions such as LazyVim and kickstart.nvim already include it).
Add this spec wherever your plugins are declared:
-
Using a distribution like LazyVim / kickstart.nvim (your
init.luacontains something likerequire("config.lazy")orrequire("lazy").setup("plugins")) — create a new filelua/plugins/vimro.luain your config directory (~/.config/nvim, or~\AppData\Local\nvimon Windows):-- ~/.config/nvim/lua/plugins/vimro.lua return { "uswebk/vimro.nvim", cmd = "Vimro", opts = {}, }
-
Plugins listed directly in
init.lua— add it inside the table passed torequire("lazy").setup({ ... }):require("lazy").setup({ -- ...your other plugins... { "uswebk/vimro.nvim", cmd = "Vimro", opts = {}, }, })
Restart Neovim (or run :Lazy sync) to install. Then run :Vimro to start.
Notes:
cmd = "Vimro"lazy-loads the plugin the first time you run:Vimro.opts = {}uses the default settings. For a Japanese UI, useopts = { lang = "ja" }. See Configuration for all options.
- Run
:Vimro(in normal mode, press:, typeVimro, then hit Enter). Or launch it straight from your terminal withnvim +Vimro— addalias vimro='nvim +Vimro'to your shell config to start drilling with a single word. - Pick a UI language (
ja/en) and a category (MVP shipsplain). - The screen splits: edit the left buffer until it matches the goal described in the right pane.
- Clearing is detected automatically; press
nin the right pane for the next problem.
Keys in the problem pane (right side):
| Key | Action |
|---|---|
n |
next problem |
p |
previous problem |
r |
reset the practice buffer |
? |
toggle hints |
s |
toggle solutions |
g |
pick a problem from the list (jump to any problem) |
q |
quit |
The same actions are also available from the practice buffer with a prefix, so you don't have to switch panes: <leader>n for next, <leader>r for reset, and so on. <leader> depends on your config — Space in LazyVim, \ by default — and the problem pane shows the resolved key (e.g. "Press Space before each key"). Plain n / r etc. keep their normal Vim meaning in the practice buffer — that's what you are here to train. The prefix is configurable via practice_prefix (set it to false to disable).
Matching ignores trailing whitespace on each line and trailing blank lines; line contents and line count are otherwise strict. How you get there — which keys, how many — is up to you.
Progress (solved problem ids) is saved to stdpath("data")/vimro/progress.json (e.g. ~/.local/share/nvim/vimro/progress.json).
Defaults shown; every field is optional.
require("vimro").setup({
lang = "en", -- default UI language: "ja" | "en"
fallback_lang = "en", -- used when a translation is missing
keys = { -- problem-pane keymaps
next = "n",
prev = "p",
reset = "r",
hint = "?",
solution = "s",
list = "g",
quit = "q",
},
practice_prefix = "<leader>", -- prefix for the same actions in the practice buffer
-- (e.g. <leader>n = next); set to false to disable
pane_width = 42, -- width of the problem pane (columns)
})Problems live in problems/<category>/NNN-<slug>.json, one file per problem:
- Fill the language-independent fields:
id,category,difficulty,start,goal,cursor(1-based[row, col]),solutions(keysin Vim notation like<Esc>, mark the shortest with"optimal": true),tags. - Fill both
i18n.jaandi18n.en(title,description,hints,notes).notes[i]describessolutions[i]. - Verify in Neovim that each
solutions[].keysreally transformsstartintogoal. - Aim for one target operation per problem, and keep
descriptionconsistent withgoal.
See problems/plain/001-delete-word.json for a complete example.
