TL;DR
LuaSnip is fast and doesn’t have to be complicated. Give it a try!
Even if that article shows how LuaSnip shines, I have great respect for the work that has gone into UltiSnips. It is still a reliable, reasonably fast plugin given the constraint it operates in (in particular, Vim compatibility requires a fair amount of Vimscript). I’ve written this article shortly after trying LuaSnip and I’m still very much evaluating it.
Introduction
Snippets are a convenient feature of some text editors to insert and adapt reusable pieces of code. For instance, snippets for for
loops are common, to get the tedious bits of the syntax out of the way.
To get this feature in Vim back in the days, I started using UltiSnips. There are default snippets sets for it, and it’s easy to write custom snippets. These custom snippet can call Bash or Python scripts if you need more dynamic replacements. UltiSnips has been very powerful and has served me quite well over the past decade or so, and I have kept it when I migrated to NeoVim a few years ago.
Startup time
Every once in a while though, I run the excellent vim-startuptime command to assess the impact of various configurations and plugins on the startup time of Neovim. With UltiSnips and the corresponding completion plugins in my configuration, the first few lines are:
Extra options: []
Measured: 10 times
Total Average: 104.218300 msec
Total Max: 109.719000 msec
Total Min: 99.533000 msec
AVERAGE MAX MIN
------------------------------
64.218600 70.419000 61.066000: ~/.config/nvim/init.lua
6.255900 7.238000 5.764000: loading packages
3.939000 5.338000 3.533000: ~/.local/share/nvim/site/pack/paqs/start/onedark.nvim/colors/onedark.lua
3.651100 4.003000 3.381000: loading rtp plugins
2.761600 3.957000 2.474000: expanding arguments
2.683900 3.035000 2.566000: reading ShaDa
2.418700 3.610000 2.131000: sourcing vimrc file(s)
2.389600 2.751000 2.228000: /usr/share/nvim/runtime/filetype.lua
1.954900 2.293000 1.702000: loading after plugins
1.842500 2.015000 1.763000: BufEnter autocommands
1.583350 3.532000 0.018000: ~/.local/share/nvim/site/pack/paqs/start/ultisnips/plugin/UltiSnips.vim
1.027700 1.170000 0.831000: ~/.local/share/nvim/site/pack/paqs/start/nvim-treesitter/plugin/nvim-treesitter.lua
0.968200 1.071000 0.860000: ~/.local/share/nvim/site/pack/paqs/start/cmp-nvim-ultisnips/after/plugin/cmp_nvim_ultisnips.lua
0.859000 1.014000 0.702000: ~/.local/share/nvim/site/pack/paqs/start/nvim-treesitter-textobjects/plugin/nvim-treesitter-textobjects.vim
0.590700 0.674000 0.524000: ~/.local/share/nvim/site/pack/paqs/start/indent-blankline.nvim/plugin/indent_blankline.vim
0.557200 0.817000 0.493000: init highlight
0.553200 0.898000 0.322000: opening buffers
The init.lua
line covers a lot of different plugins and mappings set up in that file. Besides the onedark.nvim colorscheme1, the next biggest contributor is ~/…/ultisnips/plugin/UltiSnips.vim
. The script to integrate UltiSnips with cmp, ~/…/cmp-nvim-ultisnips/after/plugin/cmp_nvim_ultisnips.lua
, is not far behind. In total, we spend nearly 4 ms of the startup time for UltiSnips-related files, on top of the setup done in init.lua
. That feels suboptimal, so I’ve been looking for a possible snippet plugin alternative.
Installing LuaSnip
LuaSnip aims to be a faster2 snippet engine, with support of treesitter in the snippets. It can also understand the LSP snippet “format”. Finally, it’s a pure Lua plugin, without any Python requirement, contrary to UltiSnips. That makes installation on various system much easier.
Its drawbacks for me are that it does not support the same snippet definition as UltiSnips and even in its own SnipMate-like syntax, backticks to execute code are not supported. As a result, I’ll have to migrate my (admittedly very small) snippet collection. Conversely, if I want to do more advanced things, I’ll have to learn the relatively complex “VS Studio Code” snippets in JSON or even the pure Lua snippets.
That’s said, I believe I can get the LuaSnip benefits without writing Lua snippets or JSON ones, at least to start. So let’s try and do that, using only the SnipMate-like syntax!
Overview of the Configuration Changes
I’ve made the following changes.
- Remove UltiSnips and its associated plugins, namely for completion with cmp and the telescope integration.
- Remove the UltiSnips mappings.
- Add LuaSnip (following the instructions in the Readme), its cmp integration, a set of snippets and a telescope integration along with some mappings (requires nvim 0.7+3):
vim.keymap.set({ "i", "s" }, "<C-i>", function() require'luasnip'.jump(1) end, { desc = "LuaSnip forward jump" }) vim.keymap.se({ "i", "s" }, "<M-i>", function() require'luasnip'.jump(-1) end, { desc = "LuaSnip backward jump" })
- Migrate my existing snippets, see below.
- Add code to load the snippet set and my own snippet collection:
require("luasnip.loaders.from_vscode").lazy_load() require("luasnip.loaders.from_snipmate").lazy_load({ paths = {"./snippets"} })
After
Let’s run vim-startuptime again. Here are the new top contributors:
Extra options: []
Measured: 10 times
Total Average: 98.251400 msec
Total Max: 100.159000 msec
Total Min: 96.519000 msec
AVERAGE MAX MIN
------------------------------
60.469100 62.341000 59.619000: ~/.config/nvim/init.lua
5.991100 6.219000 5.865000: loading packages
4.088400 4.162000 3.942000: loading after plugins
3.635700 3.752000 3.415000: loading rtp plugins
3.561800 3.936000 3.220000: ~/.local/share/nvim/site/pack/paqs/start/onedark.nvim/colors/onedark.lua
2.550500 2.628000 2.492000: reading ShaDa
2.454200 2.525000 2.401000: expanding arguments
2.363000 2.420000 2.270000: /usr/share/nvim/runtime/filetype.lua
2.271100 2.337000 2.146000: sourcing vimrc file(s)
1.701000 1.767000 1.635000: BufEnter autocommands
1.676700 2.049000 1.366000: opening buffers
1.034100 1.280000 0.782000: ~/.local/share/nvim/site/pack/paqs/start/nvim-treesitter/plugin/nvim-treesitter.lua
0.901000 1.021000 0.799000: ~/.local/share/nvim/site/pack/paqs/start/nvim-treesitter-textobjects/plugin/nvim-treesitter-textobjects.vim
0.590700 0.674000 0.524000: ~/.local/share/nvim/site/pack/paqs/start/indent-blankline.nvim/plugin/indent_blankline.vim
0.549500 0.789000 0.514000: init highlight
The snippet infrastructure is now absent from that list! More importantly, the overall startup time is down, and we can be confident that the new calls to load the snippets in init.lua
are not costlier than UltiSnips settings before, because the init.lua
line is down as well.
As a bonus, the snippet expansion feels slightly snappier with LuaSnip, although it might be an illusion and I don’t have hard numbers to back this claim.
Migrate my Snippet Collection
Back to the topic of migrating existing UltiSnips snippets: LuaSnip will loudly complain when given UltiSnips snippets but it’s relatively easy to rewrite those snippets to the SnipMate-like format that LuaSnip understands.
Two Syntaxes
On the one hand, UltiSnips snippets roughly follow this syntax
snippet trigger "Comment" option
snippet content
endsnippet
And so, my markdown snippets would look like this:
priority 10
snippet bjtoday "“Bullet Journal”-styled date for today" b
# `date +'%F %A'`
endsnippet
On the other hand, LuaSnip uses a simplified version of SnipMate snippets:
# Comment
snippet toggle
snippet content
Simple Snippets
Since I heavily rely on snippet sets, I have only about 30 snippets defined in my own snippet collection. Most of them are really simple, with only a few lines and almost no interactive text. So for most of those, the process has been to simply remove the priority …
lines and then a simple substitution command4 did the trick.
Advanced Snippets With Environment Variables
However, there is also the case of the snippets calling external commands, like date
in the example below:
snippet bjtoday "“Bullet Journal”-styled date for today" b
# `date +'%F %A'`
endsnippet
The problem is, to call arbitrary commands, one needs to define the snippets in Lua.
It turns out though that nearly all my snippets calling external command were actually inserting a date. And luckily LuaSnip defines “environment variables” holding just the values I need like $CURRENT_MONTH
. So that snippet becomes:
# “Bullet Journal”-styled date for today
snippet bjtoday
# ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE} $CURRENT_DAY_NAME_SHORT
and we get to keep the simple SnipMate-like syntax!
You can find more of those environment variables in the sources.
EDIT(2022-08-03): See also snippet-converter.nvim to convert between various snippet formats.5
What’s Next
I now have a slightly faster NeoVim and the snippet syntax that I use the most is simpler. However, there is more to explore!
So far, I’ve steered clear of writing more complicated JSON and Lua snippets. The latter would be necessary to unlock smart snippets using treesitter context. I’ll look into that next, in particular to generate go error handling code.
This is also a very simple configuration, the impact on startup time of LuaSnip might go up slightly as we use more advanced feature, but during my tests, even using all available features, it was much lighter than UltiSnips.
EDIT(2022-07-31): Follow-up posts dig deeper in other aspects of LuaSnip
Resources
- Many more details are covered in LuaSnip documentation.
- The SnipMate help contains the SnipMate snippet syntax, if you are unfamiliar with it.
- The LSP snippet documentation is also helpful.
That run is actually on a fork of onedark.nvim that contains a massive speed up. The changes of that fork are being up streamed in this PR. ↩︎
According to that comment from the author for instance: https://github.com/L3MON4D3/LuaSnip/issues/60#issuecomment-873630664. ↩︎
This code snippet uses new APIs introduced in Neovim 0.7 and the newly freed
<C-i>
, see https://gpanders.com/blog/whats-new-in-neovim-0-7/ for more details. ↩︎For instance:
%s/^\(snippet \+\S\+\) "\(.*\)" \w\+\n\(\_.\{-}\)endsnippet/# \2^M\1^M \3^M
. ↩︎Thanks to Miserable-Ad-7341 for pointing this out. ↩︎
Liked this post? Subscribe:
Discussions
This blog does not host comments, but you can reply via email or participate in one of the discussions below:
Featured on: