NeovimのスニペットエンジンLuaSnipの使い方とスニペットの書き方
NeovimのスニペットエンジンLuaSnipを使い始めたので紹介します。
スニペットの書き方が難しそうで諦めたそこのあなたに向けて書いています。

先にまとめ
最初にLuaSnipのドキュメントを見てしまうと、スニペットの書き方が難しそう見えます。
そこで、筆者なりに考えた一番楽な書き方を見せておきます。もちろん後で解説します。
local s = ls.snippetlocal i = ls.insert_nodelocal fmt = require("luasnip.extras.fmt").fmt
return { s( "const", -- トリガー名 fmt( -- 挿入文字列 [[ const {} = {}; ]], -- カーソルジャンプの順番とプレースホルダー { i(1, "name"), i(2, "value") } ) ),}
LuaSnipの導入方法
L3MON4D3/LuaSnip
各々が使っているプラグインマネージャーでインストールします。ここではlazy.nvimでの書き方を紹介します。
{ "L3MON4D3/LuaSnip", dependencies = { "rafamadriz/friendly-snippets" }, version = "v2.*", build = "make install_jsregexp", config = function() require("luasnip.loaders.from_vscode").lazy_load() end, }, { "saadparwaiz1/cmp_luasnip", },
上記のように、スニペット集friendly-snippetsを導入するといいでしょう。JavaScript、Lua、Pythonなどでよく使われるであろうスニペットが詰まっています。
READMEに書かれているとおりdependencies
に指定し、require("luasnip.loaders.from_vscode").lazy_load()
を呼び出しましょう。
本記事では自動補完はnvim-cmpを想定しています。nvim-cmpでLuaSnipを扱うにはcmp_luasnipもインストールします。
キーマップの設定
まずはキーマップを設定するにあたって参考となる情報源を記載します。
- キーマップの例
- キーマップで使えるAPI
- LuaSnipのAPI:
:help luasnip-api
- nvim-cmpのAPI:
:help cmp-function
- LuaSnipのAPI:
筆者のキーマップ
筆者が実際に使っているキーマップの例を載せておきます。
local cmp = require("cmp")local ls = require("luasnip")local t = function(str) return vim.api.nvim_replace_termcodes(str, true, true, true)endreturn { ["<Tab>"] = cmp.mapping({ c = function() if cmp.visible() then cmp.select_next_item({ behavior = cmp.SelectBehavior.Insert }) else cmp.complete() end end, i = function(fallback) if cmp.visible() then cmp.select_next_item({ behavior = cmp.SelectBehavior.Insert }) elseif ls.expand_or_jumpable() then ls.expand_or_jump() else fallback() end end, s = function(fallback) if ls.jumpable() then ls.jump(1) else fallback() end end, }), ["<S-Tab>"] = cmp.mapping({ c = function() if cmp.visible() then cmp.select_prev_item({ behavior = cmp.SelectBehavior.Insert }) else cmp.complete() end end, i = function(fallback) if cmp.visible() then cmp.select_prev_item({ behavior = cmp.SelectBehavior.Insert }) elseif ls.jumpable(-1) then ls.jump(-1) else fallback() end end, s = function(fallback) if ls.jumpable(-1) then ls.jump(-1) else fallback() end end, }), ["<CR>"] = cmp.mapping.confirm({ select = true }), -- ...}
スニペットを展開できるか・ジャンプできるかに応じて設定しているだけです。
スニペットの種類
LuaSnipで扱えるスニペットの種類は3つです。
VSCodeスタイル
VSCodeスタイルはjsonで書きます。
{ "await": { "prefix": "aw", "body": "await ${0}" }}
参考になるスニペット集:rafamadriz/friendly-snippets
SnipMateスタイル
SnipMateスタイルではvim-snipmateのフォーマットで書けます。
snippet aw "await" await ${0:${VISUAL}}
参考になるスニペット集:honza/vim-snippets
Luaスタイル
Luaでスニペットを書けます。この記事で解説するのはこの書き方です。
local ls = require("luasnip")local s = ls.snippetlocal i = ls.insert_nodelocal fmt = require("luasnip.extras.fmt").fmtreturn { s("aw", fmt("await {}", i(1))),}
参考になるスニペット集:LuaSnip/Examples/snippets.lua
Luaスタイルでのスニペットの置き場所
スニペットの置き場所は次のように指定します。
require("luasnip.loaders.from_lua").load({ paths = { "~/dotfiles/.config/nvim/snippet" } })
ここで指定したディレクトリにファイルタイプ.lua
を書けば、そのファイルタイプのときに使えます。
.config/nvim/snippet/├── all.lua // どのファイルタイプでも使える├── mdx.lua└── php.lua
スニペットの書き方
LuaSnipのスニペットではテキストをノードとして扱いますが最初からそれだと難しいため、本記事では筆者の考える一番わかりやすい書き方を紹介します。
まずは次の例を見てください。
local ls = require("luasnip")local s = ls.snippetlocal i = ls.insert_nodelocal fmt = require("luasnip.extras.fmt").fmtreturn { s( "ret", fmt("return {};", { i(1, "value") }) ),}
ret// ^ カーソル位置
return value;// ^ カーソル位置
ret
を入力してスニペットを展開すると、return value;
に書き換わります。value
が選択状態となり、カーソル位置はv
になります。
s関数
s()
の第1引数がトリガー名、第2引数が挿入される文字列の処理です。
s( "ret", -- トリガー名 fmt("return {};", { i(1, "value") }) -- 挿入される文字列の処理),
fmt関数
fmt()
の第1引数が挿入したい文字列、第2引数がカーソル位置とプレースホルダーの定義です。
fmt( "return {};", -- 挿入したい文字列 { i(1, "value") } -- カーソル位置とプレースホルダーのテーブル)
{}
の部分がカーソル位置となります。
{}
とi()
の対応関係について見ていきましょう。
次のスニペットを展開するとfirst
の箇所にカーソルがジャンプします。
s( "ret", fmt("return {} {};", { i(2, "second"), i(1, "first") } )),
return second first;// ^ カーソル位置
挿入文字列の{}
の順と、fmtの第2引数の中身の順番は対応しています。i()
の引数はi(ジャンプの順番, "プレースホルダー")
となっています。
プレースホルダーなしでi(1)
でもOKです。
複数行のスニペットを書く
複数行のスニペットを書くならLuaの二重角括弧を使います。[[
と]]
で囲むだけです。
最初から複数行前提の形式で書いたほうが見やすいと筆者は考えています。
s( "imgcomponent", fmt( [[ <Image width="500" src="{}" alt="{}" /> ]], { i(1, "src"), i(2, "alt") } )),
このとき、スニペットにインデントがあれば展開後にはいい感じに取り除いてくれます。
<Image width="500" src="src" alt="alt"/>
インデントを維持したい
インデントを維持したい場合は第3引数にdedent=false
を指定します。
s( "imgcomponent", fmt( [[ <Image // 省略 /> ]], { i(1, "src"), i(2, "alt") }, { dedent = false } )),
カーソル位置の指定文字列を変えたい
カーソル位置を{}
ではなく他の文字列を使って指定するには、第3引数のdelimiters
に書きます。
s( "tdisable", fmt( [[ {/* textlint-disable */}
<>
{/* textlint-enable */} ]], { i(1, "text") }, { delimiters = "<>" } )),
挿入する文字列として{}
が含まれている場合はこの設定を忘れないようにしましょう。
カーソル位置にラベルを付けたい
{}
とi()
の対応関係をわかりやすくするために、次のようにしてラベルを付けられます。
s( "retaaa", fmt( [[ return {node2} {node1}; ]], { node1 = i(1, "first"), node2 = i(2, "second") } )),
名前や説明を付けたい
自動補完での表示をわかりやすくするためにname
やdesc
を指定できます。
この場合は第1引数がテーブルとなり、trig
にトリガー名を書きます。
s( { trig = "trigger", name = "名前", desc = "説明" }, fmt("return {};", { i(1, "first") })),

トリガーを正規表現にしたい
トリガー名を正規表現にするにはtrigEngine = "vim"
を指定します。Vimの正規表現が使えます。
s({ trig = "(trigger|fooooo)", trigEngine = "vim" }, fmt("return {};", { i(1, "first") })),

ちなみにregTrig
というオプションもありますが、こちらはLua patternであり正規表現ではありません。
Lua patterだと(a|b)
のような書き方ができないため、基本的にはVimの正規表現を使ったほうが罠にハマりにくいです。
ジャンプの最後の位置を変えたい
デフォルトで最後にジャンプする場所は、スニペットの最後のテキストの後ろです。
最後のジャンプ位置を決めたい場合はi(0, "foo")
のように0
を指定します。
その他の書き方
筆者はfmtだけで十分運用できそうです。もっと複雑な書き方を知りたい方は:help luasnip-snippets
を読みましょう。
ノードの概念からオプションの指定まで全部書いてあります。LusSnipのヘルプはめっちゃ長いです。現時点では3700行を超えています。
関数を使いたい人は:help luasnip-functionnode
を読んでみましょう。
LuaSnipをさらに便利にするおすすめ設定
いくつか設定を紹介します。
スニペットをすぐに開けるコマンドの設定
次のようにコマンドを定義することで、:LuaSnipEdit
でスニペットが開きます。
vim.api.nvim_create_user_command( "LuaSnipEdit", ':lua require("luasnip.loaders").edit_snippet_files()', {})
別のファイルタイプのスニペットを読み込む方法
filetype_extend
を使うことで、別のファイルタイプのスニペットを読み込めます。
require("luasnip").filetype_extend("astro", { "javascript" })require("luasnip").filetype_extend("typescript", { "javascript" })
UltiSnipsからの移行
ここまでの情報でultisnipsからの移行できます。マニアックな使い方をしていた人はMigrating from UltiSnipsも読むといいでしょう。
スニペットを書くためのスニペット
スニペットを書くためのスニペットです。lua.lua
としてスニペット置き場に保存しましょう。
local ls = require("luasnip")local s = ls.snippetlocal i = ls.insert_nodelocal fmt = require("luasnip.extras.fmt").fmt
return { s( "snippet", fmt( [=[ s( "<trigger>", fmt( [[ <text> ]], { i(1, "<placeholder>") } ) ), ]=], { trigger = i(1, "trigger"), text = i(2, "text"), placeholder = i(0, "placeholder") }, { delimiters = "<>" } ) ), s( "snippet-require", fmt( [[ local ls = require("luasnip") local s = ls.snippet local i = ls.insert_node local fmt = require("luasnip.extras.fmt").fmt return { <> } ]], { i(0, "snipet") }, { delimiters = "<>" } ) ),}
途中で[=[
と]=]
で囲っているのは[[
などをエスケープするためです。
一次情報のまとめ
- スニペットエンジン
- GitHubリポジトリ:L3MON4D3/LuaSnip
- GitHubのWiki:Wiki
- Luaでのスニペットの書き方(ヘルプと同じ内容):DOC.md
- nvim-cmpとの連携:saadparwaiz1/cmp_luasnip
- スニペット集:rafamadriz/friendly-snippets
以上、LuaSnipの使い方でした。スニペットの書き方にとっつきにくそうなイメージがありましたが、fmt
を使ってみると簡単でした。