🐼

Neovimでlazy loadするプラグインのヘルプを読めるようにする

2024/12/06に公開

tl;dr

遅延読み込みしているプラグインのヘルプは:helpが引けないという苦しみを、自分なりの方法で解消してみました。

https://github.com/kyoh86/dotfiles/commit/01a6514079c444e20e7f6220483bda4b1096c912

背景

プラグインと遅延読み込み

Vim/Neovimにはプラグインが色々とありますが、プラグインが増えるほどにVim/Neovim自体の起動時間も増えてしまいます。
そこで各種のプラグインマネージャは遅延読み込み(Lazy loading)という仕組みを用意しています。
プラグインが読み込まれるタイミングを任意の「プラグインが必要になったタイミング」に遅延させることで、起動時間を節約しようという企みです。

遅延読み込みとヘルプ

しかし、遅延読み込みにおいては、共通しがちな苦しみとして

未だ読み込まれていないプラグインの:helpが引けない

という問題があります。

プラグインマネージャーはVim/Neovimの機構からは外にあるものですから、Vim/Neovimの標準機能である:helpが引けないのも無理からぬ事です。
プラグインマネージャーがそこまで面倒を見てくれるケースもありますが、そう多くはありません。

例えば、lazy.nvimなどは起票されたIssueがNot plannedとしてCloseされています

「:helpが引ける」状態とは

ところで、:help foobarと引いたとき、特定のヘルプが表示されるのはどういう仕組みなのでしょうか。
ここではあくまでも雑な誤り含みの解説にとどめますが、概ね次のような仕組みになっています。

  • 'runtimepath'に設定されたパスの一覧を走査する
  • 各パスのdocディレクトリからtagsファイルを探す
  • tagsファイルの中からキーワードfoobarに相当するファイルパスを探す
  • 見つかったファイルパスを開く

そしてdocディレクトリの中のtagsファイルは、同梱されたヘルプドキュメント(*.txtファイル)から:helptagsコマンド[1]によって生成されています。
多くのプラグインマネージャは、インストールや更新の後で:helptagsコマンドを呼んでtagsファイルを生成しています。

tagsファイルとは

tagsファイル自体は、(ヘルプに限らず)Vimのキーワードでジャンプする機能を支えるものです。[2]
またも非常に雑な誤り含みに説明すると、次のようなテキストファイルになっています。

<対象のキーワード><TAB><ジャンプ先のファイル名><TAB><ファイル内でジャンプする方法>

たとえば、:helptags自体のタグは次のように記録されています。

:helptags	helphelp.txt	/*:helptags*
  • このファイルはキーワードの昇順に並んでいる必要がある
  • ファイル名には多言語対応のため、Language Codeが末尾につく場合がある(例:tags-ja

点には留意が必要です。[2:1]

解決

ここまでの背景を踏まえると、要は遅延読み込みのプラグインも含めて、tagsファイルをすべて読み込めれば、すべてのヘルプを引くことができるようになることが分かります。
そしてtagsファイルは単なるテキストファイルですから、読み込まれる場所にマージしてしまえば良いのです。

解決全体は私の設定を見ていただくとして、ここでは重要な要素のみピックアップして解説します。

https://github.com/kyoh86/dotfiles/commit/01a6514079c444e20e7f6220483bda4b1096c912#diff-01bc5a3aea598b518f9e5d9c3c379d427495120255203731bfdf3a8b5ddbfd2cR14

登録されたプラグインのディレクトリを列挙する

遅延読み込み対象か否かに関わらず、すべてのプラグインを列挙します。
lazy.nvimの場合はrequire("lazy.core.config").pluginsで列挙できます。
各エントリのdirプロパティにはインストール先のディレクトリパスが入っています。

local plugins = require("lazy.core.config").plugins
for _, p in pairs(plugins) do
    local dir = vim.fs.joinpath(p.dir, "doc")
    ...
end

tagsファイルをマージする

tagsファイルを開いて、2番目の列(ファイル名)を絶対パスに変換していきます。
(ここでは後の処理のためにバッファへ書き込んでいます)

local function is_tags_file(source_name, source_type)
  if source_type ~= "file" then
    return false
  end
  if source_name == "tags" then
    return true
  end
  if string.find(source_name, "^tags-.+$") then
    return true
  end
  return false
end

...

for fname in vim.iter(vim.fs.dir(dir)):filter(is_tags_file) do
    local words = vim.split(line, "\t")
    words[2] = vim.fs.joinpath(dir, words[2])
    vim.api.nvim_buf_set_lines(buf, 0, 0, true, { vim.fn.join(words, "\t") })
end

sortして保存する

並び順を:sortコマンドで昇順にしたうえで、Neovim設定ディレクトリのafter/doc配下に保存します。
after'runtimepath'に登録されているため[3]、これでtagsファイルとして読み込まれます。

local after_docs_dir = vim.fs.joinpath(vim.fn.stdpath("config") --[[@as string]], "after", "doc")
...
vim.api.nvim_buf_call(buf, function()
    vim.cmd.sort("u")
    vim.cmd.write({ args = { vim.fs.joinpath(after_docs_dir, name) }, bang = true })
end)

とりまとめ処理を呼び出す

これらの処理を、プラグインのインストールや更新が行われた後で呼び出します。
lazy.nvimの場合はLazyUpdateLazyInstallユーザーイベントが相当します。

vim.api.nvim_create_autocmd("User", {
    group = vim.api.nvim_create_augroup("kyoh86-lazy-help-doc", { clear = true }),
    pattern = { "LazyInstall", "LazyUpdate" },
    callback = require("kyoh86.lib.lazy_help").collect,
})

まとめ

私のこれに関する設定全体を再掲しておきます。

https://github.com/kyoh86/dotfiles/commit/01a6514079c444e20e7f6220483bda4b1096c912#diff-01bc5a3aea598b518f9e5d9c3c379d427495120255203731bfdf3a8b5ddbfd2cR14

みなさんの環境や設定に応じて、各要素が参考になれば幸いです。

遅延読み込みと:helpの機能性を両立したい方は、是非試してみてください。

脚注
  1. :helptags - Neovim docs ↩︎

  2. Tagsrch - Neovim docs ↩︎ ↩︎

  3. 'runtimepath' - Neovim docs ↩︎

Discussion