2023/01/20

Neovimを完全にLuaLuaさせた

前回の記事の続編にして最終章の幕開けである。ついにinit.lua化は果たされ、主だったプラグインはどれもLua製に置き換わった。プラグイン総数が50前後しかないカジュアルユーザの僕でも丸一日かかったがやるだけの価値はあったと思いたい。

今や業務以外ではエディタをVim一本に絞りきれるところまで馴染んだ。主流のプラグインマネージャがNeoBundleの時代からVimに触れてきた割にはずいぶん手間を食ったものだ。「Vimはサブ武器です」と尻込みしていた頃とはうってかわり、Vimはもう僕のメイン武器となった。

init.lua化の実践

■シングルファイル
Web上の様々な設定例はたいていファイルが細かく分割されている。プラグインごとに独立したファイルを与えている事例もよく見るし、init.luaが数行しかないのも珍しくない。だいたいみんな5個くらいには分けているようだ。僕も以前はinit.vimとdein.vim、dein_lazy.vimの3つに分けていたが、編集を重ねていく過程で僕はこのメソッドにさほど嬉しみを感じていないことに気がついた。

もしかすると僕の知らない利点があるのかもしれないが、単に可読性の都合でしかないとしたら全部まとめても500行程度に収まる設定をあえてバラけさせる理由はなさそうだ。というわけでinit.lua化に合わせて全設定を一つのファイルに集約した。したがって、本エントリの記述例はすべてinit.luaに記されていると考えて構わない。

■記述のパターン化
init.lua化に役立つ知見はありとあらゆる場所に記されているとはいえ、手早く移行を完了させたいユーザにとっては迂遠な説明が多かったり、特殊な設定に文面を割いていたりしていまいち要領を得ないのが実情だ。そこで僕はいくつかのパターンを重点的に捉えることにした。

とりわけ重要なパターン例はVim scriptにおけるset xxx構文がinit.luaではvim.opt.xxx = booleanに置き換わっているところである。前者では設定した値がそのまま有効化されるが、後者の場合はboolean型で変数を入力する。つまり、trueなら有効でfalseなら無効化される。設定の大部分はこれだけ知っておけば書き換えられなくもない。

 1set hidden --Vim script
 2vim.opt.hidden = true --大半の設定はこのパターンで書ける。
 3
 4set helplang='ja', 'en' --Vim script
 5vim.opt.helplang = 'ja', 'en' --設定部分によっては値も変わる。
 6
 7set cmdheight=2 --Vim script
 8vim.opt.cmdheight = 2 --元の値が数値ならここも数値で指定する。
 9
10set signcolumn=yes --Vim script
11vim.opt.signcolumn = 'yes' --数値でもboolean型でもない設定も稀にある。

他にショートハンドのvim.o.xxxvim.bo.xxxなどもあるが、基本的にはvim.opt.xxxを使っておけば無用な誤りを減らせる。一方、数少ない例外への対処法としてはLuaっぽく直した構文をかたっぱしからGitHubの検索窓に叩き込む手法が非常に有効だった。ヒット件数が多ければ多いほど設定の確からしさを推測できる。

惜しむらくは資料の膨大さゆえ誰のどのコードを参考にしたのかまるで憶えておらず、技術文章にあるまじき不誠実な状態に陥ってしまったことだ。さしあたってはinit.luaを書いた全人類に感謝を捧げるという体裁でどうか容赦願いたい。

 1set mapleader='/<Space>' --Vim scriptの場合はエスケープ処理と文字コードが必要。
 2vim.g.mapleader = ' ' --この設定は構文が異なりエスケープ処理も文字コードも不要。
 3
 4set clipboard+=unnamedplus --Vim script
 5vim.opt.clipboard:append{'unnamedplus'} --特別な指定方法の一つ。
 6
 7set list listchars=tab-,trail:-,eol:↲,extends:»,precedes:«,nbsp:% --Vim script
 8vim.opt.listchars = {tab='»-', trail='-', eol='↲', extends='»', precedes='«', nbsp='%'} --特別な指定方法の一つ。
 9
10set shortmess+=I --Vim script
11vim.cmd('set shortmess+=I') --代替しうる構文が見つからない時はvim.cmdを利用してVim scriptで書く。
12
13set noundofile --Vim script
14vim.opt.undofile = false --init.luaに否定形の構文はないため、この場合は指定すべき真偽値が逆になることに注意されたし。

キーマッピングも同様のパターンで対応可能だ。たとえばnmap a bのようなマッピングをinit.luaはvim.keymap.set('n', 'a', 'b')の形で記す。vim.api.nvim_set_keymap()といった記述も有効だがこれは古い書き方である。このように既存の設定をパターン化して順次書き換えていき、必要に応じてGitHubの検索を活用すればいずれinit.lua化が完了するだろう。ただし、autocmdを多用している人はちょっと苦労しそうだ。

 1nnoremap <silent> sv :<C-u>vsplit<CR> --Vim script
 2vim.keymap.set('n', 'sv', ':<C-u>vsplit<CR>', {silent = true}) --<silent>などもboolean型で指定する。
 3
 4--やっている人が多そうなカーソル位置の保存設定をLua化したもの。
 5vim.api.nvim_create_autocmd({ 'BufReadPost' }, {
 6    pattern = { '*' },
 7    callback = function()
 8        vim.api.nvim_exec('silent! normal! g`"zv', false)
 9    end,
10})

ベテランユーザにとって以上の実践例は本質を無視した邪悪な所業かもしれないが、とにかく有効なinit.luaを仕立てる上ではこんな大雑把な理解でも一応差し支えはない。そのうちちゃんと:help lua-guideを読むから許してほしい。

lazy.nvim

init.lua化の次に取り掛かるのはプラグインマネージャの移行だ。今まで使っていたdein.vimにはなんの不満もないどころか、むしろ離れがたいとさえ感じているがLua化の欲望には抗えない。どうせやるからには真のルアラーを目指していきたい。まあ、真のルアラーは設定をコピペで書いたりはしないんだろうけど。

かつてLua製のプラグインマネージャといえばpacker.nvimの存在感が大きかった。僕もinit.lua化を果たした暁には半ば自動的にそれに移行するものと考えていたが、昨年の晩秋に突如現れたlazy.nvimの新進気鋭ぶりが凄まじく、せっかくならと新しい方を試すことにした。

使ってみると好評の理由はすぐに判った。俗な言い方をすればUIが強すぎる。プラグインの追加を検知すると再起動後にグラフィカルなインストール処理が自動で走り、アップデート時にはその概要までもが過不足のない洗練された画面で表示される。似たような機能は他のプラグインマネージャも持っているがポン付けでよしなにやってくれる点ではこちらが上回る。

極めつけは:Lazy profileのベンチマーク機能だ。nvim --startup抜きで即座に起動時間が把握できて、なおかつどのプラグインがどんな順序で読み込まれているのかも判る。伊達にlazyなどと銘打っていないだけはあり、遅延に関する機能はかなり豊富に見える。

遅延設定そのものも書きやすい。さすがにフルオートメーションとはいかず結局は手動で書かざるをえなかったがドキュメント類も賢くまとまっており、移行直前の億劫ささえ乗り越えればなんとかなった。これもいくつかのパターンを抑えれば簡単に遅延化を実現できる。以下に設定の一部を記す。

1{'windwp/nvim-autopairs', event = 'InsertEnter'}, --文字の挿入を伴うプラグインは'InsertEnter'を指定する。
2{'j-hui/fidget.nvim', event = 'LspAttach'}, --LSPと連動するプラグインは'LspAttach'を指定する。
3{'nvim-telescope/telescope.nvim', cmd = 'Telescope'}, --特定のコマンドを入力するまで不要なプラグインはcmd = 'cammand'で対応する。
4{'vim-jp/vimdoc-ja', ft = 'help'}, --特定のファイルタイプでのみ必要なプラグインはft = 'filetype'で対応する。
5{'lewis6991/gitsigns.nvim', event = 'BufNewFile, BufRead'}, --ファイルを読み込んだ後に装飾を加えるプラグインは'BufNewFile'と'BufRead'が有力。
6{'echasnovski/mini.surround', event = 'ModeChanged'}, --モードの切り替え時に発動させたいプラグインは'ModeChanged'が適切。
7{'nvim-lualine/lualine.nvim', event = 'VeryLazy'}, --他の設定でうまく動かなかったものは一律に'VeryLazy'で対処する。(VimEnter相当らしい)
8{'vim-denops/denops.vim', lazy = false}, --即時読み込んでくれないと不都合なプラグインは逆に遅延を無効化する。(config.default.lazy = falseの場合)

lazy.nvimの作者曰く、遅延設定を最強に極めると90以上のプラグインを抱えていても10ms台で立ち上がるらしい。僕の周りでもベテランユーザたちが次々と30ms台を達成している。残念ながら上記の中途半端なやり方では50msを切るか切らないかが精一杯だが、それでもデフォルト設定の2倍近く高速化できたので当面はこれで納得しておく。

nvim-cmp

補完プラグインも例によってLua製に置き換えた。僕は補完プラグインについてはShougoware一筋(neocomplete.vim→deoplete.nvim→ddc.vim)だったが、当初の懸念とは裏腹にこのnvim-cmpは使っていてなんの不満も感じない。Neovim界隈で事実上のデファクトスタンダートと認められているのも納得の完成度と言える。大衆人気を得るプラグインにありがちなおせっかいさも見られず、自分でキーマップやソースを設定しなければならないところも好印象だ。

たとえばLSPと連携するためのcmp-nvim-lsp、バッファ内のワードを拾うcmp-buffer、コマンドラインの入力を補完してくれるcmp-cmdlineなどが挙げられる。ソース群の種類はddc.vimに引けをとらず豊富で、なにがなくて困るというよりはトレンドを掴む方がかえって大変かもしれない。以下に僕の設定を示す。

 1local cmp = require('cmp')
 2local lspkind = require('lspkind')
 3
 4 cmp.setup({
 5   snippet = {
 6     expand = function(args)
 7       vim.fn['vsnip#anonymous'](args.body)
 8     end
 9   },
10
11   window = {
12      completion = cmp.config.window.bordered({
13        border = 'single'
14    }),
15      documentation = cmp.config.window.bordered({
16        border = 'single'
17    }),
18   },
19
20   mapping = cmp.mapping.preset.insert({
21     ['<Tab>'] = cmp.mapping.select_next_item(),
22     ['<S-Tab>'] = cmp.mapping.select_prev_item(),
23     ['<C-b>'] = cmp.mapping.scroll_docs(-4),
24     ['<C-f>'] = cmp.mapping.scroll_docs(4),
25     ['<C-Space>'] = cmp.mapping.complete(),
26     ['<C-e>'] = cmp.mapping.abort(),
27     ['<CR>'] = cmp.mapping.confirm({ select = true }),
28   }),
29
30 formatting = {
31   format = lspkind.cmp_format({
32     mode = 'symbol',
33     maxwidth = 50,
34     ellipsis_char = '...',
35   })
36  },
37
38 sources = cmp.config.sources({
39   { name = 'nvim_lsp' },
40   { name = 'vsnip' },
41   { name = 'nvim_lsp_signature_help' },
42   { name = 'calc' },
43  }, {
44   { name = 'buffer', keyword_length = 2 },
45  })
46 })
47
48 cmp.setup.cmdline({ '/', '?' }, {
49   mapping = cmp.mapping.preset.cmdline(),
50   sources = cmp.config.sources({
51  { name = 'nvim_lsp_document_symbol' }
52  }, {
53   { name = 'buffer' }
54  })
55 })
56
57 cmp.setup.cmdline(':', {
58 mapping = cmp.mapping.preset.cmdline(),
59 sources = cmp.config.sources({
60   { name = 'path' }
61  }, {
62   { name = 'cmdline', keyword_length = 2 }
63  })
64 })
65
66local capabilities = require('cmp_nvim_lsp').default_capabilities()
67vim.cmd('let g:vsnip_filetypes = {}')

もちろん、これらを適用するには任意のプラグインマネージャに導入したいソース群を予め列挙しておく必要がある。lazy.nvimでの記述例は以下の通りになる。

 1{'hrsh7th/nvim-cmp', event = 'InsertEnter, CmdlineEnter'},
 2{'hrsh7th/cmp-nvim-lsp', event = 'InsertEnter'}, 
 3{'hrsh7th/cmp-buffer', event = 'InsertEnter'},
 4{'hrsh7th/cmp-path', event = 'InsertEnter'},
 5{'hrsh7th/cmp-vsnip', event = 'InsertEnter'},
 6{'hrsh7th/cmp-cmdline', event = 'ModeChanged'}, --これだけは'ModeChanged'でなければまともに動かなかった。
 7{'hrsh7th/cmp-nvim-lsp-signature-help', event = 'InsertEnter'},
 8{'hrsh7th/cmp-nvim-lsp-document-symbol', event = 'InsertEnter'},
 9{'hrsh7th/cmp-calc', event = 'InsertEnter'},
10{'onsails/lspkind.nvim', event = 'InsertEnter'},
11{'hrsh7th/vim-vsnip', event = 'InsertEnter'},
12{'hrsh7th/vim-vsnip-integ', event = 'InsertEnter'},
13{'rafamadriz/friendly-snippets', event = 'InsertEnter'},

導入すると下記動画のような美しい補完ウインドウが姿を現す。すっかり手垢のついた言い回しになってしまうが、GUIのIDEに勝るとも劣らない上等な表現力だと思っている。

telescope-file-browser.nvim

昨今のファイラと言えばfern.vimがよく知られている。しかし今の僕には可能なかぎりNeovimをLuaLuaさせたいモチベが堆積しており、Telescopeをファイラに仕立てた本プラグインをチョイスした。当然ながら操作方法はTelescopeそのもので、あとはファイル管理に関わるショートカットキーさえ覚えればすでに手に馴染んだも同然である。

他のファジーファインダーを使っている人もこれを目当てにTelescopeに移行すべきかと訊かれたら「そこまでではない」と答えるが、突出した特長がない代わりに一通りの機能は揃っているためTelescopeユーザには一度試してもらいたいファイラだ。デフォルトでAltキーを占有しているところは若干気に入らないものの、その気になれば容易に変更できるのでtmuxやi3wmユーザでも不都合はないだろう。

おわりに

init.lua化、プラグインマネージャ、補完プラグイン、ファイラと立て続けに大がかりな移行を終えた現在、もはや完全にNeovimをLuaLuaさせたと言っても過言ではないはずだ。よって本シリーズはこれにて一旦の完結を見るが、今後もなにか面白い発見があれば随時紹介していきたい。もし読者の皆さんに「あまり知られていないが自分はこれがないと生きていけない」というようなプラグインがあったらぜひ教えていただきたい。

あわせて読ませたい

NeovimをさらにLuaLuaさせた
主にDAPとLSPの関連プラグインについて書いた待望の番外編。

©2011 Rikuoh Tsujitani | Fediverse | Bluesky | Keyoxide | RSS | 小説