UltiSnips, snippet design and GIFs

Some time ago I made D snippets for UltiSnips. It was my first real attempt to write snippets for more than just my own projects, and it didn’t go without issues. The snippets weren’t very composable, some didn’t save all that many keystrokes, there were various bugs (especially with code wrapping and indentation), etc.

I continued to improve those snippets on the side as I write more D code, and recently I’ve been trying to get them to releasable state that could hopefully be merged back into the default snippets in near future (after some exposure to the D community).

The result is now on GitHub, including a reference with GIFs demonstrating each snippet. The new snippets are more “smart”, taking advantage of Python interpolation and handle various corner cases where the older snippets weren’t useful. The snippets file is now much larger, but should be more readable for anyone interested in improving them. While much of it is D specific, I think some ideas may be useful to improve snippets for other uses. BTW... those GIFs took a surprising amount of time. More on that below.

animation demonstrating the try, catch and throw snippets

try, catch and throw used to wrap code in a try/catch with 2 catch blocks.

Snippet guidelines

While working on DSnippets I came up with some basic guidelines on how to design a snippet. Many of these are obvious but may be easy to miss when hacking together snippets for a new project.

These guidelines refer to various D snippets in DSnips, but they are useful regardless of what language you use.

  • Use short triggers for snippets that will be used frequently (as - assert(), wr - writeln()) or if the trigger is unlikely to collide with autocompletion (sw - switch).

  • Use long triggers for snippets that will be used less often, but where a snippet can still save a lot of typing (opApply).

  • Regexp triggers are awesome... but they don’t work with autocomplete (YouCompleteMe).

    At first, most of my snippets used regexps for aliases (e.g. sw|switch). This was convenient (a user could use a snippet even after missing it by writing too many characters), but autocompletion makes it easy to discover and remember snippets. It may also be hard to get the less trivial regexps just right to avoid collisions. Now I only use regexps when necessary (e.g. DDoc as there are multiple comment styles).

  • A frequently used snippet helps even if it saves only a few keystrokes. On the other handle, if you use a snippet only once a month, saving 3 keystrokes out of 50 is not worth it.

  • To counter the previous point, even rarely used snippets can help to avoid forgetting things or to write good code.

    Operator overloading snippets such as op[] ir op$ generate a function with a correct name and can check if its signature is valid for the operator. Snippets can also help with documentation (e.g. Params: in D when defining a function; same could be done with C++ and Doxygen.

Minimize keystrokes

  • Check whether a snippet really saves keystrokes.

    Compare the number of keystrokes when typing code manually vs. with a snippet. Macros can help: qw<code>q"wp will get you all characters typed in <code>. Keep in mind that certain characters are more “difficult”, especially when holding a modifier such as Shift.

  • Sometimes, a snippet only saves keystrokes when used to wrap code with $VISUAL. Wrapping code is usually more intuitive than indent this code, surround it, move to top, add a header.

  • It is useful to have help information as the default contents of a tabstop, e.g. $1{/*loop counter*/}. With a few exceptions:

    • If the user may want to keep the tabstop empty (e.g. function params), having a default would require one more keystroke (<Backspace>) to delete the default.
    • If there is an obvious default (e.g. void for function return types), using it will save keystrokes.
  • Don’t forget to use ${0} as the last tabstop (where it makes sense). Upon reaching ${0}, UltiSnips considers a snippet to be done.

    If a snippet without ${0} is used in a tabstop of another snippet, trigger must be pressed twice to end the inner snippet; once to exit the inner snippet and once to move to the next tabstop of the outer snippet. With ${0} UntiSnips knows that the inner snippet is done and will move to the next tabstop of the outer snipper directly.

Snippets should work together

  • Tabstops should be placed (where possible) to enable other snippets to be used within the tabstops.

    For example, the module snippet contains a tabstop for the license header, which has default contents (Boost license) but can be changed to GPL simply by typing gpl<Trigger>.

  • The final tabstop (${0}) should place the cursor somewhere useful, even for one-liners.

    For example, imp - import puts the cursor on the next line so imp can be used again to import another module, try places the cursor so catch can be used immediately after (and catch places the cursor so another catch can be used...), same for sw - switch / case and so on.

  • Snippets should be usable from within other snippets when it makes sense (or rather, should be usable anywhere that makes sense). E.g. snippets for expressions that return a value such as format, tup - tuple, new should not be followed by a newline or a semicolon.

Recording Vim GIFs

While I often see articles with GIFs showing off Vim, it’s been surprisingly difficult hard to find a convenient way to record GIFs en masse. DSnips has tens of snippets and recording each one with an ffmpeg script would be way too cumbersome.

I wanted to find a recording tool that could:

  1. Start/stop with a keypress, without having a window open in the foreground
  2. Record arbitrary screen area
  3. Run on Linux
  4. Output GIFs (or something I can trivially batch convert to GIFs)

Unfortunately I didn’t find that tool. (I still think I might be missing something obvious.)

What I did find was Byzanz, which can be used from command-line and can be found in Debian/Ubuntu/Mint repos:

sudo apt-get install byzanz

Byzanz can do #2, #3 and #4, but can’t really do #1 (recording duration is fixed and passed as a parameter to the byzanz-record command).

Since it’s usable from the command line, I was able to at least create a few simple mappings to launch Byzanz (you might need to modify these if you want to use them):

mappings

  • nnoremap <Leader>rqq :!byzanz-record<space>-w<space>560<space>-h<space>80<space>-d<space>24<space>gvim.gif&<CR><CR>
  • nnoremap <Leader>rwq :!byzanz-record<space>-w<space>560<space>-h<space>160<space>-d<space>30<space>gvim.gif&<CR><CR>
  • nnoremap <Leader>rWq :!byzanz-record<space>-w<space>560<space>-h<space>240<space>-d<space>48<space>gvim.gif&<CR><CR>
  • nnoremap <Leader>rEq :!byzanz-record<space>-w<space>560<space>-h<space>320<space>-d<space>64<space>gvim.gif&<CR><CR>
  • nnoremap <Leader>rRq :!byzanz-record<space>-w<space>560<space>-h<space>480<space>-d<space>96<space>gvim.gif&<CR><CR>

These launch Byzanz with increasing recording times and an enlarging screen area (starting at the top/left corner of the screen). The recorded image is written to gvim.gif in the working directory. The times are longer than needed to type the snippets; an overly long GIF is better than one that ends abruptly in the middle of typing.

I ended up with a bunch of GIFs that were too long; half the time it seemed as if they were showing a code example, not a snippet usage example. I used Gimp to shorten them; Gimp opens a GIF as an image with one layer per frame, and the name of a layer specifies its duration. Most of the GIFs had a last frame taking multiple seconds, so they could be cut simply by deleting its layer.

In the end, these GIFs took way too much time, definitely more than I expected (more than a day). Maybe in future I’ll write a minimal keyboard-controlled screen recorder. With a Vim plugin. Or maybe not; too much stuff to do already.