NeovimのスニペットエンジンLuaSnipの使い方とスニペットの書き方

NeovimのスニペットエンジンLuaSnipを使い始めたので紹介します。

スニペットの書き方が難しそうで諦めたそこのあなたに向けて書いています。

nvim-cmpでの表示例

先にまとめ

最初にLuaSnipのドキュメントを見てしまうと、スニペットの書き方が難しそう見えます。
そこで、筆者なりに考えた一番楽な書き方を見せておきます。もちろん後で解説します。

local s = ls.snippet
local i = ls.insert_node
local fmt = require("luasnip.extras.fmt").fmt
return {
s(
"const", -- トリガー名
fmt(
-- 挿入文字列
[[
const {} = {};
]],
-- カーソルジャンプの順番とプレースホルダー
{ i(1, "name"), i(2, "value") }
)
),
}

LuaSnipの導入方法

リポジトリ名だけコピペしたい人向け
L3MON4D3/LuaSnip

各々が使っているプラグインマネージャーでインストールします。ここではlazy.nvimでの書き方を紹介します。

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もインストールします。

キーマップの設定

まずはキーマップを設定するにあたって参考となる情報源を記載します。

筆者のキーマップ

筆者が実際に使っているキーマップの例を載せておきます。

local cmp = require("cmp")
local ls = require("luasnip")
local t = function(str)
return vim.api.nvim_replace_termcodes(str, true, true, true)
end
return {
["<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.snippet
local i = ls.insert_node
local fmt = require("luasnip.extras.fmt").fmt
return {
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.snippet
local i = ls.insert_node
local fmt = require("luasnip.extras.fmt").fmt
return {
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") }
)
),

名前や説明を付けたい

自動補完での表示をわかりやすくするためにnamedescを指定できます。
この場合は第1引数がテーブルとなり、trigにトリガー名を書きます。

s(
{ trig = "trigger", name = "名前", desc = "説明" },
fmt("return {};", { i(1, "first") })
),
nvim-cmpでの表示例

トリガーを正規表現にしたい

トリガー名を正規表現にするには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.snippet
local i = ls.insert_node
local 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 = "<>" }
)
),
}

途中で[=[]=]で囲っているのは[[などをエスケープするためです。

一次情報のまとめ


以上、LuaSnipの使い方でした。スニペットの書き方にとっつきにくそうなイメージがありましたが、fmtを使ってみると簡単でした。