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