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