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