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