this repo has no description
1---
2title: "Writing Vim Plugin"
3date: 2019-11-04T18:21:18+01:00
4description: |
5 Article about writing Vim plugins, but not about writing Vim plugins. It is
6 how to concieve plugin, how to go from an idea to the full fledged plugin.
7tags:
8 - vim
9 - viml
10---
11
12While there are many "tutorials" for writing plugins in Vim, I hope this one
13will be a little bit different from what is out there, because it won't be
14about writing plugin *per se*. If you want to find information about that then
15you should check out [`:h write-plugin`][h-write-plugin]. I want this article to be about how
16plugins come to life, using my own experience on writing
17[`vim-backscratch`][scratch] as an example.
18
19## Problem
20
21All plugins should start with a problem. If there is no problem, then there
22should be no code, as there is no better code than [no code][nocode]. In this
23case, my problem was pretty trivial: I wanted a temporary buffer that would let
24me perform quick edits and view SQL queries while optimising them (and run them
25from there with Tim Pope's [dadbod][dadbod]).
26
27## "Simple obvious solution"
28
29Now that we have defined the problem, we need to try the first possible solution.
30In our case, it is opening a new buffer in a new window, edit it, and then close it
31when no longer needed. It is simple in Vim:
32
33```vim
34:new
35" Edits
36:bd!
37```
38
39Unfortunately this has bunch of problems:
40
41- if we forgot to close that buffer, then it will hang there indefinitely,
42- running `:bd!` in the wrong buffer, can have unpleasant consequences,
43- this buffer is still listed in `:ls`, which is unneeded (as it is only
44 temporary).
45
46## Systematic solution in Vim
47
48Fortunately Vim has solutions for all of our problems:
49
50- the "scratch" section in [`:h special-buffers`][h-special-buffers], which
51 solves the first two problems,
52- [`:h unlisted-buffer`][h-unlisted-buffer], which solves the third problem.
53
54So now our solution looks like:
55
56```vim
57:new
58:setlocal nobuflisted buftype=nofile bufhidden=delete noswapfile
59" Edits
60:bd
61```
62
63However that is a long chain of commands to write. Of course we could condense
64the first two into a single one:
65
66```vim
67:new ++nobuflisted ++buftype=nofile ++bufhidden=delete ++noswapfile
68```
69
70But in reality this does not shorten anything.
71
72## Create a command
73
74Fortunately we can create our own commands in Vim, so we can shorten that to a
75single, easy to remember command:
76
77```vim
78command! Scratch new ++nobuflisted ++buftype=nofile ++bufhidden=delete ++noswap
79```
80
81For better flexibility, I prefer it to be:
82
83```vim
84command! Scratchify setlocal nobuflisted buftype=nofile bufhidden=delete noswap
85command! Scratch new +Scratchify
86```
87
88We can also add a bunch of new commands to give us better control over our new
89window's location:
90
91```vim
92command! VScratch vnew +Scratchify
93command! TScratch tabnew +Scratchify
94```
95
96Those commands will open a new scratch buffer in a new vertical window, and
97a new scratch buffer in a new tab page, respectively.
98
99## Make it a more "vimmy" citizen
100
101While our commands `:Scratch`, `:VScratch`, and `:TScratch` are nice, they are
102still not flexible enough. In Vim we can use modifiers like [`:h
103:aboveleft`][h-aboveleft] to define exactly where we want new windows to appear
104and our current commands do not respect that. To fix this problem, we can
105simply squash all the commands into one:
106
107```vim
108command! Scratch <mods>new +Scratchify
109```
110
111And we can remove `:VScratch` and `:TScratch` as these can be now done via
112`:vert Scratch` and `:tab Scratch` (of course you can keep them if you like, I
113just wanted the UX to be minimal).
114
115## Make it powerful
116
117This has been in my `$MYVIMRC` for some time in the form described above until
118I found out [Romain Lafourcade's snippet][redir] that provided one additional
119feature: it allowed to open a scratch buffer with the output of a Vim or shell
120command. My first thought was - hey, I know that, but I know I can make it
121better! So we can write a simple VimL function (which is mostly copied from
122romainl's snippet, with a few improvements):
123
124```vim
125function! s:scratch(mods, cmd) abort
126 if a:cmd is# ''
127 let l:output = []
128 elseif a:cmd[0] is# '!'
129 let l:cmd = a:cmd =~' %' ? substitute(a:cmd, ' %', ' ' . expand('%:p'), '') : a:cmd
130 let l:output = systemlist(matchstr(l:cmd, '^!\zs.*'))
131 else
132 let l:output = split(execute(a:cmd), "\n")
133 endif
134
135 execute a:mods . ' new'
136 Scratchify
137 call setline(1, l:output)
138endfunction
139
140command! Scratchify setlocal nobuflisted noswapfile buftype=nofile bufhidden=delete
141command! -nargs=1 -complete=command Scratch call <SID>scratch('<mods>', <q-args>)
142```
143
144The main differences are:
145
146- special case for empty command, it will just open an empty buffer,
147- use of `is#` instead of `==`,
148- use of `:h execute()` instead of `:redir`.
149
150As it is quite self-contained and (let's be honest) too specific for `$MYVIMRC`
151we can can extract it to its own location in `plugin/scratch.vim`, but to do so properly we need
152one additional thing, a command to prevent the script from being loaded twice:
153
154```vim
155if exists('g:loaded_scratch')
156 finish
157endif
158let g:loaded_scratch = 1
159
160function! s:scratch(mods, cmd) abort
161 if a:cmd is# ''
162 let l:output = []
163 elseif a:cmd[0] is# '!'
164 let l:cmd = a:cmd =~' %' ? substitute(a:cmd, ' %', ' ' . expand('%:p'), '') : a:cmd
165 let l:output = systemlist(matchstr(l:cmd, '^!\zs.*'))
166 else
167 let l:output = split(execute(a:cmd), "\n")
168 endif
169
170 execute a:mods . ' new'
171 Scratchify
172 call setline(1, l:output)
173endfunction
174
175command! Scratchify setlocal nobuflisted noswapfile buftype=nofile bufhidden=delete
176command! -nargs=1 -complete=command Scratch call <SID>scratch(<q-mods>, <q-args>)
177```
178
179## To boldly go…
180
181Now my idea was, hey, I use Vim macros from time to time, and these are just
182simple lists of keystrokes stored in Vim registers. Maybe it would be nice to have
183access to that as well in our command. So we will just add a new condition that
184checks if `a:cmd` begins with the `@` sign and has a length of two. If so, then
185set `l:output` to the spliced content of the register:
186
187```vim
188function! s:scratch(mods, cmd) abort
189 if a:cmd is# ''
190 let l:output = ''
191 elseif a:cmd[0] is# '@'
192 if strlen(a:cmd) is# 2
193 let l:output = getreg(a:cmd[1], 1, v:true)
194 else
195 throw 'Invalid register'
196 endif
197 elseif a:cmd[0] is# '!'
198 let l:cmd = a:cmd =~' %' ? substitute(a:cmd, ' %', ' ' . expand('%:p'), '') : a:cmd
199 let l:output = systemlist(matchstr(l:cmd, '^!\zs.*'))
200 else
201 let l:output = split(execute(a:cmd), "\n")
202 endif
203
204 execute a:mods . ' new'
205 Scratchify
206 call setline(1, l:output)
207endfunction
208```
209
210This gives us a pretty powerful solution where we can use `:Scratch @a` to open
211a new scratch buffer with the content of register `A`, edit it, and yank it
212back via `"ayy`.
213
214## Pluginize
215
216Now, it would be a shame to keep such a useful tool for ourselves so
217let's share it with the big world. In this case we need:
218
219- a proper project structure,
220- documentation,
221- a good catchy name.
222
223You can find help on the two first topics in [`:h
224write-plugin`][h-write-plugin] and [`:h write-local-help`][h-write-local-help]
225or in any of the bazillion tutorials on the internet.
226
227Finding a good name is something I can't help you with. I have picked
228`vim-backscratch`, because I like back scratches (everyone likes them) and, as
229a nice coincidence, because it contains the word "scratch".
230
231## Summary
232
233Creating plugins for Vim is easy, but not every functionality needs to be
234a plugin from day one. Start easy and small. If something can be done with
235a simple command/mapping, then it should be done with a simple command/mapping
236at first. If you find your solution really useful, then, and only then, you
237should think about turning it into a plugin. The whole process described in this
238article didn't happen in a week or two. It took me about a year to reach the step
239*Make it a more "vimmy" citizen*, when I heard about romainl's script on IRC.
240I didn't need anything more, so take your time.
241
242Additional pro-tips:
243
244- make it small, big plugins will require a lot of maintenance, small plugins
245 are much simpler to maintain,
246- if something can be done via a command then it should be made as a command,
247 do not force your mappings on users.
248
249[scratch]: https://github.com/hauleth/vim-backscratch
250[nocode]: https://github.com/kelseyhightower/nocode
251[dadbod]: https://github.com/tpope/vim-dadbod
252[redir]: https://gist.github.com/romainl/eae0a260ab9c135390c30cd370c20cd7
253[h-write-plugin]: https://vimhelp.org/usr_41.txt.html#write-plugin
254[h-write-local-help]: https://vimhelp.org/usr_41.txt.html#write-local-help
255[h-special-buffers]: https://vimhelp.org/windows.txt.html#special-buffers
256[h-unlisted-buffer]: https://vimhelp.org/windows.txt.html#unlisted-buffer
257[h-aboveleft]: https://vimhelp.org/windows.txt.html#%3Aaboveleft