Transcription of an Impressive Vim Programming Session
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:
- formats the buffer, left justifying some lines, inserting tabs in front of function parameters,
- 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:
- Delete the line that had the leading parenthesis with
dd
before formatting - Comment out the line
(import "memory" (memory))
by prepending\\
. This seems to make it approximate valid JS syntax enough for formatting. - With my cursor on the top line in normal mode, enter
=G
from the top line to indent the whole file. - 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.
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.