Original photo by Anna Nekrashevich from Pexels.

5 smart mini-snippets to amp up your text editing experience in Neovim

Tiny, delightful editing utilities to brighten your day

Joosep Alviste
8 min readApr 19, 2023

--

I recently tried out Visual Studio Code a bit and, to my surprise, I actually found some inspiration in how some small text editing utilities can improve the experience of writing code. I discovered that in some cases VSCode automatically adds extra text and “autocompletes” some common patterns. Here’s an example where it adds the quotes if I type an =:

A mini-snippet in VSCode

I like to call these things “mini-snippets” (this is a term I made up and it is in no way official), and I found that they are not super commonly used in Vim or Neovim. They seem quite useful and offer a fun way to improve your text editing experience.

I want to show you a couple of mini-snippets I created and how they can be made smart with the help of regular expressions or even Treesitter. I hope that this will inspire you to write a couple of mini-snippets yourself.

Before we get into it — note that these examples are written in Lua for Neovim (version 0.9), but I’m sure that something similar can be done in Vim as well (using :h synstack instead of Treesitter, for example).

From scratch

Neovim offers a lot of possibilities for setting up these types of snippets out of the box. Let’s take a look at some built-in ways to write mini-snippets.

1. Automatically add a trailing comma in JSON files

See the source in my dotfiles.

Automatically add a trailing comma with o

A pain with JSON files is that they do not allow having trailing commas for the last item in a list or an object definition, so when adding a new item, a comma needs to be added to the end of the previous line. Some editors are smart enough to add a comma to the end of the line automatically when a new line is added, but Vim doesn’t do this by default.

Luckily, it is very simple to add a custom keymap in Vim that adds a custom behaviour when creating a new line:

-- ftplugin/json.lua
vim.keymap.set('n', 'o', function()
return 'A,<cr>'
end, { buffer = true, expr = true })

However, this adds a comma any time a new line is added with o. With a bit of help from regular expressions, we can only trigger this if there is no existing comma. We can also make our mini-snippet smarter by ignoring lines ending with a { and a few other cases. Here is the mapping for o that can be added to ftplugin/json.lua:

-- ftplugin/json.lua
vim.keymap.set('n', 'o', function()
local line = vim.api.nvim_get_current_line()

local should_add_comma = string.find(line, '[^,{[]$')
if should_add_comma then
return 'A,<cr>'
else
return 'o'
end
end, { buffer = true, expr = true })

With a bit of work, any snippet can be made more dynamic with some custom Lua code. I call these types of intelligent snippets smart mini-snippets.

2. Automatically add quotes in Vue element attributes

See the source in my dotfiles.

Automatically adding quotes when adding an attribute

A small inconvenience in Vue and HTML files is that element attribute quotes need to be manually added. To set up a mini-snippet, we can use a similar approach to the JSON snippet by adding some extra logic when the user types =:

-- ftplugin/vue.lua
vim.keymap.set('i', '=', function()
return '=""<left>'
end, { expr = true, buffer = true })

This, again, will be triggered every time an equals sign is typed, even if you are not adding a new attribute to an element. We could probably use a regular expression to check if our cursor is in the correct place, but Neovim provides something even smarter in the form of Treesitter, which requires the nvim-treesitter plugin. Treesitter gives us access to a syntax tree where we can check the type of the node that the cursor is currently located in. To figure out what the syntax tree looks like, we can use the :InspectTree command.

Treesitter tree for the template in a Vue file

We can use Treesitter to make this mapping smart and only add the quotes if we’re inside an attribute node:

-- ftplugin/vue.lua
vim.keymap.set('i', '=', function()
-- The cursor location does not give us the correct node in this case, so we
-- need to get the node to the left of the cursor
local cursor = vim.api.nvim_win_get_cursor(0)
local left_of_cursor_range = { cursor[1] - 1, cursor[2] - 1 }

local node = vim.treesitter.get_node { pos = left_of_cursor_range }
local nodes_active_in = {
'attribute_name',
'directive_argument',
'directive_name',
}
if not node or not vim.tbl_contains(nodes_active_in, node:type()) then
-- The cursor is not on an attribute node
return '='
end

return '=""<left>'
end, { expr = true, buffer = true })

Treesitter can be used to make these mini-snippets more intelligent by providing access to the whole syntax tree. The possibilities are endless!

3. Automatically close a self-closing tag

See the source in my dotfiles.

Automatically closing a self-closing tag

Another rather common flow is closing a self-closing tag with />. Typing / on its own should be enough for us to be able to automatically finish closing the tag since there’s no other reason to type a / while inside a start tag.

The main structure of this snippet is quite similar to the previous ones, but it takes a little bit more work to figure out if the cursor is in the correct location:

-- ftplugin/vue.lua
vim.keymap.set('i', '/', function()
local node = vim.treesitter.get_node()
if not node then
return '/'
end

local first_sibling_node = node:prev_named_sibling()
if not first_sibling_node then
return '/'
end

local parent_node = node:parent()
local is_tag_writing_in_progress = node:type() == 'text'
and parent_node:type() == 'element'

local is_start_tag = first_sibling_node:type() == 'start_tag'

local start_tag_text = vim.treesitter.get_node_text(first_sibling_node, 0)
local tag_is_already_terminated = string.match(start_tag_text, '>$')

if
is_tag_writing_in_progress
and is_start_tag
and not tag_is_already_terminated
then
local char_at_cursor = vim.fn.strcharpart(
vim.fn.strpart(vim.fn.getline '.', vim.fn.col '.' - 2),
0,
1
)
local already_have_space = char_at_cursor == ' '

-- We can also automatically add a space if there isn't one already
return already_have_space and '/>' or ' />'
end

return '/'
end, { expr = true, buffer = true })

We need to check the node parents and siblings as well as the text content of the node. Luckily, the Neovim Treesitter API allows us to query all of the needed information.

With the help of plugins

In the previous section, we wrote these mini-snippets more or less from scratch. However, we can also use plugins to help us put together these snippets with less effort.

4. Automatically add spacing in Vue text interpolation

See the source in my dotfiles.

Automatically adding spacing in template interpolation

A common case in Vue files is text interpolation in templates with {{ myVariable }}. Conventionally, spacing is included inside the braces. It would be nice if Vim itself were smart enough to add the spacing as well as the closing braces automatically.

nvim-autopairs is a plugin for automatically closing parenthesis, brackets, and other pair-able things, but it also includes some utilities for creating your own pairing snippets. Custom rules for the auto-pairing can be added quite easily:

require('nvim-autopairs').add_rules {
Rule('{{', ' }', 'vue'):set_end_pair_length(2),
}

set_end_pair_length is used to move the cursor left twice after inserting the ending pair. And we can even make use of Treesitter to restrict this snippet to Vue text nodes:

require('nvim-autopairs').add_rules {
Rule('{{', ' }', 'vue')
:set_end_pair_length(2)
:with_pair(ts_conds.is_ts_node 'text'),
}

For simple “pairs-like” mini-snippets, using custom nvim-autopairs rules can work great and be quite straightforward to set up.

5. Automatically finish an IF statement

See the source in my dotfiles.

Automatically expanding an if statement

We can also use a fully-fledged snippet engine to create these autosnippets. LuaSnip lets you mark snippets as “autosnippets”, meaning that they don’t need to be manually expanded. Other snippet engines can probably be configured to do something similar.

To enable autosnippets in LuaSnip, pass in enable_autosnippets = true to luasnip.config.set_config:

require('luasnip').config.set_config {
-- Other configuration...
enable_autosnippets = true,
}

Now autosnippets can be added and an automatic IF statement would look like this:

local ls = require 'luasnip'
local s = ls.s
local fmt = require('luasnip.extras.fmt').fmt
local i = ls.insert_node

ls.add_snippets('lua', {
s(
{
trig = 'if',
condition = function()
local ignored_nodes = { 'string', 'comment' }

local pos = vim.api.nvim_win_get_cursor(0)
-- Use one column to the left of the cursor to avoid a "chunk" node
-- type. Not sure what it is, but it seems to be at the end of lines in
-- some cases.
local row, col = pos[1] - 1, pos[2] - 1

local node_type = vim.treesitter
.get_node({
pos = { row, col },
})
:type()

return not vim.tbl_contains(ignored_nodes, node_type)
end,
},
fmt(
[[
if {} then
{}
end
]],
{ i(1), i(2) }
)
),
}, {
type = 'autosnippets',
})

The condition option can be used to check if the snippet should or should not be expanded.

With a fully-fledged snippet engine, the possibilities really are endless, especially with an engine as powerful as LuaSnip.

Final words

I hope that I was able to inspire at least some readers to create their own mini-snippets for a more convenient developer experience. These mini-snippets truly highlight how extensible Neovim is as an editor as it’s super easy to add new mappings to do whatever you want. Finally, Treesitter can be used to make these snippets quite intelligent, only triggering them when it makes sense to do so.

Try to find repetitive key presses in your workflows that can be automated, automate them, and let us know how it goes!

Thank you to Scoro for enabling me to spend time on putting together this blog post! Check out our open positions at https://www.scoro.com/careers/

--

--