Using Snippets in NeoVim

Tutorial on configuration and use of snippets for UltiSnips, a snippets plugin for Vim and NeoVim.

UltiSnips is an extension for vim and NeoVim that adds snippet support for the text editor. Generally speaking, a snippet is a keyword and a block of code that runs when triggered to that keyword, replacing it and its contents with programmatic text.

Why Use Snippets?

Part of the reason why I made the initial shift from Emacs to vim related to repetitive-stress issues from typing. Laptops tend in the size range that I prefer tend to feature one Control key, the left, and with the complex key chords in Emacs, it put a very heavy strain on my hands. To the point where one hard day of work would leave me unable to type for a day or so after.

Making the switch to vim corrected a lot of this and especially in that I switched to vim and swapped Escape and Caps Locks on the keyboard map, so it was much easier to pop out to Normal mode.

However, the whole ordeal made me conscious of the fact that overuse of modkeys like Control, Shift, Alt, and Super would re-introduce repetitive strain. At the time I was writing a lot of XML, which involved frequent typing of < and >, which on an American keyboard requires the Shift key.

Snippets basically remove the need to type repetitive boilerplate text. For instance, in my XML snippets file, I now have an entry for the letter p. So, if I type

p_

And then click Tab, NeoVim removes the p and replaces it with,

<para>
_
</para>

Note, the underscores here denote the cursor position. NeoVim ran the snippet block for p, it removed the p and replaced it with open and close tags for the DocBook XML para element. It put each para on a separate line and placed the cursor in the middle.

That is a fairly extensive chunk of text that I now don't have to write myself. And the value of snippets become more obvious when you introduce Python code. For instance, when I open an XML file I start by writing the root element for the file:

chapter_

I could hit Tab and call the chapter snippet, but for the root element I need something more complicated. Instead, I highlight the line and then hit Tab.

_

The word chapter disappears, but it's saved in a snippet buffer. Then I type the snippet block I want to call:

xml_

Now, when I hit tab, it renders the xml snippet block, I get this:

<?xml version="1.0" encoding="UTF-8" ?>
<chapter xml:id="example" version="5.0"
       xmlns="http://docbook.org/ns/docbook"
       xmlns:xi="http://www.w3.org/2001/XInclude"
       xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
       xmlns:xlink="http://www.w3.org/1999/xlink"
       xmlns:dion="http://avoceteditors.com/xml/dion">
_ 

</chapter>

Few things happened here. First, the chapter I yanked into the register is passed in as an argument which sets the opening and closing of the root elements along with the namespaces for the XML applications I use most often.

On top of that, notice the xml:id attribute. In the xml snippet there's a bit of Python code that extracts the filename and pairs off the extension. So, running this in the example.xml file renders the xml:id="example" attribute, which works for me since I often use unique filenames.

Lastly, it moves the cursor to the first line after the root element, with a blank space under it so that it doesn't get cluttered.

Eagle-eyed users of XML may notice some indentation issues in my XML code. Since I only use XML for prose, I don't follow standard practices there. I do indent data, such as in an info block, but para text remains entirely unindented for readability.

Installation

Using Dein, it is fairly straightforward to install UltiSnips from GitHub and to keep it up to date. In the listing of packages for Dein to install, add this line:

call dein#add('SirVer/ultisnips')

When you next start NeoVim, it'll download and install UltiSnips.

In addition to the installation, you also need to configure the key interfaces that trigger snippet processing. Add the following text somewhere outside of the Dein processing block, (mine is located in ~/.nvimrc, which I source from init.vim. That way it's easier to access and modify):

" UltiSnips Configuration
let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"
let g:UltiSnipsEditSplit="vertical"

Configuration

Snippet files follow the pattern of FILETYPE.snippets. So, a snippets file for XML would be xml.snippets, for Python python.snippets. It appears that UltiSnips looks for snippets by default at ~/.config/nvim/UltiSnips, though I'm not 100% sure that isn't something I've manually set somehow and forgotten about.

Snippet files contain a series of snippet blocks. Each block opens with the keyword snippet and closes with endsnippet. The text just after snippet defines the key used to call the snippet code. The text on the lines between the start and end define the text that the key expands. So, the earlier XML snippet for para elements looks like this in my file:

snippet p 
<para>
${VISUAL}${0}
</para>
endsnippet

The snippets block accepts some arguments from NeoVim. The ${VISUAL} block contains highlighted text that was tabbed into a register. So, if I highlight text and hit Tab, that text is written to ${VISUAL}.

The ${0} indicates where the cursor lands after rending the block. These are numeric positions that you can jump through after rendering the block to add common text that might occur in the snippet.

In addition to the basic snippet functionality here, UltiSnips also supports executing arbitrary Python code in the snippets block. For instance, the XML root element shown earlier looks a bit like this:

snippet xml
<?xml version="1.0" encoding="UTF-8" ?>
<${VISUAL} xml:id="`!p
import re
fn = re.sub('\.xml$|\.xsl$', '', fn)
snip.rv = fn`" version="5.0${1}"
    xmlns="http://docbook.org/ns/docbook"
    xmlns:xi="http://www.w3.org/2001/XInclude"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    xmlns:dion="http://avoceteditors.com/xml/dion">
$0

</${VISUAL}>
endsnippet

Note the double occurrence of ${VISUAL} here. This ensures that the opening and closing elements match. The ${0} here is where the cursor lands after it renders, but note the ${1} a little further up. I use this block for both DocBook XML version 5.0 and XSLT versions 1 or 2, depending on whether I'm targeting Xsltproc or Saxon. Having the ${1} there lets me hit C-b to jump up to that position in the text to change the version number on a stylesheet then jump back to the ${0} position.

The text contained within `!p...` is passed to the Python interpreter. Here, we import re, the Python Regular Expressions library. The variable fn provides the filename, which we then pass to the re.sub() method. This method looks for an .xml or an .xsl file extension and removes that part of the string, so we just have the file basename. (There's a better way to do this with pathlib, but I haven't updated my XML snippets file in a while.) The string set on snip.rv is what UltiSnips adds to that part of the snippets block.