Target audience: You've used Vim for a little while, and maybe even used it to write and edit code. You know that Vim has a Normal mode and an Insert mode (even though you maybe spend most time in Insert mode).
You know that Vim is supposed to make you super-efficient at editing text and code, but you feel like you're spending a lot of time fighting Vim to make it do what you want.
Let's get better at Vim together!
Prerequisites: vimtutor. In vimtutor you learn about a lot of basic commands that are useful when editing text, and I am not going to spend too much time going over the basic commands.
Optional prerequisites: LaTeX compiler and Python 3 interpreter. Some of the exercises involve LaTeX and Python code, which is more fun if you can compile or run the code after editing it.
I recommend that you go through vimtutor (type vimtutor
in your terminal)
before the workshop to freshen up the following commands:
-
i A o a A
-
x
-
:q! :wq
-
h j k l
-
w e 0 $
-
dw d$ dd
-
2w d2w 2dd
-
u CTRL-R
-
p
-
r ce c$
-
gg G CTRL-G
-
/ ? n N
-
CTRL-O CTRL-I
-
%
-
:%s/old/new/g
-
:!ls :r
-
:w
-
R vy yw p
-
:set hls :set nohls
Here's a useful cheatsheet from viemu.com:
If you're working with LaTeX or any kind of programming, and you don't use the US keyboard layout (shown in the cheatsheet above), I strongly recommend that you give it a try!
On the US keyboard layout, you get a lot of useful special characters without having to hold down Shift, Ctrl or Alt:
- \
- [ ]
- '
- ;
- /
- =
And with just the Shift key you get the following:
- |
- { }
- "
- :
- ?
- +
- ~
This is a lot better than e.g. Danish keyboard layout where you need the nasty AltGr key to access \ { } | ~.
For entering special characters such as æ ø å ä ö ü etc., you can configure a so-called Compose key in Linux so that you can enter e.g. æ by typing Compose a e.
Goals for this lesson:
- Learn about pasting text into Vim from the outside
- Learn about recording and replaying macros
Tip: Press "+p to paste text into Vim that you copied from outside Vim.
Here's a small LaTeX document. Copy it into Vim by selecting it in your web browser, pressing CTRL-C, opening Vim, and typing "+p.
\documentclass{memoir}
\usepackage[utf8]{inputenc}
\begin{document}
I år består festudvalget af følgende FU'er:
\begin{tabular}{lll}
\hline
Navn & Menneskenavn & Email \\
\hline
\hline
\end{tabular}
\end{document}
Tip: Press :w (FILENAME) to save a new file.
Next, type :w fu.tex to save the document. Optionally compile it with pdflatex.
Go to https://TAAGEKAMMERET.dk/bestfu/ in your web browser, scroll to the bottom and copy the table with ten rows.
Tip: Press } to go to the next blank line. Press { to go to the previous blank line.
In fu.tex
, press } twice to navigate to the second blank line
and paste the copied table with "+p.
You should obtain something like:
\documentclass{memoir}
\usepackage[utf8]{inputenc}
\begin{document}
I år består festudvalget af følgende FU'er:
\begin{tabular}{lll}
\hline
Navn & Menneskenavn & Email \\
\hline
FUNA Znguvnf Xnaartnneq Avryfra SHNA@TAAGEKAMMERET.dk
FURN Yvaarn Frurfgrq Fxnsgr SHRN@TAAGEKAMMERET.dk
FURF Wraf Gebyyr SHRF@TAAGEKAMMERET.dk
FUSB Zbegra Uhyoæx Sbt SHSB@TAAGEKAMMERET.dk
FUVO Fbsvr Orueznaa SHVO@TAAGEKAMMERET.dk
FUAØ Xngevar Uøubyg Xnfcrefra SHABR@TAAGEKAMMERET.dk
FUEN Znevn Nfxubyz Avryfra SHEN@TAAGEKAMMERET.dk
FUEB Serqrevx Naqrefra SHEB@TAAGEKAMMERET.dk
FUHS Nyoreg Serhq Novyqtnneq SHHS@TAAGEKAMMERET.dk
FULB Znwn Qloobr SHLB@TAAGEKAMMERET.dk
\hline
\end{tabular}
\end{document}
That's not a proper LaTeX table. Let's do something about that.
Tip: Press f(TAB) to jump to the next TAB character. (works with any single character; tab is simply an example)
Tip: Press s to delete a character and go to insert mode.
Tip: Press ; to repeat the last f command.
Tip: Press . to repeat the last text manipulation command.
Navigate to the first line you inserted and type f(TAB)s&(SPACE)(ESC);.A(SPACE)\(ESC)
That was quick! But we still have 9 more lines to go. You could press (RETURN) to go to the beginning of the next line and repeat the above key presses, but that would be tedious.
Tip: Press q(LETTER) to record a series of keystrokes. End the recording with q. This is known as recording a macro.
Tip: Press @(LETTER) to execute a series of keystrokes. Press @@ to re-execute the last executed series. This is known as replaying a macro.
Go to the beginning of the next line, press qq, redo the keystrokes you entered for the first line, go to the beginning of the next (i.e. third) line, and press q.
Now press @q. If all went well, this should redo for the third line what you did for the second line, and put your cursor at the beginning of the fourth line. If something doesn't look right, maybe you didn't record the macro completely right; try recording it again.
Now press 7@@ to run the macro seven more times.
In this case, we knew there are 10 lines in total, so it was easy to calculate the correct number of repeats in our head (i.e. 7).
However, sometimes there's a lot of lines, more than we can quickly count. For these cases there's another way to run a macro on a set of lines.
Press u to undo what you did.
Tip: Press V to start selecting lines. This is known as Visual Line mode.
Tip: Press :norm @q while in Visual Line mode to replay macro q on the selected lines.
Note that Vim inserts '<,'>
in-between :
and norm
; this is intended, so don't delete it!
Move the cursor to the first line that needs to be processed, press V, move the cursor to the last line, and press :norm @q. If all went well, this should replay the macro on each selected line -- no line counting needed on your part!
In this case, the lines that we needed to run the macro on were contiguous. Sometimes, we want to run a macro on all lines that match a specific pattern instead.
Tip: Press :g/(PATTERN)/norm @q to replay macro q on all lines that contain (PATTERN)
.
Undo the previous macro replays with u.
This time, without going into Visual Line mode,
press :g/(TAB)/norm @q.
Vim should display the tab character as ^I
- this is intended and is simply the way Vim displays tab characters.
If all went well, this should replay the macro on every line that contains a tab character.
You should now have a file containing something like:
\documentclass{memoir}
\usepackage[utf8]{inputenc}
\begin{document}
I år består festudvalget af følgende FU'er:
\begin{tabular}{lll}
\hline
Navn & Menneskenavn & Email \\
\hline
FUNA & Znguvnf Xnaartnneq Avryfra & SHNA@TAAGEKAMMERET.dk \\
FURN & Yvaarn Frurfgrq Fxnsgr & SHRN@TAAGEKAMMERET.dk \\
FURF & Wraf Gebyyr & SHRF@TAAGEKAMMERET.dk \\
FUSB & Zbegra Uhyoæx Sbt & SHSB@TAAGEKAMMERET.dk \\
FUVO & Fbsvr Orueznaa & SHVO@TAAGEKAMMERET.dk \\
FUAØ & Xngevar Uøubyg Xnfcrefra & SHABR@TAAGEKAMMERET.dk \\
FUEN & Znevn Nfxubyz Avryfra & SHEN@TAAGEKAMMERET.dk \\
FUEB & Serqrevx Naqrefra & SHEB@TAAGEKAMMERET.dk \\
FUHS & Nyoreg Serhq Novyqtnneq & SHHS@TAAGEKAMMERET.dk \\
FULB & Znwn Qloobr & SHLB@TAAGEKAMMERET.dk \\
\hline
\end{tabular}
\end{document}
Exercise: Add \newcommand{\FU}[3]{FU#1 & #2 & #3 \\}
to the preamble of the document,
and use what you have learned so far to convert each row
into an invocation of the \FU
LaTeX command.
Tip: Press I to insert text at the beginning of the line.
Tip: Press C to delete the rest of the line and enter Insert mode.
Goals for this lesson:
- Learn about token-completion and line-completion in Insert mode.
- Learn how to perform simple refactorings efficiently with macros and mappings.
Copy the following Python code and save it as fu.py
:
import re
import urllib.request
with urllib.request.urlopen('https://TAAGEKAMMERET.dk/bestfu/') as request:
html = request.read().decode('utf-8')
for row in re.finditer('<tr>.*?</tr>', html, re.S):
name, human_name = re.findall(r'<td>(.*?)</td>', row.group())
if not name.startswith('FU'):
continue
email, = re.findall('mailto:([^"]*)', row.group())
print('%s & %s & %s \\\\' % (name, human_name, email))
[Aside: Press :filetype to see your current filetype settings.
Vim should respond with filetype detection:ON plugin:ON indent:ON
.
If it does not, you need to add filetype indent plugin on
to the file ~/.vimrc
.
This ensures that Vim automatically indents e.g. Python code correctly.
Close Vim and reopen fu.py after editing your vimrc file.
---End aside]
Tip: Press CTRL-P while in Insert mode to complete the word at the cursor using other words in the current file.
Navigate to the for
-line and press O
to open a new line above, type print("TACTRL-Ps FUer:").
This should yield the line print("TAAGEKAMMERETs FUer:")
.
Tip: While completing in Insert mode, use CTRL-N and CTRL-P to navigate the suggestions. When you are satisfied with the completed word, press (ESCAPE) or continue typing to close the completion popup.
Tip: Press CTRL-XCTRL-L while in Insert mode to complete the line at the cursor using other lines in the current file.
Tip: Press CTRL-W while in Insert mode to erase the last word you inserted.
Go to the second import
-line and press o
to open a new line above and type
imCTRL-XCTRL-LCTRL-Wparse.
This should yield the line import urllib.parse
. Delete it again with dd.
Remember to use these two completion commands (token completion and line completion) -- they will save you a lot of typing in the long run!
But now for something more interesting: code refactoring.
The with
-line is a bit long, don't you think?
We should extract the URL as a variable, i.e. change it to this:
url = 'https://TAAGEKAMMERET.dk/bestfu/'
with urllib.request.urlopen(url) as request:
html = request.read().decode('utf-8')
Tip: Press ci( to delete the insides of a set of parentheses and switch to Insert mode.
Tip: Press O to Open a line above the current line.
Tip: Press CTRL-A while in Insert mode to insert the same text as you inserted the last time you were in Insert mode.
Tip: Press p to put the text you last deleted.
Here's how I would do it:
- Navigate to the
(
character in thewith
-line -- remember the f( command? - Press ci(url(ESCAPE) to replace the contents of the parentheses with the variable name
url
- Press OCTRL-A(SPACE)=(SPACE)(ESCAPE) to insert
url =
. - Press p to insert the URL you deleted using in step 2.
Exercise: Record step 3 and 4 in a macro to save you a couple keystrokes in the rest of this lesson.
Tip: Press ct, to delete everything till the next comma.
Let's use a macro to extract some more variables in this Python program.
Navigate to the first '
in the for
-line with f'.
Then use ct, to delete the first argument (i.e. '<tr>.*?</tr>'
)
and type pattern(ESCAPE).
Then repeat steps 3 and 4 above (or replay the macro you just recorded).
Tip: Press c% to delete everything from the cursor until the parenthesis matching the first parenthesis to the right.
The motion % moves the cursor to the parenthesis matching the one under the cursor. However, if there is no parenthesis under the cursor, Vim first searches to the right in the line for the first open or close parenthesis, and then moves to the matching one.
This is very useful when you want to affect a function call such as
re.findall(r'<td>(.*?)</td>', row.group())
:
First move the cursor to the "r" in "re.findall", and then press %
to jump to the end of the function call.
Exercise: Extract row.group()
to a variable using c% and your macro.
Tip: Press :nnoremap (COMMAND) (COMMANDS) to remap (COMMAND) to (COMMANDS).
If you like this trick, create a mapping for it! Personally, I use \r as a shortcut for steps 3 and 4. Press :nnoremap \r O<C-A> = <Esc>p, and then try replacing some argument with a variable name and pressing \r.
Tip: Press :nnoremap <buffer> (COMMAND) (COMMANDS) to create a mapping that only applies in the current file.
Put au FileType python (COMMAND) in your vimrc to execute (COMMAND) whenever you open a Python file.
If you want to use this trick in all Python files, add the following to your ~/.vimrc
:
au FileType python nnoremap <buffer> \r O<C-A> = <Esc>p
This works for languages other than Python too.
If you're unsure what to put after au FileType
for a given type of file,
open a file of the given type and type :set ft,
and Vim will respond with the filetype of the currently-open file.
Exercise: Create a mapping for \r in your vimrc that works for LaTeX instead of Python.
I.e. when you replace M_{\odot}
with \sunmass
in a LaTeX file and press \r,
Vim should insert \newcommand{\sunmass}{M_{\odot}}
on a new line above.
Tip: :set spell spelllang=en_us to enable American English spell-checking for the current file.
Tip: :set spellfile=(SPELLFILE) to use the extra words in (SPELLFILE) when spell-checking.
Tip: ]s and [s navigate to the next and previous misspelled word in the open file.
Tip: zg saves the word under the cursor as "good" in (SPELLFILE). zw saves it as "wrong" in (SPELLFILE). zuw and zug are used to undo zg and zw.
Tip: au BufRead (PATH)/*.tex (COMMAND) to run (COMMAND) whenever you open a .tex-file inside (PATH).
Tip: Add let g:tex_comment_nospell=1 in your vimrc to disable spell-checking inside LaTeX comments.
In my vimrc, I have the following lines to enable spell-checking for my thesis:
let g:tex_comment_nospell=1
au BufRead /home/rav/work/thesis/*.tex
\ setlocal spell spelllang=en_us spellfile=
\/home/rav/work/thesis/spell.txt.utf8.add
Goals for this lesson:
- Learn about HTML scraping with Vim.
- Learn about split windows to edit multiple files.
- Learn about processing multiple files efficiently using macros and splits.
- Learn about diffing files inside Vim.
If you go to https://TAAGEKAMMERET.dk/galleri/2018, you will see a list of albums for the year 2018/19, named "BEST", "KBEST-perioden", "FU", "GF", and so on. If you replace "2018" in the URL with a previous year, you'll get a list of that year's albums.
For the next exercise we'll retrieve the list of album names for multiple years.
Tip: If you tell Vim to open a webpage instead of a filename, Vim will download and display the HTML source code of that page.
Open the page's source code in Vim by typing :e https://TAAGEKAMMERET.dk/galleri/2018.
It's a long page, full of hard-to-decipher HTML code. However, we can still use a couple of Vim tricks to boil it down to just the list of album names we're interested in.
Tip: Press /(PATTERN) to search in the open file. Press n and N to move to the next or previous search result.
Try searching the file for the string "KBEST-perioden" by typing /KBEST-perioden. You should end up at the following part of the file:
<div class="col-xs-6 col-sm-4 col-md-3">
<a class="thumbnail" href="https://raw.githubusercontent.com/Mortal/vim-workshop/master//galleri/2018/kbest-perioden">
<div class="thumbcap">
<img src="https://raw.githubusercontent.com/Mortal/vim-workshop/master//media/__sized__/2018/kbest-perioden/img_6585-crop-c0-5__0-5-253x253-70.JPG" alt="KBEST-perioden">
<div class="caption">
<h5>KBEST-perioden
<small>53 billeder</small>
</h5>
</div>
</div>
</a>
</div> <!-- col-xs-6 thumb -->
Notice that the album name is shown next to the code <h5>
.
Now we might guess that all the album names will be lines that contain <h5>
.
Tip: Press :v/(PATTERN)/d to delete all lines that do not contain (PATTERN)
.
Type :v/<h5>/d to keep only the lines containing <h5>
.
If you scroll through the resulting text, you should find that all the lines that remain
correspond exactly to the album names!
Exercise: Record a macro where you press 0 to move to the beginning of the line and df> to delete everything until and including the >
in <h5>
. Replay the macro on all lines in the buffer.
Tip: Press :sav (FILENAME) to save the current file under a new name.
There is a subtle difference between using :w (FILENAME) and :sav (FILENAME) to save a file. If you're currently editing a saved file, say foo.txt, and you press :w bar.txt, then Vim will write the contents to bar.txt, but you will still be editing foo.txt. You need to press :sav bar.txt to write the contents to bar.txt and switch to bar.txt.
Press :sav 2018.tex to save the contents to 2018.tex. It might not look like LaTeX code right now, but don't worry, we will fix that later.
Next, switch to a new file names links.txt by pressing :e links.txt.
Insert the URL from before in the new file: https://TAAGEKAMMERET.dk/galleri/2018
Tip: Press CTRL-X to decrease the number under the cursor by one. Press CTRL-A to increase the number under the cursor by one. If there is no number under the cursor, Vim will search to the right in the current line for the first number it can find.
Press 0 to move to the beginning of the line and CTRL-X to decrease the year in the URL by one.
Next, press yyp to make a copy of the line and press CTRL-X to decrease the year once more.
Repeat the process until you have, let's say, 6 URLs. Your file should contain the contents:
https://TAAGEKAMMERET.dk/galleri/2017
https://TAAGEKAMMERET.dk/galleri/2016
https://TAAGEKAMMERET.dk/galleri/2015
https://TAAGEKAMMERET.dk/galleri/2014
https://TAAGEKAMMERET.dk/galleri/2013
https://TAAGEKAMMERET.dk/galleri/2012
Tip: Press CTRL-Ws to split the view of the current file into two.
Tip: Press CTRL-WCTRL-W to move the cursor to the next split.
Tip: Press CTRL-Wc to close the split that the cursor is currently in.
Tip: Press CTRL-Wo to make the split that the cursor is currently in the only split, that is, close all other splits.
Exercise: Try splitting the current file, moving between the splits, make some edits in one split and see them reflected in the other, undo your edits, and close the splits again.
[Aside: There are ways to manage many open splits, to perform vertical splits instead of horizontal, and to resize splits. In my experience it is rarely useful to have more than two files open on the same screen, but if you want to learn more, type :h windows. ---End aside]
Tip: Press gf to go to the file under the cursor.
Tip: After moving the cursor far, e.g. to a different file, press CTRL-O to go back to where you came from. Press CTRL-I to go forward again.
Split the current file with CTRL-Ws, move to the top line in the file with gg and press gf to open the URL under the cursor.
Repeat the steps you took to create 2018.tex, but don't save the file as 2017.tex just yet.
Tip: Press yiw to yank (i.e. copy) the word under the cursor.
Move the cursor to the split containing links.txt by pressing CTRL-W CTRL-W, and press $yiw to move the cursor to the end of the line and yank 2017
.
If you press p anywhere now, you can verify that 2017
has been copied successfully.
Move back to the split containing the album names of 2017.
Tip: Press CTRL-R" while editing the command-line at the bottom of the screen to put the text you last yanked or deleted. This inserts the same text as would be inserted with p in Normal mode.
Press :sav (CTRL-R)".tex to save the file as 2017.tex
. When you press CTRL-R, Vim should display a "
character without moving the cursor to the right. Then when you press ", the copied text 2017
should appear.
Close the split with CTRL-Wc and delete the 2017-link with dd.
Exercise (hard): Repeat the above steps to create 2016.tex
, only this time recording the steps into a macro! Remember to use a different macro-letter for this macro than the macro you used to remove the <h5>
in the above buffer. You won't be able to record a small macro while recording this larger macro.
Hopefully, you should end up with files named 2018.tex, 2017.tex, 2016.tex, 2015.tex, 2014.tex, 2013.tex, and 2012.tex.
But they don't contain proper LaTeX code -- let's do something about that!
Tip: Press :n and :prev to move to the next and previous file specified on the command line.
Tip: Press :args to display the list of files specified on the command line.
Tip: Press :wn to write the current file and go to the next file. :wn is short for :w followed by :n.
Close Vim and reopen it with the command-line vim 20*.tex
. Vim should open and display 2012.tex, and if you press :args, Vim should respond with the list of 7 files we have created so far.
Tip: Press CTRL-V to enter Visual Block mode. After making a Visual Block selection, press I, type some text and press (ESCAPE) to insert text in front of each line in the selected block. Type $A to insert text in the end of each selected line.
Exercise: Use Visual Block mode to type \item
in front of each line in 2018.tex. Then type :wn to write and go to the next file.
Exercise: Record a macro to insert \item
in front of each line, write the file and go to the next one. Replay the macro to process each of the opened files.
Here's a LaTeX template for you. Copy it into a new file named albums.tex
.
\documentclass{memoir}
\usepackage[utf8]{inputenc}
\begin{document}
\chapter{TK albums}
\end{document}
Tip: Press :r !ls (PATTERN) to insert a list of filenames matching (PATTERN)
.
Move the cursor to the blank line and press :r !ls 20*.tex. This should insert a list of filenames, so your file should look like this:
\documentclass{memoir}
\usepackage[utf8]{inputenc}
\begin{document}
\chapter{TK albums}
2012.tex
2013.tex
2014.tex
2015.tex
2016.tex
2017.tex
2018.tex
\end{document}
Exercise: Without typing 2012
, change the first line to \section{2012}\begin{itemize}\input{2012.tex}\end{itemize}
. Hint: Use yiw
to copy 2012
and p
to put a copy of it.
Exercise: Repeat the steps for 2013.tex
, this time recording a macro.
Replay your macro to process the rest of the lines.
Tip: Press :diffthis to enable diff-mode for the split that the cursor is currently in. When you have enabled diff-mode for two splits, Vim will highlight the differences between the split contents. Press :diffoff to disable diff-mode.
Exercise: Use splits, gf and diff-mode to find the similarities and differences between the album names in 2016.tex and 2017.tex.
Tip: Press :windo (COMMAND) to run (COMMAND) in all visible splits. For example, press :windo diffthis to enable diff-mode in all visible splits.
Tip: On the command-line, the vimdiff (FILE1) (FILE2) command opens Vim in diff-mode with the two files.
Tip: In bash, type CTRL-X CTRL-E to open the command-line in an editor and execute the result as a shell command.
bash will use the editor specified by the environment variable $VISUAL
, so you should add the following to your ~/.bashrc
:
export VISUAL=vim
Restart your shell with the command exec bash after editing your bashrc.
In bash, type echo '2019 is the year of Linux on the desktop!' > the-truth.txt
without pressing RETURN.
Press CTRL-X CTRL-E, and, if all goes well, Vim should open, displaying the command line you have just typed.
Press :q to quit Vim, and then bash should execute the command, creating the file the-truth.txt
.
Press up-arrow to redisplay the command line and press CTRL-X CTRL-E again.
Press CTRL-A in Vim to increment 2019
to 2020
, and quit with :q.
Then bash executes the command line as you updated it in Vim, overwriting the-truth.txt
with the new truth.
Tip: In Vim, the text you highlight in Visual mode, Visual Line mode or Visual Block mode, is available to other programs in the so-called "selection buffer". The selection buffer is accessed via the middle-mouse button, or in some programs (such as your terminal emulator) with the keystroke Shift-Insert
Open Vim and insert the text echo '2019 is the year of Linux on the desktop!' > the-truth.txt
.
Press V (capital V, that is, Shift-v) to select the line in Visual Line mode.
Then, open a new terminal and press Shift-Insert. If all goes well, your terminal should insert the code you highlighted in Vim.
[Aside: There is a so-called "vi readline keymap" in bash that supposedly makes editing your bash command-line more Vim-like. Don't use it. Use the standard keymap for normal uses, and use CTRL-X CTRL-E to launch a real editor when you need it. ---End aside]
[Aside:
There is a 'clipboard'
setting in Vim that can be used to
modify how Vim interacts with the Linux clipboard buffer and selection buffer.
Don't change the setting --- just use the default Vim behavior.
---End aside]
Exercise: Type history
in bash and look for long commands that you use often.
Store them in a text file, and use what you have learned in this lesson
the next time, instead of retyping the long commands from scratch.
What's next? I didn't cover everything in this workshop. I should have probably talked more about:
- Put-at-current-indent with
]p
-
vp
to swap things - Setting up vimrc to use arrow keys for scrolling
- Multi-file editing with
:set autowrite
:argdo
- Search and replace with
:s
- Error-lists with
vim -q <(...)
:cn
- Editing macros with
"qp
"qD
- Using
:norm
with more than just macro-replays