lazy.nvim で “dependencies” を賢く使う

:::note info

これは [Vim 駅伝](https://vim-jp.org/ekiden/) 2024/9/9 の記事です。前回は atusy さんの [zfをline-wise化して直感的な挙動にするマッピング | Atusy's blog](https://blog.atusy.net/2024/09/06/linewise-zf/) でした。

:::

:::note warn

当初この記事は「lazy.nvim で dependencies を使うのはやめよう」という題名で公開していたのですが、[英語版](https://dev.to/delphinus35/dont-use-dependencies-in-lazynvim-4bk0)の公開時に海外掲示板で内容に[突っ込みを受け](https://www.reddit.com/r/neovim/comments/1fhavpk/dont_use_dependencies_in_lazynvim/)ました。そのためタイトルの変更と追記を修正・追記を行っています。この注記と同じ、「！」記号が付いた橙色のものが修正・追記した部分です。

:::

[lazy.nvim](https://lazy.folke.io) は Neovim のプラグインマネージャです。おそらくこのジャンルではシェア No.1 の定番プラグインでしょう。Neovim の代表的な distribution である [LazyVim](http://www.lazyvim.org) や [NvChad](https://nvchad.com) などでも利用されていますので、知らない内に使っている人も多いはずです。今日はそんな lazy.nvim の小ネタ。

## 1. lazy.nvim でプラグインの遅延読み込みをする

Neovim のプラグインは、基本的に Neovim 起動時に読み込むものです。しかしそのままでは、プラグインを多数インストールするとどんどん起動が遅くなってしまいます。そのため、「遅延読み込み」（lazy loading）という手法が発明されました。lazy.nvim はその名前の通り、遅延読み込みに関する機能を多く持っています。

例えば、同じく著名なプラグインである [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)。こちらの README では、lazy.nvim で利用する場合以下のように記述するよう指定しています。

```lua
-- 余計な記述は省いています（以下同様）
{
  "nvim-telescope/telescope.nvim",
  dependencies = { "nvim-lua/plenary.nvim" },
}
```

これはどういうことかと言うと、telescope.nvim を GitHub からインストールするように設定（`"nvim-telescope/telescope.nvim"`）し、依存関係として `"nvim-lua/plenary.nvim"` を指定しています。この書き方では遅延読み込みをせず、Neovim 起動時にプラグインが読み込まれます。

遅延読み込みをする場合、どのような契機で読み込むのか指定する必要があります。その方法はいくつかあるのですが、例えば以下のようにしてみます。

```lua
{
  "nvim-telescope/telescope.nvim",
  cmd = { "Telescope" },
  dependencies = { "nvim-lua/plenary.nvim" },
}
```

この方法だと、Neovim 起動時には読み込まず、初めて `:Telescope find_files` などの `:Telescope` コマンドを実行した時、プラグインが読み込まれます。

## 2. 遅延読み込みした時、dependencies はいつ読み込まれる？

上記の場合、telescope.nvim は `:Telescope` コマンドを実行した時に読み込まれることが分かりました。では、それが依存している plenary.nvim はいつ読み込まれるのでしょうか？

> 答えは「Neovim の起動時」です。起動時には最低限のプラグインを読み込み、他は後で必要になった時に読み込むのが理想ですが、dependencies を使うとこれが上手く行きません。

:::note warn

上記の文には誤りがありました。正しくは以下の通りです。

> dependencies に指定したプラグインは Neovim 起動時には読み込まれません。依存元のプラグインが読み込まれる時、その前に読み込まれます。理想的には、実際に必要になった時に読み込まれて欲しいのですが、dependencies を使うとそうはなりません。

:::

plenary.nvim をも telescope.nvim と連動して読み込ませる場合、以下のように設定します。

```lua
{ "nvim-telescope/telescope.nvim", cmd = { "Telescope" } },
{ "nvim-lua/plenary.nvim", lazy = true },
```

`lazy = true` は、特に読み込み契機を設定せず必要になったら読み込む、という意味です。この「必要になったら」というのがどうやって判定されているのかと言うと、`require "plenary"` された瞬間、というのが答えです。

:::note info

* これは `require "plenary.log"` などの、より深い所にあるモジュールに対しても動作します。
* このモジュール読み込みの仕組みは、lazy.vim が Lua  の `package.loaders` をイジって実現しています。詳細な仕組みについては冗長なので省きます。

:::

つまり、Neovim を起動した後 `:Telescope find_files` すると以下のように動作する訳です。

1. `:Telescope find_files` する。
2. telescope.nvim が読み込まれる。
3. telescope.nvim のソースの中で `require "plenary.……"` している部分に到達。
4. plenary.nvim が読み込まれる。
5. 処理が続行され、`:Telescope find_files` の画面が開く。

## 3. dependencies、要らなくない？

ってなりますよね。だって、「必要になったら勝手に読み込んでくれる」のなら、依存関係を定義する意味が無い訳です。

```lua
{
  "nvim-telescope/telescope.nvim",
  cmd = { "Telescope" },
  dependencies = {
    { "nvim-lua/plenary.nvim", lazy = true },
  },
}
```

> じゃあ、↑こうしたらどうなるの、と思う訳ですが、**これは明確に誤り**です。`lazy = true` が指定してあるので必要になった（`require "plenary.……"` に到達した）時に読み込まれるように見えるんですが、実はこれ、telescope.nvim の前に読み込まれます。

:::note warn

これも「明確に誤り」とまでは言えないです。先述の通り、「実際に必要になった時」では無く、「依存元のプラグインが読み込まれる前」に読み込まれます。そのため、「理想的なタイミングでは無い」と修正します。

:::

1. `:Telescope find_files` する。
2. plenary.nvim が読み込まれる（dependencies なので）。
	- `lazy = true` のため、Neovim 起動時には読み込まれません。
3. telescope.nvim が読み込まれる。
4. 処理が続行され、`:Telescope find_files` の画面が開く。

となるんですね。あーややこしい。Neovim 起動時には読み込まれなくなっているのですが、「必要になった時に読み込む」には至っておりません。

:::note warn

以下の 4. 節は公式ドキュメント[このページ](https://lazy.folke.io/developers)について書いたものですが、これはプラグイン製作者向けに [package spec](https://lazy.folke.io/packages) の書き方を説明したページです。そのため、一般のユーザーの使い方にそのまま適用できるものではありません。

以上により、まとめてコメントアウトしています（上手く色を薄くすることができないので引用にしています）。

:::

> ## 4. 「dependencies を使うな」公式もそう言っている
> 
> 実はこのこと、裏技でもなんでもありません。lazy.nvim のドキュメントにちゃんと書いてあります。
> 
> > [🔥 Developers | lazy.nvim](https://lazy.folke.io/developers)
> >
> > Only use dependencies if a plugin needs the dep to be installed **AND** loaded. Lua plugins/libraries are automatically loaded when they are `require()`d, so they don't need to be in dependencies.
> >
> > （訳）dependencies は、プラグインがその対象の依存プラグインがインストールされ、**かつ**、読み込まれていないとダメな場合にのみ、使うようにしましょう。Lua で書かれたプラグインやライブラリは、それらが `require()` された際に自動で読み込まれます。なので、dependencies の中に書く必要は無いんです。
> 
> 「その対象の依存プラグインがインストールされ、**かつ**、読み込まれていないとダメな場合」というのが分かりにくいですね。これは例えば、`plugin` ディレクトリに初期化するスクリプトがあり、その初期化処理が依存元のプラグインの動作に必要な場合が該当します。
> 
> もっと分かりにくい？では言い換えましょう。
> 
> **最近の、Lua で書かれたプラグインでこれに該当するものはありません！dependencies はだから、使う必要はないんです！**

## 5. おまけ

言いたいことはもう言ったので、以下はおまけです。

### 5.1. じゃあなんで、README の例では dependencies が使われているの？

別に telescope.nvim に限らず、多くのプラグインの README には、依存しているプラグインに対して dependencies を使っている例が載っています。この理由には 3 つのパターンが考えられます。

#### 5.1.1. 問い合わせを減らしたい場合

多分これが大半です。そもそも遅延読み込みって難しいんです。「プラグインが読み込まれないんですけど！」という問い合わせ、大抵は遅延読み込みに起因するものだったりします。

README には「誰でも動く設定」を書くべきです。dependencies を使うことで、依存するプラグインを Neovim 起動時に読み込ませているんですね。

#### 5.1.2. Vim Script で書かれたプラグインの場合

`require` による自動読み込みができるのは Lua で書かれたプラグインのみです。Vim Script で書かれたプラグインには dependencies が効果的に利用できるでしょう。

:::note info

lazy.nvim は [denops.vim](https://vim-denops.github.io/denops-documentation/) で書かれたプラグインをサポートしていません。dependencies に [vim-denops/denops.vim](https://github.com/vim-denops/denops.vim) を記述するだけで上手く動くこともありますが、詳しい方法は別稿に譲ります。

:::

#### 5.1.3. 本当に必要な場合

上の方に書いた「初期化処理が依存元のプラグインの動作に必要な場合」ですね。でもこのパターンは本当に少ないです。

依存するプラグインに初期化処理が必要でも、例えば次のように書けます。

```lua
{ "foo/bar.nvim", cmd = { "Bar" } },
{ "hoge/fuga.nvim", lazy = true, opts = {} },
```

上記のような設定で、bar.nvim の中で `require "fuga"` しているコードがあるとしましょう。この場合は以下のような動作をします。

1. `:Bar` する。
2. bar.nvim が読み込まれる。
3. `require "fuga"` に到達する。
4. fuga.nvim が読み込まれる。
5. `require("fuga").setup {}` が実行される（opts オプションによる効果）。
6. `require "fuga"` が機能し、`:Bar` の処理が続行される。

fuga.nvim の初期化処理（`setup()`）はこの場合でもきちんと実行されるんです。もしそれでも dependencies が必要になるとしたら、bar.nvim のソースで `require "fuga"` **する前**に、`require("fuga").setup {}` しておく必要がある場合に限られます。そんな依存関係、普通は作らないでしょう。

### 5.2. telescope.nvim の extension（拡張機能）を遅延読み込みする

telescope.nvim を使う人は多くがその extension をも使っているでしょう。例えば [telescope-project.nvim](https://github.com/nvim-telescope/telescope-project.nvim) を使う場合、以下のように設定することができます。

```lua
{ "nvim-telescope/telescope-project.nvim", lazy = true },
{ "nvim-lua/plenary.nvim", lazy = true },

{
  "nvim-telescope/telescope.nvim",
  cmd = { "Telescope" },
  opts = {
    extensions = {
      project = { theme = "dropdown" },
    },
  },
},
```

この場合は以下のように動作します。

1. `:Telescope project` する。
2. telescope.nvim が読み込まれる。
3. `require("telescope").setup { extensions = { …… } }` が実行される。
4. 「project」という picker が読み込まれていないため、`require("telescope").load_extension "project"` が[実行される](https://github.com/nvim-telescope/telescope.nvim/blob/5972437de807c3bc101565175da66a1aa4f8707a/lua/telescope/command.lua#L192-L201)。
5. `require "telescope._extensions.project"` が[実行される](https://github.com/nvim-telescope/telescope.nvim/blob/5972437de807c3bc101565175da66a1aa4f8707a/lua/telescope/_extensions/init.lua#L7-L13)。
6. telescope-project.nvim が読み込まれる。
7. telescope-project.nvim の初期化（`require("telescope._extensions.project").setup { theme = "dropdown" }`）が[実行される](https://github.com/nvim-telescope/telescope.nvim/blob/5972437de807c3bc101565175da66a1aa4f8707a/lua/telescope/_extensions/init.lua#L19-L21)。
8. `:Telescope project` の処理が続行される。

これはムズい！！！README に dependencies を書きたくなる気持ちも分かる！

## 6. 終わりに

いやあ、遅延読み込みって本当に奥が深いですね！