Vim
Lua
vimscript
neovim
39
どのような問題がありますか?

この記事は最終更新日から1年以上が経過しています。

Organization

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

0. 前置き

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

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

image.png

(クソコラみたいな画像ですね……)

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

なお、この記事では Neovim の設定を Lua で書く方法について説明しません。下記のガイドが非常によく纏まっておりますので、困ったらこちらを読んでください。
Getting started using Lua in Neovim(日本語版)

1. そもそも packer.nvim とは

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

1.1 ほぼ全て Lua で書かれてる。

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

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

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

  • start ディレクトリにプラグインを置いておくと、起動時に読み込んでくれる。
  • opt ディレクトリに置いたプラグインは起動後に packadd コマンドで遅延読み込みできる。

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

  • start, opt 双方のディレクトリに対するプラグインのインストール・更新・削除をコマンド一つで実行できる。
  • 様々な契機で遅延読み込みするタイミングを記述できる。
  • プラグインごとの設定を(Lua で)記述でき、「コンパイル」することで Neovim 起動時に高速に読み込むことができる。

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

2. packer.nvim の導入方法

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

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

brew install neovim --HEAD

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

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

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

# 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
init.lua
require'plugins'

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

  • ~/.config/nvim/lua ディレクトリなど、&runtimepath + /lua ディレクトリに置かれたファイルは require'hogehoge' という単純な書式で読み込むことができます。
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 が読み込まれる前に、自動的に packadd されます。
  use{
    'tyru/capture.vim',
    requires = {
      {'thinca/vim-prettyprint', cmd = {'PP', 'PrettyPrint'}},
    },
    cmd = {'Capture'},
  }
end)

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

:PackerInstall

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

:PackerCompile

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

init.lua
vim.cmd[[autocmd BufWritePost plugins.lua PackerCompile]]

plugin.luainit.lua と分けて記述したのはこれが理由です。

2.3 コマンド一覧

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

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

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

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

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

3.1 Vim パッケージの仕組み

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

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

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

3.2 config オプションの使い所

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

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

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

3.3 setup オプションの使い所

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

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 オプション双方を使った例

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

configオプションとsetupオプション双方を使った例
use{
  'lambdalisue/vim-gista',
  cmd = {'Gista'},
  setup = function()
    vim.nvim_set_keymap('n', 'gl', '<Cmd>Gista list<CR>', {noremap = true})
  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 も必要です。

4. さらに複雑な使い方

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

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

これは Vim パッケージ自身の仕様による注意点です(ドキュメントにも書いてあります)。多くの Filetype プラグインは ftdetect スクリプトを使って Neovim の知らないファイル形式を認識してくれるのですが、そのためにはそのファイルを読み込む前に ftdetect スクリプトがロードされている必要があります。

-- このプラグインは読み込まれることはない
use{'aklt/plantuml-syntax', ft = {'plantuml'}}

この場合は素直に start プラグインとして起動時に読み込むか、

use'aklt/plantuml-syntax'

あまり推奨される書き方ではないですが、ftdetect スクリプトの中身自体を setup オプションに書いても良いでしょう。

use{
  'aklt/plantuml-syntax',
  ft = {'plantuml'},
  setup = function()
    vim.cmd[[autocmd BufRead,BufNewFile * if !did_filetype() && getline(1) =~# '@startuml\>'| setfiletype plantuml | endif]]
    vim.cmd[[autocmd BufRead,BufNewFile *.pu,*.uml,*.plantuml,*.puml,*.iuml set filetype=plantuml]]
  end,
}

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

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

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

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

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

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

init.lua
packadd packer.nvim

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

  ...
end)

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

init.lua
vim.cmd[[command! PackerInstall packadd packer.nvim | lua require'packers'.install()]]
vim.cmd[[command! PackerUpdate packadd packer.nvim | lua require'packers'.update()]]
vim.cmd[[command! PackerSync packadd packer.nvim | lua require'packers'.sync()]]
vim.cmd[[command! PackerClean packadd packer.nvim | lua require'packers'.clean()]]
vim.cmd[[command! PackerCompile packadd packer.nvim | lua require'packers'.compile()]]
~/.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 からいただきました。

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

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

packer.init{
  display = {
    open_fn = require'packer.util'.float,
  },
}

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

packersync.gif

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

5. dein.vim との比較

dein.vim は Shougo さん作の大変高機能なパッケージマネージャです。これと packer.nvim を比べるのは少々酷なのですが、利用者視点からはやっぱり気になるところです。

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

まあこれいうと身も蓋もないし packer.nvim だけの問題ではないんですが……。少なくとも現状では、Lua 製プラグイン全てに当てはまる問題ではあります。Lua は文法も単純で十分枯れている言語なので習得するのはそんなに難しくはないはずです。Neovim の設定を Lua で書く際に困る諸々は別の記事にまとめようと思います。

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

dein.vim がスピード重視のプラグインマネージャなので仕方ないところですが、packer.nvim 管理に切り替えると Neovim の起動速度が激遅になりました。なぜこのようなことになるのかというと、packer.nvim(というか Vim パッケージ)は読み込むプラグインを単純に runtimepath に羅列するだけのため、スクリプトを探索するときに時間がかかってしまうのです。

一方 dein.vim はこれを解決するため、起動時に読み込まれる全てのプラグインのスクリプトを、一つのディレクトリに再配置しています。Vim パッケージにはこのように複雑な機能は追加されないでしょうから、packer.nvim が dein.vim 並の速度になることは今後もないでしょう。

5.3 ftplugin の管理ができない

これはおまけ的な機能なのですが、dein.vim では ftplugin をも 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 ファイルにまとめて、

~/.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
      error'cannot detect filetype from <sfile>'
    elseif not M[ft] then
      error('unknown filetype: '..ft..' in ftplugins.lua')
    end
    M[ft]()
  end,
}

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

~/.config/nvim/ftplugin/plantuml.vim
lua require'ftplugins'.run()

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

6. まとめ

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

  • Neovim の設定を Lua で書きたい!
    • 書いたからどうだってもんでもないんですが、書きたいんだからしょうがないです。
  • Vim パッケージを使ってシンプルにプラグインを管理したいけど、インストールや更新は自動化したい。

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

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

  • Neovim と Vim で同じ設定ファイルを使いたい。
  • とにかく速いプラグインマネージャがいい。

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

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
ユーザー登録ログイン
dena_coltd
We delight people beyond their wildest dreams.

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
39
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー