=============================================================================== VIM PRACTICE FILE companion to "Vim From Scratch" heiberg.co.za ยท Insights =============================================================================== HOW TO USE THIS FILE 1. Open it: vim vim-practice.txt 2. Wreck it freely. Every drill is meant to be edited. 3. Reset any time: :e! (discards all changes, reloads the file) 4. Work one part at a time. The drills are ordered to match the series. Each drill looks like this: >> PLAY the line you operate on == WANT what it should look like afterwards Do the PLAY line with the keys named. Compare against WANT. If you got there with different keys and fewer of them, yours is better -- that is the whole skill. A note on counts: almost every command below takes a number in front. 3dw deletes three words, 5j goes down five lines, 2ci" changes inside the second quote pair. Try adding counts once the base motion is comfortable. =============================================================================== PART 2 -- MODES =============================================================================== DRILL 2.1 Entering insert mode five ways i before cursor a after cursor I start of line A end of line o new line below O new line above Put the cursor on the word "middle" in the PLAY line and use each in turn. Press Esc after every one. (Or jk, if you set that mapping in Part 9.) >> PLAY start middle end DRILL 2.2 Replace and change r replace one character R replace mode (overtype) ~ toggle case cc change the whole line >> PLAY the qulck brown fox == WANT the quick brown fox (r on the l) >> PLAY make this line shout == WANT MAKE THIS LINE SHOUT (V then U, or g~~) DRILL 2.3 Undo, redo, and the dot u undo Ctrl-R redo . repeat the last change Delete a word with dw, then press . four times. Then u five times. The dot command is the most under-used key in Vim. Get it in your fingers now, because half of Part 4 depends on it. >> PLAY alpha beta gamma delta epsilon zeta eta theta =============================================================================== PART 3 -- MOTIONS =============================================================================== DRILL 3.1 Word motions w next word start b previous word start e end of word W B E same, but a "word" is anything between spaces Move along this line using only w. Then only b. Notice where w stops compared to W -- punctuation is the difference. >> PLAY config.lua:42 -- set('n', 'ff', builtin.find_files) DRILL 3.2 Line motions 0 first column ^ first non-blank $ end of line g_ last non-blank >> PLAY indented line with trailing spaces here Try 0 then ^ and watch the difference. DRILL 3.3 Find a character on the line f forward to it t forward until just before it F backward to it T backward until just after it ; repeat the find , repeat it backwards Task: change "mat" to "rug" using f, then jump to the third comma with 3f, >> PLAY the cat sat on the mat == WANT the cat sat on the rug (f m then cw rug, or fmce rug) >> PLAY one, two, three, four, five DRILL 3.4 Bigger jumps gg top of file G bottom 42G or :42 line 42 { paragraph back } paragraph forward % matching bracket H M L top / middle / bottom of the visible screen Ctrl-D Ctrl-U half a screen down / up Put the cursor on the first ( below and press % repeatedly. >> PLAY def total(items, rate=(1 + tax["vat"]), *, rounding=None): DRILL 3.5 The jumplist Ctrl-O back to where you were Ctrl-I forward again Press gg, then G, then 50G, then Ctrl-O three times. You just walked backwards through your own history. This works across files too, which is what makes go-to-definition free in Part 11. =============================================================================== PART 4 -- OPERATORS AND TEXT OBJECTS =============================================================================== The grammar: operator + motion-or-text-object OPERATORS d delete c change y yank > indent < outdent gu lower gU upper g~ toggle = reformat OBJECTS iw inner word aw a word (plus a space) i" inside quotes a" quotes included i( inside parens a( parens included i{ i[ it same for braces, brackets, XML tags ip inner paragraph ap a paragraph DRILL 4.1 Inner vs a Cursor anywhere inside the quotes. Try ci" then u, then ca" then u. The difference is whether the delimiters go too. >> PLAY local greeting = "hello there" .. suffix == WANT local greeting = "goodbye" .. suffix (ci" goodbye) DRILL 4.2 Parentheses Cursor anywhere inside the argument list. >> PLAY result = calculate(alpha, beta, gamma) == WANT result = calculate(x, y) (ci( x, y) DRILL 4.3 Delete a whole paragraph Cursor anywhere in the block. dap removes it and the blank line after; dip leaves the blank line. >> PLAY This paragraph exists purely to be destroyed. It has several lines so that you can see the difference between dip and dap clearly. Try one, undo, try the other. >> END OF PLAY DRILL 4.4 Compose your own Work out the shortest sequence for each of these, then do it. >> PLAY const timeout = 3000; // milliseconds == WANT const timeout = 500; // milliseconds >> PLAY badly spaced arguments here == WANT badly spaced arguments here >> PLAY keep this text == WANT keep this text (ci" works) =============================================================================== PART 5 -- SEARCH, SUBSTITUTE, AND JUST ENOUGH REGEX =============================================================================== DRILL 5.1 Search /pattern forward ?pattern backward n next N previous * search for the word under the cursor # same, backwards Put the cursor on any "handler" below and press *. Then n a few times. >> PLAY def handler(request): # handler one def other(request): # not a handler def handler_v2(request): # handler two def cleanup(request): # calls handler internally DRILL 5.2 Substitute :s/old/new/ this line, first match :s/old/new/g this line, all matches :%s/old/new/g whole file, all matches :%s/old/new/gc ...with confirmation on each :'<,'>s/old/new/g just the visual selection Task: turn every "colour" below into "color", but only in this block. Select it with V, then use :'<,'>s/colour/color/g >> PLAY set colour scheme the colour of the border colour background-colour: red; DRILL 5.3 Just enough regex . any character * zero or more of the previous \d a digit \w a word character \s whitespace ^ start of line $ end of line \( \) a capture group, referred to as \1 \2 in the replacement \zs start the match here (everything before is context, not replaced) Task 1: swap the two columns. :%s/\(\w\+\),\(\w\+\)/\2,\1/ Task 2: strip trailing spaces. :%s/\s\+$// Task 3: number the lines. :%s/^/\=line('.') . '. '/ >> PLAY alpha,one beta,two gamma,three DRILL 5.4 \zs, the one worth knowing Replace only the number, keeping "port " as context: :%s/port \zs\d\+/9000/ >> PLAY listen on port 8080 and forward to port 3000 =============================================================================== PART 6 -- REGISTERS AND MACROS =============================================================================== DRILL 6.1 Named registers "ayy yank this line into register a "ap put register a :reg show every register and its contents Yank the first line into a, the second into b, then paste them in reverse order below. >> PLAY first line goes into register a >> PLAY second line goes into register b DRILL 6.2 The black hole and the yank register "_dd delete without touching any register "0p paste the last YANK (not the last delete) This solves the classic annoyance: yank a word, delete another word, try to paste -- and get the deleted one. Try it, then fix it with "0p. >> PLAY keep_this_word and_delete_this_one DRILL 6.3 Macros qa start recording into register a ... do the edits q stop recording @a play it back @@ play back the last macro 5@a play it five times Task: record a macro on the first line that turns it into the WANT form, then run it on the rest. The trick is to start with 0 and end with j, so the macro leaves the cursor ready for the next line. >> PLAY alice admin bob editor carol viewer dave admin == WANT { name = "alice", role = "admin" }, { name = "bob", role = "editor" }, DRILL 6.4 Edit a macro Macros live in registers, and registers are text. Record something, then "ap to paste it, fix the mistake by editing the characters, then "ayy to load it back. This is why the register system is worth knowing. =============================================================================== PART 7 -- MARKS, JUMPS, BUFFERS, WINDOWS =============================================================================== DRILL 7.1 Marks ma set mark a here 'a jump to the line of mark a `a jump to the exact spot :marks list them `. where you last edited `` where you were before the last jump Set mark a here. Then go to the bottom of the file with G. Then 'a. DRILL 7.2 Windows :vsplit or Ctrl-W v vertical split :split or Ctrl-W s horizontal split Ctrl-W h j k l move between them Ctrl-W = equalise sizes Ctrl-W o close all the others :q close this one Split this file twice, navigate between the panes, then close them. DRILL 7.3 Buffers :e file open one :ls list buffers :bn :bp next / previous :b 3 go to buffer 3 :bd close a buffer =============================================================================== PART 8 -- EX COMMANDS AND THE QUIET MAGIC OF :g =============================================================================== Ranges first, because everything below takes one: :5,10cmd lines 5 to 10 :.,$cmd here to end of file :%cmd the whole file :'<,'>cmd the visual selection :.,+3cmd here and three more DRILL 8.1 Move, copy, join :m move :t or :co copy :j join lines :m0 move this line to the top :m$ move it to the bottom :t. duplicate this line below >> PLAY duplicate me with :t. DRILL 8.2 :g -- run a command on every matching line :g/pattern/d delete every line matching :v/pattern/d delete every line NOT matching (v = inverse) :g/pattern/normal A; append a ; to every matching line :g/pattern/m$ move every matching line to the end Task 1: delete every DEBUG line. :g/DEBUG/d Task 2: keep only the ERROR lines. :v/ERROR/d Task 3: move every WARN line to the end. :g/WARN/m$ >> PLAY 2026-08-26 10:01 INFO server started on port 8080 2026-08-26 10:01 DEBUG loading config from /etc/app.yaml 2026-08-26 10:02 ERROR failed to connect to database 2026-08-26 10:02 DEBUG retrying in 5s 2026-08-26 10:03 WARN connection pool nearly exhausted 2026-08-26 10:03 INFO connected 2026-08-26 10:04 ERROR timeout on /api/orders 2026-08-26 10:04 DEBUG closing idle connections 2026-08-26 10:05 WARN disk usage at 91% DRILL 8.3 :g with normal mode -- the power combination :g/^def /normal O# TODO: document this That inserts a comment line above every function definition. Try it: >> PLAY def load(path): pass def save(path, data): pass def delete(path): pass DRILL 8.4 :sort and :sort u Select the block with V, then :'<,'>sort and then try :'<,'>sort u >> PLAY gamma alpha beta alpha delta beta DRILL 8.5 Read and write :r file read a file in below the cursor :r !date read the output of a shell command :w /tmp/x write this buffer somewhere else :.,+5w /tmp/y write just these lines =============================================================================== WHEN THIS FILE STOPS BEING USEFUL =============================================================================== When you can do most of the drills above without reading the key hints, the file has done its job. The next step is not more drills -- it is noticing, during real work, the moment you do something repetitive by hand. That moment is a macro, or a :g, or a text object you have not learned yet. Keep this file around anyway. It is a good place to test a new mapping without risking anything that matters. The Modal IDE series picks up from here: the same grammar, applied to a real development environment. Its practice file is modal-ide-practice.py, which needs a language server and a debugger rather than just an editor. ===============================================================================