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