Welcome to the world of vim plugins. In this tutorial you will create a plugin for generating markdown–specifically, DigitalOcean markdown–by exposing a set of insert-mode mappings for efficient markdown creation. Additionally, you will create a function that uses a vim API to search and reposition the cursor.
- You should be comfortable using a Linux terminal. Have a look at An Introduction to the Linux Terminal.
- Experience using vim and some knowledge about creating mappings. Here is a good resource for that: How To Use Vim for Advanced Editing of Plain Text or Code on a VPS.
In this section you will install vim and set up all the requisite files and directories. Let’s begin by installing vim.
Install vim using the following command:
sudo apt update
sudo apt install vim-gtk3
You can verity the vim version using the following command:
vim --version
You need a .vimrc
file and a .vim/plugin
directory structure. .vimrc
is a configuration file with settings like mappings, syntax highlighting, and line numbers. Let’s create it first.
Change to your home directory using the following command:
cd
Use the ls -al
command to look for .vimrc
and .vim
. If you don’t see one or the other, you will create them. Let’s assume you don’t see them, so you’ll create them. Use the following command to create .vimrc
:
touch .vimrc
Usemkdir
with a -p
flag to create both the .vim
and .vim/plugin
folders:
mkdir -p ~/.vim/plugin
Your plugin goes in ~/.vim/plugin
and can be composed of a single file or a directory with multiple files. Your plugin will have multiple files, so create a directory for it using the following command:
mkdir ~/.vim/plugin/do_markdown
Change to your plugin directory and create your plugin file do_markdown.vim
using the following commands:
cd ~/.vim/plugin/do_markdown
touch do_markdown.vim
To activate your plugin every time vim starts up, you need to modify .vimrc
and source it. Edit .vimrc
using the following command:
vim ~/.vimrc
Add the following line of code to the end of the file:
source ~/.vim/plugin/do_markdown/do_markdown.vim
Save and exit vim using the :x
command Good job! You just created all the plugin boilerplate.
Markdown is a language used to format text documents with basic styles like headings, bold and italic, and block quotes. For instance, you can make text bold by surrounding–or bracketing–it with double asterisks as in **Alert**.
DigitalOcean (DO) has its own cool looking markdown that works well with formatting code blocks and callouts. This plugin will generate DO markdown.
Note: This plugin only generates a subset of the DO markdown. Feel free to add the rest!
This plugin creates two types of markdown: brackets and blocks. Brackets surround a string of text with symbols that apply formatting to it, and blocks are multiline sections of text enclosed in symbols. Let’s tackle brackets first.
Your plugin exposes a collection of mappings. A mapping is a shortcut that automates some repetitive task. Working efficiently is all about reducing keystrokes, and that is exactly what mappings do for you. As you know, vim has multiple modes of operation: insert, normal, command, and visual. Well, you can create mappings for each mode: imap, nmap, cmap, and vmap. For simplicity, this article refers to all of these generically as mappings.
It’s tedious to repeatedly type closing brackets, so your plugin will generate the closing bracket for you and move the cursor between the brackets. Change to your plugin’s directory and edit your plugin file using the following commands:
cd ~/.vim/plugin/do_markdown
vim do_markdown.vim
Enter the following code to the bottom of the file:
inoremap ** ****<left><left>
The following table shows what happens when you test the imap by entering insert-mode and typing **
:
Note: The _ character represents the cursor location.
Type: | Result |
---|---|
** | **_** |
As the table illustrates, when you type **
in insert-mode, the mapping inserts four *
characters and places the cursor between both pairs.
There is a problem with this imap: Once you finish typing Note:
the cursor remains between the brackets as follows:
**Note:_**
Now you have to use <esc>A
to move past the closing bracket. Wouldn’t it be easier if instead you could stay in insert-mode , type ;;
, and achieve the same thing--moving the cursor to the end of the line in insert-mode? Fortunately, you can create an imap to do this. The problem with using <esc>A
is you have to move your hand off the home row, reach up and press <esc>
, reposition your hand on the home row, then do a <shift>a
. With an imap all you do is move your right pinky finger over slightly and press ;;
. Add the following imap to your plugin:
inoremap ;; <esc>A
The following table illustrates the result of using imap ;;
in conjunction with the bracket imap **
. Begin by entering insert-mode.
Step | Type: | Result |
---|---|---|
1. | ** | **_** |
2. | Note: | **Note:_** |
3. | ;; | **Note:**_ |
Observe the position of the cursor _
during each step. When you test these macros, you’ll see what I mean firsthand.
There is another problem to solve: After typing ;;
you still have to tap <space>
at the end. If you type **
in insert-mode, and then type "Don't forget to save", this is what will happen:
**Note:**Don't forget to save
It's somewhat hard to notice, but there is no whitespace before "Don't". We need a space after it.
**Note:** Don't forget to save
To insert a space after **Note:**, update the imap as follows:
inoremap ** ****<space><esc>2hi
Finally, here are all of the bracket imaps in this plugin. Copy them into your plugin file.
map | Type | Result |
---|---|---|
inoremap ** ****2hi | ** | **_** |
inoremap ` `` | ` | `_` |
inoremap _ __ | _ | __ |
inoremap ^ <^><^>3hi | ^ | <^>_<^> |
Save and exit using the following vim command:
:wq
" UTILITIES
inoremap ;; <esc>A
nnoremap <F4> <esc>:w<cr>:so %<cr>
" BRACKETS
inoremap ** ****<space><esc>2hi
inoremap ` ``<space><left><left>
inoremap _ __<space><left><left>
inoremap ^ < ^><^><space><esc>3hi
Now you’ll create your block imaps, which generate multiline blocks of text using simple parameterized templates. Consider the following DO markdown code block:
```sh
vim test.py
```
You will create a template that substitutes XXX in sections that you will edit. Create a template named code.txt
using the following command.
vim code.txt
Enter the following text:
...XXX
XXX
...
Save and close the file using the following vim command:
:wq
Reopen your plugin file using the following command:
vim do_markup.vim
Now you will make an imap that reads the template file into the active buffer. Add this to your plugin:
inoremap code<cr> <esc>:read code.txt<cr>kdd
Note: The normal-mode
kdd
command deletes the blank line that gets added when you read file contents in.
Test it by entering insert-mode and typing code<cr>
. It should dump the contents of code.txt
at the cursor.
" UTILITIES
inoremap ;; <esc>A
nnoremap <F4> <esc>:w<cr>:so %<cr>
" BRACKETS
inoremap ** ****<space><esc>2hi
inoremap ` ``<space><left><left>
inoremap _ __<space><left><left>
inoremap ^ < ^><^><space><esc>3hi
" BLOCKS
inoremap code<cr> <esc>:read code.txt<cr>kdd
Here is the most tedious part. You need to create all of the templates for all of the block imaps. Here are the download links for each file:
code.txt out.txt lab.txt crp.txt ecod.txt elab.txt eout.txt ecpr.txt nowa.txt note.txt warn.txt info.txt drft.txt col.txt dets.txt
You will create the rest of the block imaps later in the tutorial.
You’re going to make a lot of these imaps, so it wouldn’t hurt to use the following function to avoid code duplication. Add this to your plugin:
function LoadTemplate(template_filename)
silent execute 'read ' . a:template_filename
normal kdd
endfunction
Update your imap code<cr>
to invoke LoadTemplate
as follows:
inoremap code<cr> <esc>:call LoadTemplate('code.txt')<cr>
This is the basic structure you will use to create all of the block imaps in this plugin.
" UTILITIES
inoremap ;; <esc>A
nnoremap <F4> <esc>:w<cr>:so %<cr>
function LoadTemplate(template_filename)
silent execute 'read ' . a:template_filename
normal kdd
endfunction
" BRACKETS
inoremap ** ****<space><esc>2hi
inoremap ` ``<space><left><left>
inoremap _ __<space><left><left>
inoremap ^ < ^><^><space><esc>3hi
" BLOCKS
inoremap code<cr> <esc>:call LoadTemplate('code.txt')<cr>
After inserting the template file contents into the buffer, the next step is to move the cursor to the next XXX and replace it with the cursor in insert-mode. To accomplish this, you are going to create a function named ToEndOrNext
that does the job of your imap ;;
plus the aforementioned functionality. Below is the pseudo code for it:
If there is no template_param in the file
Move cursor to end of line in insert-mode
Else
Move cursor to next template_param
Replace template_param with cursor in insert-mode
Add the following code to your plugin:
function ToEndOrNext(template_param)
if search(a:template_param, 'n')
execute "normal! /" . a:template_param . "\r"
normal diw
startinsert!
else
startinsert
endif
endfunction
This uses the search
VimL API to search for template_param
. Using the n
option allows you to search for a string without moving the cursor to the match–a look-ahead search, if you will.
Let’s test it. Save and source the plugin using the following vim commands:
:w
:so %
Create a new buffer using the following vim command:
:new
Enter insert-mode and type code<cr>
. This will read in the code.txt
template. Now, use the following vim command to test ToEndOrNext
:
:call ToEndOrNext('XXX')
The cursor should have replaced the first XXX. Use :call ToEndOrNext('XXX')
again. The next XXX should be replaced by the cursor. At this point both XXX strings should be gone. Now test ToEndOrNext
by moving the cursor to the middle of a line, enter insert-mode, then use <esc>:call ToEndOrNext('XXX')<cr>
. The cursor should move to the end of the line in insert-mode.
Next, update your imap ;;
to invoke ToEndOrNext
as follows:
inoremap ;; <esc>:call ToEndOrNext('XXX')<cr>
Almost there. After you load a template using imap code<cr>
, you want it to move to the next XXX and replace it with the cursor in insert-mode. Hence, you call ToEndOrNext
from within LoadTemplate
.
" UTILITIES
inoremap ;; <esc>:call ToEndOrNext('XXX')<cr>
nnoremap <F4> <esc>:w<cr>:so %<cr>
function LoadTemplate(template_filename)
silent execute 'read ' . a:template_filename
normal kdd
call ToEndOrNext('XXX')
endfunction
function ToEndOrNext(template_param)
if search(a:template_param, 'n')
execute "normal! /" . a:template_param . "\r"
normal diw
startinsert!
else
startinsert
endif
endfunction
" BRACKETS
inoremap ** ****<space><esc>2hi
inoremap ` ``<space><left><left>
inoremap _ __<space><left><left>
inoremap ^ < ^><^><space><esc>3hi
" BLOCKS
inoremap code<cr> <esc>:call LoadTemplate('code.txt')<cr>
The final step is to create the rest of your block imaps. Remember when you downloaded all of the template files earlier? Now it is only a matter of calling your LoadTemplate
function, passing the name of the template file. Here is your complete plugin with the block imaps and everything else:
inoremap code<cr> <esc>:call LoadTemplate('code.txt')<cr>
```_
XXX
```
inoremap out<cr> <esc>:call LoadTemplate('out.txt')<cr>
```
[secondary_label Output]
_
```
inoremap lab<cr> <esc>:call LoadTemplate('lab.txt')<cr>
```
[label _]
XXX
```
inoremap cpr<cr> <esc>:call LoadTemplate('crp.txt')<cr>
```custom_prefix(_)
XXX
```
inoremap ecod<cr> <esc>:call LoadTemplate('ecod.txt')<cr>
```_
[environment XXX]
XXX
```
inoremap elab<cr> <esc>:call LoadTemplate('elab.txt')<cr>
```
[environment _]
[label XXX]
XXX
```
inoremap eout<cr> <esc>:call LoadTemplate('eout.txt')<cr>
```
[environment _]
[secondary_label Output] XXX
```
inoremap ecpr<cr> <esc>:call LoadTemplate('ecpr.txt')<cr>
```custom_prefix(XXX)
[environment XXX]
XXX
```
inoremap nowa<cr> <esc>:call LoadTemplate('nowa.txt')<cr>
<$>[_]
**XXX:** XXX
<$>
inoremap note<cr> <esc>:call LoadTemplate('note.txt')<cr>
<$>[note]
**Note:** _
<$>
inoremap warn<cr> <esc>:call LoadTemplate('warn.txt')<cr>
<$>[warning]
**Warning:** _
<$>
inoremap info<cr> <esc>:call LoadTemplate('info.txt')<cr>
<$>[info]
**Info:** _
<$>
inoremap drft<cr> <esc>:call LoadTemplate('drft.txt')<cr>
<$>[draft]
**Draft:** _
<$>
Formatting
inoremap col<cr> <esc>:call LoadTemplate('col.txt')<cr>
[column
_
]
[column
XXX
]
inoremap dets<cr> <esc>:call LoadTemplate('dets.txt')<cr>
[details _
XXX
]
" UTILITIES
inoremap ;; <esc>:call ToEndOrNext('XXX')<cr>
nnoremap <F4> <esc>:w<cr>:so %<cr>
function LoadTemplate(template_filename)
silent execute 'read ' . a:template_filename
normal kdd
call ToEndOrNext('XXX')
endfunction
function ToEndOrNext(template_param)
if search(a:template_param, 'n')
execute "normal! /" . a:template_param . "\r"
normal diw
startinsert!
else
startinsert
endif
endfunction
" BRACKETS
inoremap ** ****<space><esc>2hi
inoremap ` ``<space><left><left>
inoremap _ __<space><left><left>
inoremap ^ < ^><^><space><esc>3hi
" BLOCKS
inoremap code<cr> <esc>:call LoadTemplate('code.txt')<cr>
inoremap out<cr> <esc>:call LoadTemplate('out.txt')<cr>
inoremap lab<cr> <esc>:call LoadTemplate('lab.txt')<cr>
inoremap cpr<cr> <esc>:call LoadTemplate('crp.txt')<cr>
inoremap ecod<cr> <esc>:call LoadTemplate('ecod.txt')<cr>
inoremap elab<cr> <esc>:call LoadTemplate('elab.txt')<cr>
inoremap eout<cr> <esc>:call LoadTemplate('eout.txt')<cr>
inoremap ecpr<cr> <esc>:call LoadTemplate('ecpr.txt')<cr>
inoremap nowa<cr> <esc>:call LoadTemplate('nowa.txt')<cr>
inoremap note<cr> <esc>:call LoadTemplate('note.txt')<cr>
inoremap warn<cr> <esc>:call LoadTemplate('warn.txt')<cr>
inoremap info<cr> <esc>:call LoadTemplate('info.txt')<cr>
inoremap drft<cr> <esc>:call LoadTemplate('drft.txt')<cr>
inoremap col<cr> <esc>:call LoadTemplate('col.txt')<cr>
inoremap dets<cr> <esc>:call LoadTemplate('dets.txt')<cr>
I use this plugin from VS Code. Using an extension called Markdown Preview, I preview a markdown file. Files dock to the top by default. Next, I open the markdown file in vim from a terminal, which docks to the bottom by default. Now, any time I change and save the markdown file in vim, the preview updates to reflect the change.
I typically use the following normal- and insert-mode mappings to quickly save the file.
nnoremap ;s :w<cr>
inoremap ;s <esc>:w:<cr>
You’ve learned a little bit about mappings and VimL. I hope you use this knowledge to experiment with your own vim plugins. Have fun, friend.