My personal blog hauleth.dev
blog
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #5 from hauleth/writing-vim-plugin

Article about writing Vim plugins

authored by

Łukasz Jan Niemier and committed by
GitHub
a16dd7a3 a8e30df4

+257
+257
content/post/writing-vim-plugin.md
··· 1 + --- 2 + title: "Writing Vim Plugin" 3 + date: 2019-11-04T18:21:18+01:00 4 + description: | 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. 7 + tags: 8 + - vim 9 + - viml 10 + --- 11 + 12 + While there are many "tutorials" for writing plugins in Vim, I hope this one 13 + will be a little bit different from what is out there, because it won't be 14 + about writing plugin *per se*. If you want to find information about that then 15 + you should check out [`:h write-plugin`][h-write-plugin]. I want this article to be about how 16 + plugins come to life, using my own experience on writing 17 + [`vim-backscratch`][scratch] as an example. 18 + 19 + ## Problem 20 + 21 + All plugins should start with a problem. If there is no problem, then there 22 + should be no code, as there is no better code than [no code][nocode]. In this 23 + case, my problem was pretty trivial: I wanted a temporary buffer that would let 24 + me perform quick edits and view SQL queries while optimising them (and run them 25 + from there with Tim Pope's [dadbod][dadbod]). 26 + 27 + ## "Simple obvious solution" 28 + 29 + Now that we have defined the problem, we need to try the first possible solution. 30 + In our case, it is opening a new buffer in a new window, edit it, and then close it 31 + when no longer needed. It is simple in Vim: 32 + 33 + ```vim 34 + :new 35 + " Edits 36 + :bd! 37 + ``` 38 + 39 + Unfortunately 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 + 48 + Fortunately 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 + 54 + So now our solution looks like: 55 + 56 + ```vim 57 + :new 58 + :setlocal nobuflisted buftype=nofile bufhidden=delete noswapfile 59 + " Edits 60 + :bd 61 + ``` 62 + 63 + However that is a long chain of commands to write. Of course we could condense 64 + the first two into a single one: 65 + 66 + ```vim 67 + :new ++nobuflisted ++buftype=nofile ++bufhidden=delete ++noswapfile 68 + ``` 69 + 70 + But in reality this does not shorten anything. 71 + 72 + ## Create a command 73 + 74 + Fortunately we can create our own commands in Vim, so we can shorten that to a 75 + single, easy to remember command: 76 + 77 + ```vim 78 + command! Scratch new ++nobuflisted ++buftype=nofile ++bufhidden=delete ++noswap 79 + ``` 80 + 81 + For better flexibility, I prefer it to be: 82 + 83 + ```vim 84 + command! Scratchify setlocal nobuflisted buftype=nofile bufhidden=delete noswap 85 + command! Scratch new +Scratchify 86 + ``` 87 + 88 + We can also add a bunch of new commands to give us better control over our new 89 + window's location: 90 + 91 + ```vim 92 + command! VScratch vnew +Scratchify 93 + command! TScratch tabnew +Scratchify 94 + ``` 95 + 96 + Those commands will open a new scratch buffer in a new vertical window, and 97 + a new scratch buffer in a new tab page, respectively. 98 + 99 + ## Make it a more "vimmy" citizen 100 + 101 + While our commands `:Scratch`, `:VScratch`, and `:TScratch` are nice, they are 102 + still 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 104 + and our current commands do not respect that. To fix this problem, we can 105 + simply squash all the commands into one: 106 + 107 + ```vim 108 + command! Scratch <mods>new +Scratchify 109 + ``` 110 + 111 + And 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 113 + just wanted the UX to be minimal). 114 + 115 + ## Make it powerful 116 + 117 + This has been in my `$MYVIMRC` for some time in the form described above until 118 + I found out [Romain Lafourcade's snippet][redir] that provided one additional 119 + feature: it allowed to open a scratch buffer with the output of a Vim or shell 120 + command. My first thought was - hey, I know that, but I know I can make it 121 + better! So we can write a simple VimL function (which is mostly copied from 122 + romainl's snippet, with a few improvements): 123 + 124 + ```vim 125 + function! 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) 138 + endfunction 139 + 140 + command! Scratchify setlocal nobuflisted noswapfile buftype=nofile bufhidden=delete 141 + command! -nargs=1 -complete=command Scratch call <SID>scratch('<mods>', <q-args>) 142 + ``` 143 + 144 + The 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 + 150 + As it is quite self-contained and (let's be honest) too specific for `$MYVIMRC` 151 + we can can extract it to its own location in `plugin/scratch.vim`, but to do so properly we need 152 + one additional thing, a command to prevent the script from being loaded twice: 153 + 154 + ```vim 155 + if exists('g:loaded_scratch') 156 + finish 157 + endif 158 + let g:loaded_scratch = 1 159 + 160 + function! 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) 173 + endfunction 174 + 175 + command! Scratchify setlocal nobuflisted noswapfile buftype=nofile bufhidden=delete 176 + command! -nargs=1 -complete=command Scratch call <SID>scratch(<q-mods>, <q-args>) 177 + ``` 178 + 179 + ## To boldly go… 180 + 181 + Now my idea was, hey, I use Vim macros from time to time, and these are just 182 + simple lists of keystrokes stored in Vim registers. Maybe it would be nice to have 183 + access to that as well in our command. So we will just add a new condition that 184 + checks if `a:cmd` begins with the `@` sign and has a length of two. If so, then 185 + set `l:output` to the spliced content of the register: 186 + 187 + ```vim 188 + function! 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) 207 + endfunction 208 + ``` 209 + 210 + This gives us a pretty powerful solution where we can use `:Scratch @a` to open 211 + a new scratch buffer with the content of register `A`, edit it, and yank it 212 + back via `"ayy`. 213 + 214 + ## Pluginize 215 + 216 + Now, it would be a shame to keep such a useful tool for ourselves so 217 + let'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 + 223 + You can find help on the two first topics in [`:h 224 + write-plugin`][h-write-plugin] and [`:h write-local-help`][h-write-local-help] 225 + or in any of the bazillion tutorials on the internet. 226 + 227 + Finding 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 229 + a nice coincidence, because it contains the word "scratch". 230 + 231 + ## Summary 232 + 233 + Creating plugins for Vim is easy, but not every functionality needs to be 234 + a plugin from day one. Start easy and small. If something can be done with 235 + a simple command/mapping, then it should be done with a simple command/mapping 236 + at first. If you find your solution really useful, then, and only then, you 237 + should think about turning it into a plugin. The whole process described in this 238 + article 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. 240 + I didn't need anything more, so take your time. 241 + 242 + Additional 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