🌊

neovim(nvim)のスニペット機能をまあまあ調べた

2025/01/15に公開

結局、個人的にvimからnvimに移行する目玉は

  • スニペット
  • ファイル操作
  • lsp

くらいかなと思ってたりして、まあもちろん他にもありますが。

以降は全てlazy.vimを使った例

最低限動作する例

~/.config/nvim/init.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "https://github.com/folke/lazy.nvim.git",
        "--branch=stable",
        lazypath,
    })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({
    {
        -- 補完エンジン nvim-cmp の設定
        "hrsh7th/nvim-cmp",
        event = "InsertEnter", -- 挿入モードに入ったときにプラグインをロード
        dependencies = { -- nvim-cmp に必要な依存プラグイン
            { "hrsh7th/cmp-buffer" }, -- 現在のバッファの内容を補完候補に含める
            { "saadparwaiz1/cmp_luasnip" }, -- LuaSnip と nvim-cmp を統合
            { "L3MON4D3/LuaSnip" }, -- スニペットエンジン LuaSnip
            { "rafamadriz/friendly-snippets" }, -- 事前定義されたスニペットコレクション
        },
        config = function()
            -- nvim-cmp の設定
            local cmp = require("cmp") -- nvim-cmp のメインモジュールをロード
            local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
            require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード

            cmp.setup({
                mapping = cmp.mapping.preset.insert({
                    ["<C-b>"] = cmp.mapping.scroll_docs(-4), -- 補完候補のドキュメントを上にスクロール
                    ["<C-f>"] = cmp.mapping.scroll_docs(4), -- 補完候補のドキュメントを下にスクロール
                    ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
                    ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
                }),
                sources = cmp.config.sources({
                    { name = "luasnip", priority_weight = 20 }, -- LuaSnip を補完候補に含める
                }, {
                    { name = "buffer" }, -- バッファの内容を補完候補に含める
                }),
            })
        end,
    },
})

nvim-cmp

スニペット自体はcompletionせずとも使えると思うんだけど、nvimの場合ほぼほぼこの補完サポートを使って発動させる場合が多いようだ。

require("lazy").setup({
    {
        -- 補完エンジン nvim-cmp の設定
        "hrsh7th/nvim-cmp",
        event = "InsertEnter", -- 挿入モードに入ったときにプラグインをロード
        dependencies = { -- nvim-cmp に必要な依存プラグイン
            { "hrsh7th/cmp-buffer" }, -- 現在のバッファの内容を補完候補に含める
            { "saadparwaiz1/cmp_luasnip" }, -- LuaSnip と nvim-cmp を統合
            { "L3MON4D3/LuaSnip" }, -- スニペットエンジン LuaSnip
            { "rafamadriz/friendly-snippets" }, -- 事前定義されたスニペットコレクション
        },

この中で関連するものも含め5パッケージがインストールされている。ここでnvim-cmpはいくつかのカテゴリーみたいなのにグルーピングして補完アクセスするようだ。ここでは

  • cmp-buffer
  • cmp_luasnip

を媒介してアクセスしており、バッファとスニペットで分けられているようである。

さらに

  • LuaSnip
  • friendly-snippets

と分離しており、この時点でまあまあ混乱しがちになった。

local cmp = require("cmp") -- nvim-cmp のメインモジュールをロード
local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード

ここで各種ロードを行っている。

require("luasnip/loaders/from_vscode")に関しては後述

で、キーマッピングしないとほとんど動かない

これは適当なhtmlを開いた時の補完サジェストであるが、ここから触る事ができない。ただしくマッピングすると

このように補完候補からsnippetを選択し、展開できるようになる。

ultisnipの例

これは古い型式のスニペットであり、基本的には以下のようにして発動する

$ git --no-pager diff init.lua
diff --git a/init.lua b/init.lua
index 72df0de..ff2ddb7 100644
--- a/init.lua
+++ b/init.lua
@@ -17,26 +17,41 @@ require("lazy").setup({
         "hrsh7th/nvim-cmp",
         event = "InsertEnter", -- 挿入モードに入ったときにプラグインをロード
         dependencies = { -- nvim-cmp に必要な依存プラグイン
-            { "hrsh7th/cmp-buffer" }, -- 現在のバッファの内容を補完候補に含める
-            { "saadparwaiz1/cmp_luasnip" }, -- LuaSnip と nvim-cmp を統合
-            { "L3MON4D3/LuaSnip" }, -- スニペットエンジン LuaSnip
-            { "rafamadriz/friendly-snippets" }, -- 事前定義されたスニペットコレクション
+--            { "hrsh7th/cmp-buffer" }, -- 現在のバッファの内容を補完候補に含める
+--            { "saadparwaiz1/cmp_luasnip" }, -- LuaSnip と nvim-cmp を統合
+--            { "L3MON4D3/LuaSnip" }, -- スニペットエンジン LuaSnip
+--            { "rafamadriz/friendly-snippets" }, -- 事前定義されたスニペットコレクション
+            { "quangnguyen30192/cmp-nvim-ultisnips" }, -- cmp と UltiSnips の連携プラグイン
+            { "SirVer/ultisnips" }, -- スニペットエンジン
+            { "honza/vim-snippets" }, -- スニペット集
         },
         config = function()
             -- nvim-cmp の設定
             local cmp = require("cmp") -- nvim-cmp のメインモジュールをロード
-            local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
-            require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード
+            -- local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
+            -- require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード
+            local cmp_ultisnips_mappings = require("cmp_nvim_ultisnips.mappings")
+
+            -- UltiSnips スニペットディレクトリを設定
+            vim.g.UltiSnipsSnippetDirectories = {
+                vim.fn.stdpath("data") .. "/lazy/vim-snippets/UltiSnips"
+            }

             cmp.setup({
                 mapping = cmp.mapping.preset.insert({
                     ["<C-b>"] = cmp.mapping.scroll_docs(-4), -- 補完候補のドキュメントを上にスクロール
                     ["<C-f>"] = cmp.mapping.scroll_docs(4), -- 補完候補のドキュメントを下にスクロール
-                    ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
-                    ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
+                    -- ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
+                    -- ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
+                    ["<Tab>"] = cmp.mapping(function(fallback)
+                        cmp_ultisnips_mappings.expand_or_jump_forwards(fallback)
+                    end, { "i", "s" }),
+                    ["<S-Tab>"] = cmp.mapping(function(fallback)
+                        cmp_ultisnips_mappings.jump_backwards(fallback)
+                    end, { "i", "s" }),
                 }),
                 sources = cmp.config.sources({
-                    { name = "luasnip", priority_weight = 20 }, -- LuaSnip を補完候補に含める
+                    -- { name = "luasnip", priority_weight = 20 }, -- LuaSnip を補完候補に含める
+                    { name = "ultisnips", priority_weight = 10 },
                 }, {
                     { name = "buffer" }, -- バッファの内容を補完候補に含める
                 }),

ソースをみるとultisnipをガンガン読むように切り替えているのはわかるんだけど、ここで重要なのは

                mapping = cmp.mapping.preset.insert({
                    ["<C-b>"] = cmp.mapping.scroll_docs(-4), -- 補完候補のドキュメントを上にスクロール
                    ["<C-f>"] = cmp.mapping.scroll_docs(4), -- 補完候補のドキュメントを下にスクロール
                    -- ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
                    -- ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
                    ["<Tab>"] = cmp.mapping(function(fallback)
                        cmp_ultisnips_mappings.expand_or_jump_forwards(fallback)
                    end, { "i", "s" }),
                    ["<S-Tab>"] = cmp.mapping(function(fallback)
                        cmp_ultisnips_mappings.jump_backwards(fallback)
                    end, { "i", "s" }),
                }),

ここでultisnipのmappingに特化した設定を行っている。こいつは基本的に<TAB>でどんどん確定していくのがdefaultなので、このような設定になっていると同時に作業イメージは以下のようになる。

高速性を重視するならアリだろう。

両者を混ぜるタイプ

たとえばbladeの場合、ultisnipでは基本的な補完しか効かないという問題が若干ある。これはhtmlを継承していないからだ

以下はluasnipだけでbladeを開いた例

このようにdivのスニペットなどが効いていない。これはultisnipには含まれるので、以下のようにして共存するという手が1つある

~/.config/nvim/init.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "https://github.com/folke/lazy.nvim.git",
        "--branch=stable",
        lazypath,
    })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({
    {
        -- 補完エンジン nvim-cmp の設定
        "hrsh7th/nvim-cmp",
        event = "InsertEnter", -- 挿入モードに入ったときにプラグインをロード
        dependencies = { -- nvim-cmp に必要な依存プラグイン
            { "hrsh7th/cmp-buffer" }, -- 現在のバッファの内容を補完候補に含める
            { "saadparwaiz1/cmp_luasnip" }, -- LuaSnip と nvim-cmp を統合
            { "L3MON4D3/LuaSnip" }, -- スニペットエンジン LuaSnip
            { "rafamadriz/friendly-snippets" }, -- 事前定義されたスニペットコレクション
            { "quangnguyen30192/cmp-nvim-ultisnips" }, -- cmp と UltiSnips の連携プラグイン
            { "SirVer/ultisnips" }, -- スニペットエンジン
            { "honza/vim-snippets" }, -- スニペット集
        },
        config = function()
            local cmp = require("cmp") -- nvim-cmp のメインモジュールをロード
            local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
            require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード
            local cmp_ultisnips_mappings = require("cmp_nvim_ultisnips.mappings")

            -- UltiSnips スニペットディレクトリを設定
            vim.g.UltiSnipsSnippetDirectories = {
                vim.fn.stdpath("data") .. "/lazy/vim-snippets/UltiSnips"
            }

            cmp.setup({
                mapping = cmp.mapping.preset.insert({
                    ["<C-b>"] = cmp.mapping.scroll_docs(-4), -- 補完候補のドキュメントを上にスクロール
                    ["<C-f>"] = cmp.mapping.scroll_docs(4), -- 補完候補のドキュメントを下にスクロール
                    ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
                    ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
                    -- ["<Tab>"] = cmp.mapping(function(fallback)
                    --      cmp_ultisnips_mappings.expand_or_jump_forwards(fallback)
                    --  end, { "i", "s" }),
                    --  ["<S-Tab>"] = cmp.mapping(function(fallback)
                    --      cmp_ultisnips_mappings.jump_backwards(fallback)
                    --   end, { "i", "s" }),
                }),
                sources = cmp.config.sources({
                    { name = "luasnip", priority_weight = 20 },
                    { name = "ultisnips", priority_weight = 10 },
                }, {
                    { name = "buffer" }, -- バッファの内容を補完候補に含める
                }),
            })
        end,
    },
})

すると...

となったりする

でも

2つも混ぜるのもアレだし、bladeは最近(記事書いてるにもかかわらず)全く使ってないからultisnipは必要ないかなあと思ったりして...混乱するし

lspと統合

init.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
    vim.fn.system({
        "git",
        "clone",
        "--filter=blob:none",
        "https://github.com/folke/lazy.nvim.git",
        "--branch=stable",
        lazypath,
    })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({
     -- Mason (LSPやツールのインストーラ)
     {
         "williamboman/mason.nvim",
         config = function()
             require("mason").setup()
         end,
     },

     -- MasonとLSPconfigの連携
     {
         "williamboman/mason-lspconfig.nvim",
         dependencies = { "neovim/nvim-lspconfig" },
         config = function()
             require("mason-lspconfig").setup({
                 ensure_installed = { "intelephense", "jsonls" },
             })

             local lspconfig = require("lspconfig")
             local capabilities = require("cmp_nvim_lsp").default_capabilities()

             local on_attach = function(client, bufnr)
                 vim.api.nvim_buf_set_keymap(bufnr, "i", "<C-k>", "<cmd>lua vim.lsp.buf.signature_help()<CR>", { noremap = true, silent = true })

                 vim.cmd([[autocmd CursorHoldI * lua vim.lsp.buf.signature_help()]])
             end

             -- PHP LSPの設定
             lspconfig.intelephense.setup({
                 capabilities = capabilities,
                 on_attach = on_attach,
                 root_dir = lspconfig.util.root_pattern("composer.json", ".git", "."),
                 settings = {
                     intelephense = {
                         files = {
                             maxSize = 5000000,
                             associations = { "*.php" },
                             includePaths = {
                                 "./_ide_helper.php",
                                 "./_ide_helper_models.php",
                             },
                         },
                     },
                 },
             })

             -- JSON LSPの設定
             lspconfig.jsonls.setup({
                 capabilities = capabilities,
                 on_attach = on_attach,
             })
       end,
    },
    {
        -- 補完エンジン nvim-cmp の設定
        "hrsh7th/nvim-cmp",
        event = "InsertEnter", -- 挿入モードに入ったときにプラグインをロード
        dependencies = { -- nvim-cmp に必要な依存プラグイン
             "hrsh7th/cmp-buffer" , -- 現在のバッファの内容を補完候補に含める
             "saadparwaiz1/cmp_luasnip" , -- LuaSnip と nvim-cmp を統合
             "L3MON4D3/LuaSnip" , -- スニペットエンジン LuaSnip
             "rafamadriz/friendly-snippets" , -- 事前定義されたスニペットコレクション
             "hrsh7th/cmp-nvim-lsp", -- LSP補完
        },
        config = function()
            -- nvim-cmp の設定
            local cmp = require("cmp") -- nvim-cmp のメインモジュールをロード
            local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
            require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード

            cmp.setup({
                mapping = cmp.mapping.preset.insert({
                    ["<C-b>"] = cmp.mapping.scroll_docs(-4), -- 補完候補のドキュメントを上にスクロール
                    ["<C-f>"] = cmp.mapping.scroll_docs(4), -- 補完候補のドキュメントを下にスクロール
                    ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
                    ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
                }),
                sources = cmp.config.sources({
                    { name = "luasnip" },
                    { name = "nvim_lsp" },
                }, {
                    { name = "buffer" }, -- バッファの内容を補完候補に含める
                }),
            })
        end,
    },

このようにスニペットに加えてlspの補完も可能になった。

lspkind-nvimでアイコン表示

        dependencies = { -- nvim-cmp に必要な依存プラグイン
             "hrsh7th/cmp-buffer" , -- 現在のバッファの内容を補完候補に含める
             "hrsh7th/cmp-path", -- パス補完
             "saadparwaiz1/cmp_luasnip" , -- LuaSnip と nvim-cmp を統合
             "L3MON4D3/LuaSnip" , -- スニペットエンジン LuaSnip
             "rafamadriz/friendly-snippets" , -- 事前定義されたスニペットコレクション
             "hrsh7th/cmp-nvim-lsp", -- LSP補完
             "onsails/lspkind-nvim", -- アイコン表示

onsails/lspkind-nvim を加える。ここではhrsh7th/cmp-pathも加えている。

        config = function()
            -- nvim-cmp の設定
            local cmp = require("cmp") -- nvim-cmp のメインモジュールをロード
            local luasnip = require("luasnip") -- LuaSnip のモジュールをロード
            require("luasnip/loaders/from_vscode").lazy_load() -- VSCode スタイルのスニペットをロード
            local lspkind = require("lspkind")

            cmp.setup({
                mapping = cmp.mapping.preset.insert({
                    ["<C-b>"] = cmp.mapping.scroll_docs(-4), -- 補完候補のドキュメントを上にスクロール
                    ["<C-f>"] = cmp.mapping.scroll_docs(4), -- 補完候補のドキュメントを下にスクロール
                    ["<C-e>"] = cmp.mapping.abort(), -- 補完を中断して閉じる
                    ["<CR>"] = cmp.mapping.confirm({ select = true }), -- 補完確定 (現在選択中の候補を使用)
                }),
                sources = cmp.config.sources({
                    { name = "luasnip" },
                    { name = "nvim_lsp" },
                }, {
                    { name = "buffer" }, -- バッファの内容を補完候補に含める
                    { name = "path" },
                }),
                formatting = {
                    format = lspkind.cmp_format({
                        mode = "symbol_text",
                        maxwidth = 50,
                    }),
                },
            })
        end,

大分ガラっと変わってきた。

                formatting = {
                    format = lspkind.cmp_format({
                        mode = "symbol_text",
                        maxwidth = 50,
                        menu = {
                            buffer = "[Buffer]",
                            nvim_lsp = "[LSP]",
                            path = "[Path]",
                            vsnip = "[Snippet]",
                        },
                    }),
                },

とすると

もうちょいわかりやすくなる

Discussion