author |
Steve Losh <steve@stevelosh.com> |
date |
Thu, 25 Aug 2022 23:05:04 -0400 |
parents |
f5556130bda1 |
children |
(none) |
(
:title "Writing Vim Plugins"
:snip "It's pretty much black magic."
:date "2011-09-06T09:13:00Z"
:draft nil
)
A while ago I wrote a [post][vimpost] about switching back to [Vim][]. Since then
I've written two plugins for Vim, one of which has been officially "released".
A couple of people have asked me if I'd write a guide to creating Vim plugins.
I don't feel confident enough to write an official "guide", but I do have some advice
for Vim plugin authors that might be useful.
[vimpost]: http://stevelosh.com/blog/2010/09/coming-home-to-vim/
[Vim]: http://www.vim.org/
<div id="toc"></div>
Other People Who Know More Than I Do
------------------------------------
Writing two decently-sized Vim plugins has given me some experience, but there are
a lot of people that know far more than I do. There are two in particular that come
to mind. I'd love for them to write some guides (or even books) about modern-day Vim
scripting.
### Tim Pope
The first is [Tim Pope][]. He's written a ton of Vim plugins like [Pathogen][],
[Surround][], [Repeat][], [Speeddating][] and [Fugitive][]. Each of those is clear,
focused and polished.
It would be awesome to read a guide on the ins and outs of Vim scripting by him.
[Tim Pope]: http://tpo.pe/
[Pathogen]: https://github.com/tpope/vim-pathogen
[Surround]: https://github.com/tpope/vim-surround
[Repeat]: https://github.com/tpope/vim-repeat
[Speeddating]: https://github.com/tpope/vim-speeddating
[Fugitive]: https://github.com/tpope/vim-fugitive
### Scrooloose
The other person that comes to mind is [Scrooloose][], author of [NERDTree][],
[NERDCommenter][] and [Syntastic][].
His plugins are large and full-featured but work incredibly well, considering how
tricky and painful Vimscript is to work with. I'd love to read a guide on writing
large-scale Vim plugins by him.
[Scrooloose]: http://got-ravings.blogspot.com/
[NERDTree]: https://github.com/scrooloose/nerdtree
[NERDCommenter]: https://github.com/scrooloose/nerdcommenter
[Syntastic]: https://github.com/scrooloose/syntastic
Be Pathogen-Compatible
----------------------
It's 2011. When writing your plugin, *please* make its source compatible with
[Pathogen][]. It's very easy to do this — just set up your project's files like
this:
```text
yourplugin/
doc/
yourplugin.txt
plugin/
yourplugin.vim
...
README
LICENSE
```
This will let users use Pathogen (or [Vundle][]) to install and use your plugin.
The days of "unzip and drag the files into the right directories" and the horror of
Vimballs are over. Pathogen and Vundle are the right way to manage plugins, so let
your users use them.
[Vundle]: https://github.com/gmarik/vundle
Please, For the Love of God, Use normal!
----------------------------------------
My first piece of actual scripting advice is something simple but important. If
you're writing a Vim plugin and need to perform some actions, you might be tempted to
use `normal`. Don't. Instead, you need to use `normal!`.
`normal!` is like `normal`, but ignores mappings the user has set up. If you use
plain old `normal dd` and I've remapped `dd` to do something else, the call will use
my mapping and probably not do what your plugin expects. Using `normal!` ensures
that the call will do what you expect no matter what the user has mapped.
This is a single instance of a more general theme. Vim is very customizable and users
will do lots of crazy things in their `.vimrc` files. If a key can be mapped or
a setting changed, you *have* to assume that some user of your plugin will have
mapped or changed it.
Mapping Keys the Right Way
--------------------------
Most plugins add key mappings to make them easier to use. Unfortunately this can be
tricky to get right. You can never tell what keys your users have already mapped
themselves, and shadowing someone's favorite key mapping will break their muscle
memory and annoy them to no end.
### When to Map Keys
The first question to ask is whether your plugin needs to map keys itself at all.
My [Gundo][] plugin has only one feature that needs to be mapped to a key in order to
make it useful: the "toggle Gundo" action.
Gundo doesn't map this key itself, because no matter what "default" mapping I pick
someone will have already mapped it. Instead I added a section right in the README
file that shows how a user can map the key themselves:
```vim
nnoremap <F5> :GundoToggle<CR>
```
By making users add this line to their `.vimrc` themselves it shows them which key is
used to toggle Gundo (which they would have to know anyway) and also makes it obvious
how to change it to suit their taste.
[Gundo]: https://sjl.bitbucket.io/gundo.vim/
### imap and nmap are Pure Evil
Sometimes forcing the user to map their own keys won't work. Perhaps your plugin has
many mappings that would be tedious for a user to set up manually (like my
[Threesome][] plugin), or its mappings are mnemonic and wouldn't really make sense if
mapped to other keys.
I'll talk more about how to deal with this in a moment, but the most important thing
to remember when mapping your own keys is that you must always, *always*,
***always*** use the `noremap` forms of the various `map` commands.
If you map a key with `nmap` and the user has remapped a key that your mapping uses,
your mapped key will almost certainly not do what you want. Using `nnoremap` will
ignore user mappings and do what you expect.
This is the same principle as `normal` and `normal!`: *never* trust your users'
configurations.
[Threesome]: https://sjl.bitbucket.io/threesome.vim
### Let Me Configure Mappings
If you feel that your plugin must map some keys, please make those mappings
configurable in some way.
There are a number of ways to do this. The easiest way is to provide a configuration
option that disables all mappings. The user can them remap the keys as they see fit.
For example:
```vim
if !exists('g:yourplugin_map_keys')
let g:yourplugin_map_keys = 1
endif
if g:yourplugin_map_keys
nnoremap <leader>d :call <sid>YourPluginDelete()<CR>
endif
```
Normal users will get the mappings automatically set up for them, and power users can
remap the keys to whatever they wish to avoid shadowing their own mappings.
If your plugin's mappings all start with a common prefix (like `<leader>` or
`<localleader>`) you have another option: allow users to configure this prefix. This
is the approach I've used in [Threesome][]. It works like this:
```vim
if !exists('g:yourplugin_map_prefix')
let g:yourplugin_map_prefix = '<leader>'
endif
execute "nnoremap" g:yourplugin_map_prefix."d" ":call <sid>YourPluginDelete()<CR>"
```
The `execute` command lets you build the mapping string dynamically so your users can
change the mapping prefix.
There is a third option for solving this problem: the `hasmapto()` Vim function.
Some plugins will use this to map a command to a key *unless* the user has already
mapped that command to something else. I don't personally like this option because
it feels less clear to me, but I know other people feel differently so I wanted to
mention it.
Localize Mappings and Settings
------------------------------
The next step in being a good Vim plugin author is to try to minimize the effects of
your key mappings and setting changes. Some plugins will need to have global
effects but others will not.
For example: if you're writing a plugin for working with Python files it should only
take effect for Python buffers, not all buffers.
### Localizing Mappings
Key binding are easy to localize to single buffers. All of the `noremap` commands
can take an extra `<buffer>` argument that will localize the mapping to the current
buffer.
```vim
" Remaps <leader>z globally
nnoremap <leader>z :YourPluginFoo<cr>
" Remaps <leader>z only in the current buffer
nnoremap <buffer> <leader>z :YourPluginFoo<cr>
```
However, the problem is that you need to run this command in every buffer you want
the mapping active. To do this your plugin can use an `autocommand`. Here's a full
example, using this concept plus the previously mentioned configuration options:
if !exists('g:yourplugin_map_keys')
let g:yourplugin_map_keys = 1
endif
if !exists('g:yourplugin_map_prefix')
let g:yourplugin_map_prefix = '<leader>'
endif
if g:yourplugin_map_keys
execute "autocommand FileType python" "nnoremap <buffer>" g:yourplugin_map_prefix."d" ":call <sid>YourPluginDelete()<CR>"
endif
Now your plugin will define a key mapping only for Python buffers, and your users can
disable or customize this mapping as they see fit.
This mapping command is quite ugly. Unfortunately that's the price of using
Vimscript and trying to make a plugin that will work for many users. Later I'll talk
about one possible solution to this ugliness.
### Localizing Settings
Just as you should make mappings local to buffers when appropriate, you should do the
same with settings like `foldmethod`, `foldmarker` and `shiftwidth`. Not all
settings can be set locally in a buffer. You can read `:help <settingname>` to see
if it's possible.
You can use `setlocal` instead of `set` to localize settings to individual buffers.
Like with mappings you'll need to use an autocommand to run the `setlocal` command
every time the users opens a new buffer.
Autoload is Your Friend
-----------------------
If your plugin is something that users will be using all the time you can skip this
section.
If you're writing something that will only be used in specific cases, you can help
your users by using Vim's `autoload` functionality to delay loading its code until
the user actually tries to use it.
The way `autoload` works is fairly simple. Normally you would bind a key to call one of your
plugin's functions with something like this:
```vim
nnoremap <leader>z :call YourPluginFunction()<CR>
```
You can use autoloading by prepending `yourplugin#` to the name of the function:
```vim
nnoremap <leader>z :call yourplugin#YourPluginFunction()<CR>
```
When this mapping is run, Vim will do the following:
1. Check to see if `YourPluginFunction` is already defined. If so, call it.
2. Otherwise, look in `~/.vim/autoload/` for a file named `yourplugin.vim`.
3. If it exists, parse and load the file (which presumably defines
`YourPluginFunction` somewhere inside of it).
4. Call the function.
This means that instead of putting all of your plugin's code in
`plugin/yourplugin.vim` you can put just the key mapping code there and pull the rest
out into `autoload/yourplugin.vim`.
If your plugin has a decent amount of code this can reduce the startup time of Vim by
a significant amount.
Check out the full documentation of `autoload` by running `:help autoload` to learn
much more.
Backwards Compatibility is a Big Deal
-------------------------------------
Once you've written your Vim plugin and released it into the wild, you have to
maintain it. Users will find bugs and ask for new features.
Part of being a responsible developer of any kind, including a Vim plugin author, is
maintaining backwards compatibility, *especially* for tools that users will use every
day and burn into their muscle memory. Users rely on tools to work, and tools that
break backwards compatibility will quickly lose users' trust.
Maintaining backwards compatibility will cause your plugin's code to get crufty in
spots, but it's the price of maintaining your users' happiness.
### What Matters for Backards Compatibility?
For a Vim plugin the most important part of staying backwards compatible is ensuring
that key mappings, customized or not, continue to do what users expect.
If your plugin maps key `X` to do `Y`, then pressing `X` should *always* do `Y`, even
if you change how `Y` is called by renaming `Y` to `Z`. This may mean changing `Y`
into a wrapper function which simply calls `Z`.
There are many other aspects of backwards compatibility that you will have to
consider, depending on the purpose of your plugin. The rule of thumb you should
follow is: if a user uses this plugin on a daily basis and has its usage burned into
their muscle memory, updating the plugin should not make them relearn anything.
### Use Semantic Versioning So I Can Stay Sane
A fast, simple, easy way to document your plugin's state is to use [semantic
versioning][].
Semantic versioning is simply the idea that instead of picking arbitrary version
numbers for releases of your project, you use version numbers that describe the
backwards-compatible state in a meaningful way.
In a nutshell, these rules describe how you should select version numbers for new
releases:
* Version numbers have three components: `major.minor.bugfix`. For example: `1.2.4`
or `2.13.0`.
* Versions with a major version of 0 (e.g. `0.2.3`) make no guarantees about
backwards compatibility. You are free to break anything you want. It's only after
you release `1.0.0` that you begin making promises.
* If a release introduces backwards-incompatible changes, increment the major version
number.
* If a release is backwards-compatible, but adds *new* features, increment the minor
version number.
* If a release simply fixes bugs, refactors code, or improves performance, increment
the bugfix version number.
This simple scheme makes it easy for users to tell (in a broad sense) what has
changed when they update your project.
If only the bugfix number has changed they can update without fear and continue on
without worrying about changes unless they're curious.
If the minor version number has changed they might want to look at the changelog to
see what new features they may want to take advantage of, but if they're busy they
can simply update and move on.
If the major version number has changed it's a major red flag, and they'll want to
read the changelog carefully to see what is different.
Some people don't like semantic versioning for the following reason:
> If I have to increment the major version number every time I make
> backwards-incompatible changes, I'll quickly be at ugly versions like 24.1.2!
To this I say: "Yes, but if that happens you're doing things wrong in the first
place."
Keep your project in "beta" (i.e. version `0.*.*`) for as long as you need to
experiment freely. *Take your time* and make sure you've gotten things (mostly)
right. Once you release `1.0.0` it's time to start being responsible and caring
about backwards compatibility.
Breaking functionality all the time harms your users by reducing their productivity
and frustrating them. Yes, it means adding some cruft to your code over time, but
it's the price of not being evil.
[semantic versioning]: http://semver.org/
Document Everything
-------------------
A critical part of releasing a Vim plugin to the world is writing documentation for
it. Vim has fantastic documentation itself, so your plugins should follow in its
footsteps and provide thorough docs.
### Pick Some Requirements and Stick to Them
The most important part of your documentation is telling users what they need to have
in order to use your plugin. Vim runs on nearly every system imaginable and can be
compiled in many different ways, so being specific about your plugin's requirements
will save users a lot of trial and error.
* Does your plugin only work with Vim version X.Y or later?
* Does it require Python/Ruby/etc support compiled in? Which version?
* Does it not work on Windows?
* Does it rely on an external tool?
If the answer to any of those questions is "yes", you *must* mention it in the
documentation.
### Write a README
The first step to documenting your plugin is to write a README file for the
repository. You can also use the text of this file as the description if you upload
your plugin to the [vim website][], or the content of your plugin's website if you
create one for it.
Some examples of things to include in your README are:
* An overview of what the plugin does.
* Screenshots, if possible.
* Requirements.
* Installation instructions.
* Common configuration options that many users will want to know.
* Links to:
* A canonical web address to find the plugin.
* The bug tracker for the plugin.
* The source code or repository of the plugin.
[vim website]: http://www.vim.org/
### Create a Simple Website
This isn't strictly necessary, but having a simple website for your plugin is an
extra touch that makes it seem more polished.
It also gives you a canonical URL that people can visit to get the latest information
about your plugin.
I've made simple sites for both of my plugins: [Gundo][] and [Threesome][]. Feel
free to use them as an example or even take their code and use it for your own plugin
sites if you like.
### Write a Vim Help Document
The bulk of your plugin's documentation should be in the form of a Vim help document.
Users are used to using Vim's `:help` and they'll expect to be able to use it to
learn about your plugin.
Creating a help document is as easy as creating a `doc/yourplugin.txt` file in your
project. It will be indexed automatically by `pathogen#helptags()` so your users
will have the docs at their fingertips.
Two easy ways to learn the syntax of help files are by reading `:help help-writing`
and using an existing plugin's help file as an example.
Take your time and craft a beautiful help file you can be proud of. Don't be afraid
to add a bit of personality to your docs to break the dryness. The [syntastic help
file][syntastic help] is a great example (especially the `About` section).
Things to include in your documentation:
* A brief overview of the plugin.
* A more in-depth description of how the plugin is used.
* Every single key mapping the plugin creates.
* Ways to extend the plugin, if applicable.
* All configuration variables (including their default values!).
* The plugin's changelog.
* The plugin's license.
* Links to the plugin's repository and bug tracker.
In a nutshell: your help file should contain *anything* a user would ever need to know
about your plugin.
[syntastic help]: https://github.com/scrooloose/syntastic/blob/master/doc/syntastic.txt
### Keep a Changelog
The last part of documenting your project is keeping a changelog. You can skip this
while your project is still in "beta" (i.e. less than version `1.0.0`) but once you
officially release a real version you need to keep your users informed about what has
changed between releases.
I like to include this log in the README, the plugin's website, and the
documentation to make it as easy as possible for users to see what's changed.
Try to keep the language of the changelog at a high enough level for your users to
understand without knowing anything about the implementation of your plugin. Things
like "added feature X" and "fixed bug Y" are great, while things like "refactored the
inner workings of utility function Z" are best left in commit messages.
Making Vimscript Palatable
--------------------------
The worst part about writing Vim plugins is, without a doubt, dealing with Vimscript.
It's an esoteric language that's grown organically over the years seemingly without
any strong design direction.
Features are added to Vim, then Vimscript features are added to control those
features, then hacky workarounds are added for flexibility.
The syntax is terse, ugly and inconsistent. Is `" foo` a comment? Sometimes.
Much of the time you'll spend writing your first plugin will be learning how to do
things in Vimscript. The help documentation on all of its features is thorough, but
it can be hard to find what you're looking for if you don't know the exact name.
Looking through other plugins is often very helpful in pointing you toward what you
need.
There are a couple of ways to ease the pain of Vimscript, and I'll briefly talk about
two of them here.
### Wrap. Everything.
The first piece of advice I have is this: if you want to make your plugins readable
and maintainable then you need to wrap up functionality even more than you would in
other languages.
For example, my [Gundo][] plugin has a few utility functions that look like this:
```vim
function! s:GundoGoToWindowForBufferName(name)"{{{
if bufwinnr(bufnr(a:name)) != -1
exe bufwinnr(bufnr(a:name)) . "wincmd w"
return 1
else
return 0
endif
endfunction"}}}
```
This function will go to the window for the given buffer name and gracefully handle
the case where the buffer/window does not exist. It's verbose but much more readable
than the alternative of using that `if` statement in every place I need to switch
windows.
As you write your plugin you'll "grow" a number of these utility functions. Any time
you duplicate code you should think about creating one, but you should also do so any
time you write a particularly hairy line of Vimscript. Pulling complex lines out
into named functions will save you a lot of reviewing and rethinking down the line.
### Scripting Vim with Other Languages
Another option for making Vimscript less painful is to simply not use it much at all.
Vim includes support for creating plugins in a number of other languages like Python
and Ruby. Many plugin authors choose to move nearly all of their code into another
language, using a small Vimscript "wrapper" to expose it to the user.
I decided to try this approach with [Threesome][] after seeing it used in the
[vim-orgmode][] plugin to great effect. Overall I consider it to be a good idea,
with a few caveats.
First, using another language will requires your plugin's users to use a version of
Vim compiled with support for that version. In this day and age it's usually not
a problem, but if you want your plugin to run everywhere then it's not an option.
Using another language adds overhead. You need to not only learn Vimscript but also
the interface between Vim and the language. For small plugins this can add more
complexity to the project than it saves, but for larger plugins it can pay for
itself. It's up to you to decide whether it's worth it.
Finally, using another language does not entirely insulate you from the
eccentricities of Vimscript. You still need to learn how to do most things in
Vimscript — using another language simply lets you wrap most of this up more neatly
than you otherwise could.
[vim-orgmode]: https://github.com/jceb/vim-orgmode
### Unit Testing Will Make You Drink
Unit testing (and other types of testing) is becoming more and more popular today.
In particular the Python and Ruby communities seem to be getting more and more
excited about it as time goes on.
Unfortunately, unit testing Vim plugins lies somewhere between "painful" and
"[garden-weasel][]ing your face" on the difficulty scale.
I tried adding some unit tests to [Gundo][], but even after looking at a number of
frameworks I was spending hours simply trying to get my tests to function.
I didn't even bother trying to add tests to [Threesome][] because for every hour
I would have spent fighting Vim to create tests I could have cleaned up the code and
fixed bugs instead.
I'll gladly change my opinion on the subject if someone writes a unit testing
framework for Vim that's as easy to use as [Cram][]. In fact, I'll even buy the
author a $100 bottle of scotch (or whatever they prefer).
Until that happens I personally don't think it's worth your time to unit test Vim
plugins. Spend your extra hours reading documentation, testing things manually with
a variety of settings, and thinking hard about your code instead.
[garden-weasel]: http://www.amazon.com/Garden-Weasel-90206/dp/B002ECYRH4
[Cram]: https://bitheap.org/cram/
TL;DR
-----
Writing Vim plugins is tricky. Vimscript is a rabbit hole of sadness and despair,
and trying to please all your users while maintaining backwards compatibility is
a monumental task.
With that said, creating something that people use every day to help them make
beautiful software projects is extremely rewarding. Even if your plugin doesn't get
many users, being able to use a tool *you wrote* is very satisfying.
So if you've got an idea for a plugin that would make Vim better just sit down, learn
about Vimscript, create it, and release it so we can all benefit.
If you have any questions or comments feel free to hit me up [on
Twitter][@stevelosh]. You might also enjoy following [@dotvimrc][] where I try to
tweet random, bite-sized lines you might like to put in your `.vimrc` file.
[@stevelosh]: http://twitter.com/stevelosh
[@dotvimrc]: http://twitter.com/dotvimrc