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