Transcription of an Impressive Vim Programming Session

Jordan Mandel
12 min readSep 9, 2024

--

Intro

I saw a Reddit post in the r/vim showing a guy using vim fairly adeptly (with a cool soundtrack). He was certainly a better Vimmer than I am.

The original video is found in this Reddit post.

The Medium profile of they guy who posted the video.

It is at 2X speed, so I had to download it to go through it frame by frame in Kdenlive to see what was going on.

Here is the sequence of what he does, with commentary:

Note the focus is on how he uses Vim an not what the code actually does, which I’m still not sure about

Clones the repo:

git clone https://github.com/webassembly/wasi

I checkout the commit he might be on

cd wasi && git checkout e69a158 && cd .. seems to get me to a file identical to the one being worked on in the video.

Makes new directory

Keep in mind that this is not inside the wasi directory but on the same level as it.

mkdir web-wasi
cd web-wasi

Opens a file in the original Repo

Not sure if it is Vim or Neovim the alias vi is used. For my purposes I am running nvim --clean to avoid interference from my plugins, and because I prefer the Neovim defaults, but will still write vi in the transcribed lines below. However, when referencing Vim/Neovim outside of the literal transcription I will call it Vim.

vi ../wasi/phases/snapshot/witx/wasi_snapshot_preview1.witx

Saves a Copy

Uses :sav wasi_snapshot_preview1.js

This writes a file as the first argument and then makes that file the current buffer. In this case it is in the newly created directory because that is where vim was run from. Saving it as .js sets the filetype and introduces new syntax highlighting. Same as :saveas.

Uses Global Command

Uses the :global command to delete all lines that have ;; and errno

:g/;;/d
:g/errno/d

Note that the number of ‘fewer lines’ in the output of these commands is identical to the numbers in the video when run from commit e69a158. 219 and 44 respectively.

Note that the final part of the :global command, d is an Ex command that you’d enter in : rather than normal mode commands. This is true in all cases. To use normal mode commands, use normal [normal commands here] in the last part. See :h normal.

Does a substitution:

:%s/(result $\(\w\+\).*)/\1_out/g

(result matches the those characters literally.

As this is typed out, if incsearch and hlsearch are set, once $ is typed out the matches will disappear until the terminating \) is typed. To see why this is see :h /\$:

$   At end of pattern or in front of "\|", "\)" or "\n" ('magic' on):
matches end-of-line <EOL>; at other positions, matches literal '$'.
|/zero-width|
                            */\$*
\$ Matches literal '$'. Can be used at any position in the pattern, but
not inside [].

When $ is first typed the search looks for end-of-line, which doesn’t exist after (result in the text of wasi_snapshot_preview1.js

Then:

The \( and \) characters delimit a pattern that can then be referenced later in the same regular expression; the regex symbols \1 through \9 which reference the \(/\) pairs in the order they appear. See :h /\(. This is used in the replacement text [see below].

So the highlighting doesn’t return until the closing \) because it doesn’t know what pattern to match until that character signals the end of the pattern, even though once any character after $ is typed it stops treating it as end-of-line.

  • \w matches a single word character (alphanumeric or underscore).
  • \w\+ matches one or more word characters.

Note that the definition of word character for the purpose of regular expressions is different than the definition used for motions. Word characters for the purposes of motions like b and e called keyword characters and are customized by the iskeyword setting. See :h iskeyword and :h word. To reference keyword characters in a regular expression in Vim use the \k identifier.

I wonder if this behavior might be different on Windows where line endings do incorporate the \r character

  • . matches any character except for end-of line (see :h /.)
  • .* matches any character except for end-of-line zero or more times.

The zero or more times detail is subtle. To see an example of this in action run nvim --clean and enter 10o<Esc> to add ten blank lines, followed by :%s/.*/ItMatchesZeroTimes

  • ) matches a literal left facing parenthesis.

/\1_out/ - This is the replacement text. The 1 refers to the first capturing group in the search pattern, which is the one or more word characters captured by \(\w\+\) . So, 1_out will replace the captured word characters with the same characters followed by _out .

/g is a flag that tells the :s command to replace all matches on a given line, not just the first one.

This should result in 23 substitutions.

attempt to Simplify with verymagic

This command can also be written using the \v flag:

%s/\v\(result \$(\w+).*\)/\1_out

From the documentation :h /\v:

*/\v* */\V*
Use of "\v" means that after it, all ASCII characters except '0'-'9', 'a'-'z',
'A'-'Z' and '_' have special meaning: "very magic"

So basically all non-alphanumeric characters except for the underscore automatically take on their special ‘regular expression’ role and to refer to them literally they must be escaped with \. In the case of the substitution used in the video there isn’t a real advantage to this approach (equally complicated to type) but it’s good to know about.

Another Substitution

:%s/(param $\(\w\+\).*pointer.*)/\1_ptr/g

The verymagic version of this is

%s/\v\(param \$(\w+).*pointer.*\)/\1_ptr/g

Should result in ten substitutions.

Opens Command History With q:

Changes the command from the previous section to:

%s/(param $\(\w\+\).*iovec_array.*)/\1_ptr\r\1_len/g

and runs it by pressing enter on it’s line in the command history.

Note that \r is a return character, <CR>, creating a line break. See :h /\r. Note further that \n for newline won’t work in the replacement text; It inserts null characters, represented by ^@, into the file. This behavior is is documented under :h sub-replace-special but I wish they would also include it under :h \/n to prevent confusion. For advice on dealing with those kinds of characters if you see them see this StackOverflow question

Note even further that in the search part of the :s command, \n does in fact search for the newline and \r doesn’t seem to find anything. This is in spite of the fact that :h /\r says it matches <CR>. This distinction has to do with subtle distinctions with text encoding that generally confuse me. \n is used in the search part of a substitution command below.

The command should make six substitutions.

Then, in the command history window, changes that command to:

%s/(param $\(\w\+\).*string.*)/\1_ptr\r\1_len/g

and runs it. This should make 13 substitutions.

Again goes to the command window with q: and goes:

%s/(param $\(\w\+\).*/\1_ptr/g

This should result in 78 substitutions.

Similarly he runs:

%s/(@interface.* "\(\w\+\).*/export function \1(/g

which results in 45 substitutions.

Runs some normal mode delete commands

Deletes the first 3 lines of the file with 3dd

He then runs a command that does two things:

  1. formats the buffer, left justifying some lines, inserting tabs in front of function parameters,
  2. deletes the leading parenthesis of the file on the first line (and presumably the matching parenthesis at the end of the line; later he goes down and it’s not there so this command seems to have deleted it)

but I’m not sure what that command was. It also briefly says '283 fewer lines' right here, perhaps from whatever formatting command he uses deleting and replacing the text.

He then deletes the line that had the leading parentheses with dd.

Instead to go along with what he does I:

  1. Delete the line that had the leading parenthesis with dd before formatting
  2. Comment out the line (import "memory" (memory)) by prepending \\. This seems to make it approximate valid JS syntax enough for formatting.
  3. With my cursor on the top line in normal mode, enter =G from the top line to indent the whole file.
  4. Go down to the bottom of the file and delete the last parenthesis.

This is just what worked to get formatting seemingly identical to what is in the video, not a comment on the JavaScript syntax of the file, which I don’t know about.

He then edits (import "memory" (memory)) to be

import { memory } from "program";

Runs another Command

This is a very creative command. It captures words right after the sequence newline, tab and then replaces them with what it captured, followed by comma space. It just so happened that all function parameters matched this pattern, and it converts them to being comma + space delimited and on the same line. They are on the same line because the \n character got replaced. This is the command where we search for \n instead of \r as mentioned above.

%s/\n\t\(\w\+\)/\1, /g

So

export function args_get(
argv_ptr
argv_buf_ptr
)

becomes

export function args_get(argv_ptr, argv_buf_ptr, 
)

At this point he inspects the file, seemingly to make sure everything he just did worked. This is where I notice that the final parenthesis of the file is not there.

Runs A Command to Move Ending Parentheses Up One Line, adds default return value of 0

%s/, \n)/)\r{\r\treturn 0;\r}\r/g

Before the this command there are many functions declarations with trailing parentheses on their own lines like:

export function args_get(argv_ptr, argv_buf_ptr, 
)

which becomes

export function args_get(argv_ptr, argv_buf_ptr)
{
return 0;
}

This results in 44 substitutions. Note that this command doesn’t effect commands with no arguments because they have no comma. He enters insert mode then manually changes the function in the file that was missed to match the others.

Opens a File in New Split Window, yanks from it

:vs ../wasi/phases/snapshot/witx/typenames.witx

He searches forward for enum with /enum, moves up one line to line 17, goes to the first character of that line, and searches backwards this time for enum with ?enum and, which lands the cursor at line 725 because it wraps around to the bottom. He goes down a few lines, still in visual mode to line 732, and presses y to yank 717 lines.

He deletes the split with <C-w>q so he is in wasi_snapshpt_preview1.js. He puts the lines into the new file by going to line 3 and pressing P in normal mode to add them above line 3.

Starts recording a macro

Into register q by pressing qq in normal mode.

So starting from before a typename declaration block he starts recording, searches forward to / \$ (recall the special meaning of an unescaped $ previously described, so it is escaped here for to match $ literally), then back to ?typename, then gets to the beginning of clockid or whatever comes after $ (I’ll do it using ww) and yanks to the end (I’ll use ye). He then searches forward for / \$ again, puts the yanked text after the $ (wp), appends an underscore right after with a_<Esc>, goes to the beginning of the line and deletes the dollar sign (bbx), and finishes recording the macro by pressing q.

When I finish recording the macro for myself I get

/  \$^M?typename^Mwwye/  \$^Mwpa_^[bbx

where ^M is my pressing <Return> or <Enter>, and ^[ is my pressing <Esc>. Note that ^M is a single character, rendered in a different color in Vim, and ^[ is also a single character. I’ve just written them out as two characters here so that they are rendered properly.

See: here and here

He runs the macro once to test it with @q and after seeing that it worked, 200 times with 200@q.

It runs until the search / $ fails

Deletes semicolon comments again

:g/;;/d

which deletes 329 lines.

makes a small mistake in a command but fixes it

He then wants to delete any line that starts with zero or more spaces, followed by either ( or ).

He does this with the command

:g/^\s*[()]/d

^ matches the beginning of a line, \s means a space character, * means ‘what precedes, zero or more times’, [] means ‘any of what is in here’, so either ( or ).

But the first time he tries this he accidentally writes g at the end instead of d, so it was attempting what seemed to be a nested g command, but luckily it failed.

Enters visual mode

From below preopentype_dir, line 225, enters visual mode with v, goes to the beginning of the file with gg, goes down two lines with jj so just the words from around which we removed the parentheses are highlighted.

Then all of the letters become uppercase, which he seems to have accomplished with 223gU with the visual selection still active. gU would have sufficed.

He then indents the whole file with =G, moving all of the now capital letters to the left.

Starts Recording another macro into register q.

Starts by pressing qq to begin recording into register q.

Starts with the cursor one line above the above the first paragraph of capitalized words, and runs a search that will take you to the next occurrence of the beginning of a line that starts with a capital letter.

/^[A-Z]

then enters visual block mode with <C-v>, goes to the line one-past the end of the paragraph with }, goes up one line with k. Presses I to insert at the beginning of the block on each line, and types const. Presses <Esc> and it is applied to all lines. Restores previous visual selection with gv and presses $A to append at the right side of the visual block but respecting the uneven ends of lines. Types = 0; and <esc> to apply that to each line. Presses j to go down one line, Presses V to enter ‘visual line’ mode. Presses }k again to select to exactly to end of paragraph. Seems to press g<C-a> to increment the numbers by sequentially. He seems to start pressing 4g but he must realize that this is a mistake and press <Esc> to cancel that command.

side quest

Registers from delete/yanks/puts are shared with macros, so you can edit a macro (say it is stored in q) by pasting it directly to the buffer by going "qp, editing it with insert mode and yanking it back to the register with "qy$ from the beginning of the pasted macro. Make sure to not yank the line-end character. That is why I use $ here.

You can also append to a register by using @[uppercase of register], in this case @Q

He applies it to the next paragraph but 2big messes up the incrementation of the numbers and he fixes this manually.

Then applies it to all of the other paragraphs by repeatedly pressing @q. Note that the search of the start of the macro brings the cursor to the location where it needs to be, so there is no need to move between executions of the macro.

The last execution of the macro accidentally increments the first return 0; to return 1;. He fixes that manually.

Uses a command to delete all blank lines, changes mind

:g/^$\n/d

This finds all lines where the beginning of the line (^) is immediately followed by a line ($) end. The \n seems redundant to me when I test it out.

Note that it will not delete lines that only contain whitespace.

But he presses u to undo this command.

Instead he tries:

%s/^$\n/\r/g

but this doesn’t seem to do anything, merely replacing blank lines with blank lines.

Since the beginning of the video there have been line numbers, meaning that set number is probably somewhere in his vimrc. He now adds relative numbering with :set relativenumber

He then goes through the file manually deleting extra blank lines with dd, pressing zz to center the cursor from time to time.

Replaces all Double Lines

He replaces all double blank lines with

:%s/^\n\n/\r/g

This is more effort to format the file.

I did not follow all of the manual deletions so when I do this I get a different number of substitutions than is show in the video.

He visually highlights the block of text starting with const RIGHTS_FD_DATASYNC = 0; and ending with const RIGHTS_SOCK_SHUTDOWN = 28; . I don’t see exactly how he does it, but I would use vip to visually select the paragraph.

In that paragraph he makes the substitution:

Substitutes Numbers

:'<,'>s/\d\+/0x0000000000000000n

note that '<,'> means to substitute in the range starting at the beginning of the last or current visual selection ('<) and ending at the end of the last or current visual selection ('>). This is added automatically as the range when pressing : from visual mode. \d\+ means ’a digit character one or more times`.

Columnizes

Visually selects the same paragraph again, presses : to enter command mode, (so '<,'> is prepended again, and this time filters the text through an external command with !. Basically the text is provided to STDIN and the output of the command (STDOUT or STDERR) replaces it. In this case the command is column -t which tells the column command to make columns along whitespace (see man column. )

records another macro

He is incrementing the octal numbers by powers of two (first one incremented by 1, next by 2, next by 8, then since they are in hexadecimal format, the next one gets a 1 in the next position. The macro manually uses r1j to change the ending digit, r2j, r4j, r8j, and then jh to move to the next column. Then the macro is complete and he applies it to the rest of the paragraph by running it multiple times. He manually increments the last line with r1.

Does a similar thing

To two more paragraphs, starting from ‘Substitutes Numbers’; does it to two paragraphs, but substitutes 0x0000 instead. Note that he visually selects across two paragraphs but that the columns command combines them into one, and when that happens he re-separates them. He uses the most recently recorded macro to increment by powers of two like before.

He does this in several paragraphs.

Does a global replace

Replaces all return 0.

:%s/return 0/return ERRNO_NOSYS/g

Does a global indent

Goes to the top with gg and presses =G to auto-indent the whole file.

Saves the File

:w

Exits the Session

:x

This command is interesting. It closes Vim, and only writes the file if changes have been made. Kind of redundant when :w was just ran, but good to use just in case.

This Has Been Interesting

By going through this I’ve buffed up on my regex and learned interesting techniques. Feel free to respond with anger or criticism if appropriate.

--

--