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