packer.nvim で Neovim + Lua のビッグウェーブに乗る

> 2022/12/3 本日公開の [爆速で起動する Neovim を packer.nvim で作る - Qiita][新記事] に合わせ、最新の情報を含めて修正しました。

[新記事]: https://qiita.com/delphinus/private/fb905e452b2de72f1a0f

# 0. 前置き

昔々、NeoBundle から [dein.vim][] に乗り換える話を書きました。

[dein.vim]: https://github.com/Shougo/dein.vim

* [NeoBundle から dein.vim に乗り換えたら爆速だった話 - Qiita](https://qiita.com/delphinus/items/00ff2c0ba972c6e41542)

しかし 5 年も経てば世の中色々変わります。Neovim も色々変わりましたが、最近一番ホットな話題といえばなんと言っても **Lua** でしょう。プラグインを書くための言語としてだけではなく、設定ファイルである `init.vim` すら Lua で書くことが可能になったのです。

* [[RFC] add init.lua as an alternative user config by dm1try · Pull Request #12235 · neovim/neovim](https://github.com/neovim/neovim/pull/12235)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8880/d3cbcf4d-5d56-a31f-536b-3c4a1c8675a6.png)

（クソコラみたいな画像ですね……）

このビッグウェーブに乗るべく、今までプラグインマネージャーとして使ってきた dein.vim から、[packer.nvim][] に乗り換えてみました。packer.nvim は目下 Neovim Lua 界で一番ポピュラーなプラグインマネージャです。その設定方法から、ディープな使い方、前回紹介した dein.vim との比較まで書いて行きます。

[packer.nvim]: https://github.com/wbthomason/packer.nvim

> なお、この記事では Neovim の設定を Lua で書く方法について説明しません。下記のガイドが非常によく纏まっておりますので、困ったらこちらを読んでください。
> [Getting started using Lua in Neovim（日本語版）](https://github.com/willelz/nvim-lua-guide-ja/blob/master/README.ja.md)

# 1. そもそも packer.nvim とは

[packer.nvim][] は以下のような点を特徴としています。

## 1.1 全て Lua で書かれてる。

そのため……というわけでもありませんが、packer.nvim 自体の設定も Lua で書く必要があります。とはいえ最低限の設定ならプラグイン名を羅列するだけです。

## 1.2 Vim8 / Neovim 標準のプラグイン機能を利用している。

Vim8 / Neovim には標準でプラグイン管理機能（以下では「[Vim パッケージ][]」と呼びます）が付属しています。ただこれ自体は非常に原始的で、以下のような機能しかありません。

[Vim パッケージ]: https://vim-jp.org/vimdoc-ja/repeat.html#packages

* `start` ディレクトリに置いたプラグイン（以下 start プラグインと呼びます）を置いておくと、起動時に読み込んでくれる。
* `opt` ディレクトリに置いたプラグイン（以下 opt プラグインと呼びます）は起動後に `packadd` コマンドで遅延読み込みできる。

packer.nvim はこれに以下のような機能を追加してプラグイン管理を助けてくれます。

* プラグインのインストール・更新・削除をコマンド一つで実行できる。
* opt プラグインを様々な契機で遅延読み込みするタイミングを記述できる。
* プラグインごとの設定を（Lua で）記述でき、「コンパイル」することで Neovim 起動時に高速に読み込むことができる。

どちらかというとシンプルで最小限の機能だけを実装してあり、とにかく多機能でパワフルな [dein.vim][] と比べると見劣りする点もあります。それは追々述べていくことにしましょう。

# 2. packer.nvim の導入方法

## 2.1 最新版の Neovim をインストールする

これは [packer.nvim][] に限らないことですが、Lua 製プラグインは Neovim の HEAD（最新開発版）を前提としていることが多いです。[リリースページ][]からバイナリをインストールするか、Homebrew などを使ってコンパイルしましょう。

[リリースページ]: https://github.com/neovim/neovim/releases

```sh
brew install neovim --HEAD

# 更新時はこれだけで OK
brew reinstall neovim
```

## 2.2 packer.nvim のインストールと最低限の設定

[README][] 読んで貰えば一目瞭然なのですが一応書いておきます。

[README]: https://github.com/wbthomason/packer.nvim/blob/master/README.md

```sh
# opt ディレクトリに packer.nvim をクローン
git clone https://github.com/wbthomason/packer.nvim \
  ~/.local/share/nvim/site/pack/packer/opt/packer.nvim

# 設定ファイルに追記
nvim ~/.config/nvim/init.lua
```

```lua:init.lua
require "plugins"
```

packer.nvim の設定は `init.lua` に直接書くこともできますが、後述の理由から別ファイルに分けることをお勧めします。上記のように書いておくと、`~/.config/nvim/lua/plugins.lua` を読み込んでくれます。

* `~/.config/nvim/lua` ディレクトリなど、`&runtimepath` + `/lua` ディレクトリに置かれたファイルは `require "hogehoge"` という単純な書式で読み込むことができます。


```lua:lua/plugins.lua
vim.cmd.packadd "packer.nvim"

require("packer").startup(function()
  -- 起動時に読み込むプラグインは名前を書くだけです
  use "tpope/vim-fugitive"
  use "tpope/vim-repeat"

  -- opt オプションを付けると遅延読み込みになります。
  -- この場合は opt だけで読み込む契機を指定していないため、
  -- packadd コマンドを叩かない限り読み込まれることはありません。
  use { "wbthomason/packer.nvim", opt = true }
  -- packer.nvim 自体を遅延読み込みにする理由はまた後ほど。

  -- コマンドを叩いたときに読み込む。
  use { "rhysd/git-messenger.vim", opt = true, cmd = { "GitMessenger" } }

  -- 特定のイベントで読み込む
  use { "tpope/vim-unimpaired", opt = true, event = { "FocusLost", "CursorHold" } }

  -- 特定のファイルタイプのファイルを開いたら読み込む
  use { "fatih/vim-go", opt = true, ft = { "go" } }

  -- 特定のキーを叩いたら読み込む
  -- この例ではノーマルモードの <CR> にマッピングしていますが、
  -- モードを指定する場合はテーブルを入れ子にします。
  -- keys = {
  --   { "n", "<CR>" },
  --   { "v", "<CR>" },
  -- }
  use {
    "arecarn/vim-fold-cycle",
    opt = true,
    keys = { "<CR>" },
  }

  -- 特定の VimL 関数を呼ぶと読み込む
  -- この例だと、任意の場所で Artify("hoge", "bold") のように呼び出された時に、
  -- このプラグインも読み込まれます。
  use { "sainnhe/artify.vim", opt = true, fn = { "Artify" } }

  -- 実は opt = true は省略できます。読み込む契機（この例では cmd）を指定すると、
  -- 自動的に遅延読み込みとみなされます。
  use {
    "npxbr/glow.nvim",
    cmd = { "Glow", "GlowInstall" },
    -- run オプションを指定すると、インストール時・更新時に
    -- 実行するコマンドを指定できます。
    run = [[:GlowInstall]],
    -- 先頭に : がついていないなら bash -c "..." で実行されます。
    -- run = [[npm install]],
    -- 関数も指定可能です。
    -- run = function()
    --   vim.cmd.GlowInstall()
    -- end,
  }

  -- 条件が真の時のみ読み込みます。条件は起動時に毎回判定されます。
  use {
    "thinca/vim-fontzoom",
    cond = [[vim.fn.has"gui" == 1]], -- GUI の時のみ読み込む。
    -- 関数も指定できます。
    -- conf = function()
    --   return vim.fn.has "gui" == 1
    -- end,
  }

  -- 依存関係も管理できます。vim-prettyprint は capture.vim に依存しています。
  use {
    "tyru/capture.vim",
    requires = {
      { "thinca/vim-prettyprint" },
    },
  }
end)
```

基本的な使い方を羅列してみました。この時点で Neovim を起動しても、特に何も表示は変わりません。設定ファイルを書き換えた後は以下のコマンドを叩いてプラグインをインストールします。

```vim
:PackerInstall
```

さらに、設定ファイルを「コンパイル」します。`PackerCompile` コマンドにより、`~/.config/nvim/plugin/packer_compiled.vim` が生成されます。

```vim
:PackerCompile
```

これは設定ファイルを更新するたびに叩く必要がありますので、ついでに `autocmd` を設定しておきましょう。

```lua:init.lua
vim.api.nvim_create_autocmd("BufWritePost", {
  pattern = { "plugins.lua" },
  command = "PackerCompile",
})
```

`plugins.lua` を `init.lua` と分けて記述したのはこれが理由です。

## 2.3 コマンド一覧

packer.nvim をインストールすると以下のコマンドが使えるようになります。

<dl>
  <dt><code>PackerInstall</code></dt>
  <dd>プラグインをインストールする。</dd>
  <dt><code>PackerUpdate</code></dt>
  <dd>追加されたプラグインをインストールし、既存のものは更新する。</dd>
  <dt><code>PackerClean</code></dt>
  <dd>必要なくなったプラグインを削除する。</dd>
  <dt><code>PackerSync</code></dt>
  <dd><code>PackerClean</code> の後に <code>PackerUpdate</code> を行う。</dd>
  <dt><code>PackerCompile</code></dt>
  <dd>設定ファイルを「コンパイル」する。</dd>
</dl>

簡単ですね。一度設定した後は `PackerSync` を定期的に叩くだけでプラグインは最新のものになります。

# 3. プラグインの設定を書く

多くのプラグインは様々な設定をカスタマイズして利用します。このカスタマイズスクリプトを書く方法が 2 つ（`config` と `setup`）あり、これらを使い分けるには [packer.nvim][] の元になっている **Vim パッケージ**について知る必要があります。

## 3.1 Vim パッケージの仕組み

Neovim 起動時に Vim パッケージによって管理されたプラグインやスクリプトは以下の順序で読み込まれます（完全なリストは[こちら][]）。Vim パッケージは Vim8 / Neovim 組み込みの機能ですので、この順序は packer.nvim とは直接関係ありません。

[こちら]: https://neovim.io/doc/user/starting.html#startup

1. `init.lua` とそれが `require` するもの。
2. start プラグイン。
3. その他、`&runtimepath` 内のプラグインスクリプト。
   * この中には `PackerCompile` によって生成される `~/.config/nvim/plugin/packer_compiled.vim` を含みます。
4. opt プラグインのうち、`setup` オプションのみが指定されたもの（後述）。
   * ここだけは packer.nvim による拡張です。
5. `VimEnter` イベントによって読み込まれるもの。
   * ここで Neovim の起動完了です。
6. その後、`cmd` や `fn` その他の設定によって `packadd` コマンドされる `opt` プラグインたち。

キモは _3._ の Vim スクリプトです。この中では各プラグインの設定を行なっているのですが、この内容を記述するのが `config`, `setup` オプションです。

## 3.2 `config` オプションの使い所

プラグインを読み込んだ**後に**設定を適用するには `config` オプションを使います。

```lua:configオプションを使った例
use {
  "neovim/nvim-lspconfig",
  config = function()
    require("lspconfig").tsserver.setup {}
  end,
},
```

この例では `nvim-lspconfig` が `start` プラグインとして、つまり、Neovim 起動時に読み込まれています。`require "lspconfig"` する必要があるため、プラグインの設定は `config` オプションを使ってプラグインの読み込み後に行います。

## 3.3 `setup` オプションの使い所

プラグインを読み込む**前に**設定を適用するには `setup` オプションを使います。

```lua:setupオプションを使った例
use {
  "dhruvasagar/vim-table-mode",
  setup = function()
    vim.g.table_mode_corner = "|"
  end,
}
```

`vim-table-mode` プラグインは読み込まれた時にグローバル変数（`g:table_mode_corner` など）を見て動作を変えます。このような場合 `config` オプションではすでに遅いので `setup` オプションを使います。

注意しなくてはいけないのは、`setup` オプションを使う場合、そのプラグインは**必ず `opt` プラグインになる**（つまり、遅延読み込みになる）ということです。`start` プラグインは `packer_compiled.vim` より先に読み込まれるため、`config` オプションでは間に合わないのです。

ただし今回の場合のように `setup` オプションだけを指定した場合、packer.nvim はこれを特別扱いします。`opt` プラグインであるにも関わらず、`packer_compiled.vim` の後に自動的に読み込んでくれるのです。そのため、このままの設定でも起動後すぐにこのプラグインが使えます（例えば `:TableModeToggle` とか）。

## 3.4 `config` オプションと `setup` オプション双方を使った例

`config` と `setup` はもちろん同時に使えます。この場合はプラグインの読み込み前と後にスクリプトを実行することができます（もちろん `opt` プラグインになります）。

```lua:configオプションとsetupオプション双方を使った例
use {
  "lambdalisue/vim-gista",
  cmd = {"Gista"},
  setup = function()
    vim.keymap.set("n", "gl", "<Cmd>Gista list<CR>")
  end,
  config = function()
    vim.fn["gista#client#register"]("GHE", "https://github.example.com/api/v3")
  end,
}
```

[vim-gista][] は GitHub の gist を操作するためのプラグインです。`Gista` コマンドを叩くためのマッピング `gl` を定義しているのですが、このマッピングは当然プラグイン読み込み前に行う（つまり `setup` オプションを使う）必要があります。

加えて、vim-gista には GitHub Enterprise の特殊なドメイン（この場合は `github.example.com`）にアクセスする機能もあります。この設定はプラグインの読み込み後、`gista#client#register()` という関数を呼び出すことで行う必要があります。このために `config` も必要です。

[vim-gista]: https://github.com/lambdalisue/vim-gista

# 4. さらに複雑な使い方

今まで書いてきたことは公式の README にも大体載っています。ここからはそこにないディープな使い方について書いていきます。

## 4.1 Filetype プラグインは遅延読み込みしてはいけない

> 2022/12/3 この節は完全に古くなってしまいました。[新しく書いた記事][新記事]に書いている通り、**FileType プラグインは遅延読み込みできます**。FileType プラグインの扱いについても解説していますので詳しくはそちらを読んでください。

## 4.2 Colorscheme プラグインは opt プラグイン（遅延読み込み）に設定できる

これも Neovim のドキュメントに記載がありますが、`:colorscheme` コマンドが実行された際に検索されるディレクトリは `start`, `opt` 全てのプラグインが対象です。そのため、読み込まれるタイミングなどは気にせずに `opt = true` してしまって良いです。

```lua
-- 特に読み込みタイミングは指定しない
use { "arcticicestudio/nord-vim", opt = true }
```

この状態でも `:colorscheme nord` で Colorscheme が読み込まれます。

## 4.3 packer.nvim 自体を遅延読み込みする

今までの設定例では `init.lua` の冒頭（か、そこから `require` したファイル）で packer.nvim をロードし、それから各種設定を行なっていました。

```lua:init.lua
vim.cmd.packadd "packer.nvim"

require("packer").startup(function()
  use { "wbthomason/packer.nvim", opt = true }

  ...
end)
```

しかし packer.nvim はあくまで [Vim パッケージ][]のヘルパーでしかないため、プラグイン設定に変更のない限り読み込んでおく必要はありません。`:PackerHogehoge` コマンドを叩いた時だけ読み込んでくれればいいのです。

```lua:init.lua
vim.api.nvim_create_user_command("PackerInstall", [[packadd packer.nvim | lua require("packers").install()]], { bang = true })
vim.api.nvim_create_user_command("PackerUpdate", [[packadd packer.nvim | lua require("packers").update()]], { bang = true })
vim.api.nvim_create_user_command("PackerSync", [[packadd packer.nvim | lua require("packers").sync()]], { bang = true })
vim.api.nvim_create_user_command("PackerClean", [[packadd packer.nvim | lua require("packers").clean()]], { bang = true })
vim.api.nvim_create_user_command("PackerCompile", [[packadd packer.nvim | lua require("packers").compile()]], { bang = true })
```

```lua:~/.config/nvim/lua/packers.lua
local packer
local function init()
  if not packer then
    packer = require"packer"
    packer.init { ... }
  end
  packer.reset()

  packer.use{
    -- ここでいろんなプラグインを読み込む

    { "tpope/vim-fugitive" },
    { "vim-jp/vimdoc-ja" },
    {
      "dhruvasagar/vim-table-mode",
      setup = function()
        vim.g.table_mode_corner = "|"
      end,
    },

    ...
  }
end

return setmetatable({}, {
  __index = function(_, key)
    init()
    return packer[key]
  end,
})
```

このアイディアは packer.nvim 作者の wbthomason さん自身の dotfiles からいただきました。

* [dotfiles/plugins.lua · wbthomason/dotfiles](https://github.com/wbthomason/dotfiles/blob/063850b4957a55c065f795722163efc88ffb1b42/neovim/.config/nvim/lua/plugins.lua)

## 4.4 管理画面をカッコよくする

上記の例で出ていますが、packer.nvim ロード時に `packer.init` 関数を呼び出すことで細かな設定を行っています。どんなカスタマイズができるかは README の[こちら][customize]にあるのですが、この中で注目は `display.open_fn` です。ここに関数を設定することで、`:PackerSync` などの画面を floating windows で表示できたりします。

[customize]: https://github.com/wbthomason/packer.nvim#custom-initialization

```lua
packer.init {
  display = {
    open_fn = require("packer.util").float,
  },
}
```

Neovim メンテナの tjdevries さんは自身の [plenary.nvim][] を使ってもっと複雑なことを[してます][]。

[plenary.nvim]: https://github.com/nvim-lua/plenary.nvim
[してます]: https://github.com/tjdevries/config_manager/blob/0c89222a53baf997371de0ec1ca4056b834a4d62/xdg_config/nvim/lua/tj/plugins.lua#L331

![packersync.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8880/5e1b0160-3a05-1aba-56d4-800db68efa63.gif)

簡単なコードでいい感じにウィンドウがいい感じに装飾できています。こういう拡張が簡単にできるのが Lua のいいところです。

# 5. dein.vim との比較

[dein.vim][] は Shougo さん作の大変高機能なパッケージマネージャです。これと packer.nvim を比較すると次のようになるでしょう。

## 5.1 VimL と Lua 双方の知識が要る

dein.vim はそれ自体が Vimscript で書かれており、設定も Vimscript で行うことが前提となっています。そのため Vimscript の知識しか無くても利用を始められます。それに比べると [packer.nvim][] には Lua の知識が必須ですし、Vimscript で書かれたプラグインを使う際は Vimscript の知識も必要です。

:::note info

dein.vim も勿論 Lua で書かれたプラグインを管理することができ、そのための設定を Lua で記述する構文も用意されています。

:::

この記事を公開した頃（2021/2）ではこれが結構問題だったのですが、2022/12 現在ではだいぶ状況が変わりました。Neovim 自体の設定はほとんど全て Lua で書けるようになりましたし、そもそも Neovim 用に新規に作られるプラグインはほぼ全て Lua 製です。勢い、その設定のために必ず Lua を書く必要があります。古いプラグインのためには Vimscript の知識がまだまだ必要なのですが、そのうち Neovim を使うためには Lua を知るだけで問題無くなる日も来るでしょう。

## 5.2 Neovim の起動速度が遅くなる

これは packer.nvim の利用者にとっては頭の痛い問題で、この記事を公開した 2021/2 頃には顕著な差が見られた点でした。ですが 2022/12 現在では、特に Lua 製プラグインを多用する場合にはパフォーマンスの差はほとんど無いでしょう。dein.vim にはインストールするプラグインを解析して一つのディレクトリに纏めてくれる “merge” という機能があります。これが高速化に大変寄与しているのですが、遅延読み込みを駆使することで packer.nvim でもかなり高速に起動することができます。[新しい記事][新記事]に詳しいテクニックを書いていますので読んでみてください。

## 5.3 `ftplugin` の管理ができない

これはおまけ的な機能なのですが、dein.vim では ftplugin をも TOML に書いて管理できます。

```toml
# dein.vim の設定例
[[plugins]]
repo = "aklt/plantuml-syntax"
on_ft = ["toml"]

[plugins.ftplugin]
toml = """
  set noexpandtab
"""
```

ファイルタイプごとの設定をプラグインの側に置いて見通しよく管理できます。これは dein.vim が管理するディレクトリに `after/ftplugin/plantuml.vim` のようなファイルを作ることで実現されています。

packer.nvim にはこのような機能はありませんので、今まで書いていた設定をどうにかする必要があります。僕の場合は以下のように一つの Lua ファイルにまとめて、

```lua:~/.config/nvim/lua/ftplugins.lua
local M

M = {
  plantuml = function()
    vim.bo.expandtab = false
  end,

  perl = ...
  css = ...

  ...

  run = function()
    -- 読み込み元ファイル名からファイルタイプを推測する
    local ft = vim.fn.expand "<sfile>:t:r"
    if ft == "" then
      vim.notify("cannot detect filetype from <sfile>", vim.log.levels.WARN)
    elseif not M[ft] then
      vim.notify("unknown filetype: " .. ft .. " in ftplugins.lua", vim.log.levels.WARN)
    end
    M[ft]()
  end,
}

return M
```

あとはファイルタイプごとにスクリプトを設置しました。

```lua:~/.config/nvim/ftplugin/plantuml.vim
lua require("ftplugins").run()
```

他のファイルタイプもファイル名だけ変えて中身は全部一緒です。あとは `run()` 関数がよしなにやってくれます。

# 6. まとめ

まとめると、[packer.nvim][] は以下のような点に魅力を感じる人にとっては最適なプラグインマネージャと言えるでしょう。

* Neovim の設定を Lua で書きたい！
* [Vim パッケージ][]を使ってシンプルにプラグインを管理したいけど、インストールや更新は自動化したい。

> 今回の記事には間に合わなかったのですが、Luarocks を使って Lua のパッケージ管理を行う機能も[ついたようです][luarocks]。Lua で Neovim をどんどん拡張したい人にもお勧めですね。

[luarocks]: https://github.com/wbthomason/packer.nvim/pull/104

以下のような人には向きません。

* Neovim と Vim で同じ設定ファイルを使いたい。

5 年前の前回と比べると万人にお勧めできる感じではないのですが、Neovim + Lua のビッグウェーブに乗りたい人には是非 packer.nvim を試して欲しいですね。

> 2022/12/3 更に詳しく packer.nvim を使い倒すために記事を書いています。こちらも参照してください。
> * [爆速で起動する Neovim を packer.nvim で作る - Qiita][新記事]