✏️

Neovim プラグインを(ほぼ)全て Lua に移行した

2022/06/25に公開約8,200字

2022年の Neovim のトレンドとして、 Lua 製のプラグインへの移行があります。私も様子を見ながらだましだましやっていたのですが、やや仕事が落ち着いたので、2日ほど時間を取って全面的な移行をしました。

結果的に、600行近くあった設定ファイルを200行くらいまで減らすことができました。本記事の執筆時点の init.lua はこちらです。

https://github.com/acro5piano/dotfiles/blob/f88a4f5d80b09612e69a0817589f7ae9d87125aa/home/.config/nvim/init.lua

方針

過去の経験から、なるべくプラグインや設定を減らしたいと思っていました。理由としては、プラグインや設定が競合して誤作動を起こす状態を作りたくない、管理やバージョンアップが面倒、 init.vim の行数が増えることによるモチベーション低下などです。そのため、大前提として下記の方針を立てました。

  • 肥大化を防ぐため、なるべく Neovim の標準機能を使う
  • Lua 製のプラグインを優先して採用。なるべく Vim Script 製のプラグインは入れない
  • 自前の設定ファイルは Lua のみで完結させる
  • あったら便利そうだな〜という程度のプラグインや設定はなるべく入れない

Lua にこだわっているのは、 Lua 製のプラグインには下記のようなメリットがあるからです。

  • Neovim で動くことを主眼にしており、 Vim との互換性を無視しているものが多い
  • 積極的にメンテナンスされていることが多い
  • 動作速度が速い
  • 個人的なスキルとして VimScript より Lua の方が書きやすい・読みやすいので、いざとなったらどうにかできる感がある

導入したプラグイン、設定など

基本的な設定

Neovim 自体が色々と進化しているのか、今まで色々書いていた基本的な設定は下記で済みました。秘伝のタレ的に CP932 文字コードをサポートしていた部分が無くなった、などの影響もありそうです。

vim.g.mapleader = " "
vim.o.tabstop = 2
vim.o.shiftwidth = 2
vim.o.wrap = false
vim.o.signcolumn = "yes"
vim.o.ignorecase = true
vim.o.smartcase = true
vim.api.nvim_exec("highlight SignColumn ctermbg=black", false)

-- ファイルを開いた時に、カーソルの場所を復元する
vim.api.nvim_create_autocmd({ "BufReadPost" }, {
	pattern = { "*" },
	callback = function()
		vim.api.nvim_exec('silent! normal! g`"zv', false)
	end,
})

最後の vim.api.nvim_create_autocmd は比較的新しい API で、 autocmd を Lua のみで定義できます。

プラグイン管理

デファクトスタンダードの Packer を使っています。

require("packer").startup(function(use)
	use("wbthomason/packer.nvim")
	use("ibhagwan/fzf-lua")
	use("kyazdani42/nvim-web-devicons")
	use("jparise/vim-graphql")
	use("terrortylor/nvim-comment")
	use("nvim-lualine/lualine.nvim")
	use("bronson/vim-visual-star-search")
	use("lambdalisue/fern.vim")
	use("acro5piano/nvim-format-buffer")
	use("neovim/nvim-lspconfig")
	use("hrsh7th/nvim-cmp")
	use("hrsh7th/cmp-path")
	use("hrsh7th/cmp-buffer")
	use("hrsh7th/cmp-cmdline")
	use("hrsh7th/cmp-nvim-lsp")
	use("tpope/vim-surround")
	use("dcampos/nvim-snippy")
	use("dcampos/cmp-snippy")
end)

それぞれ見ていきます。

ファイラー

もともと NERDTree で最近 LuaTree に移行したところでした。が、 LuaTree の細かい挙動にイラッとすることが増えた( / が入ったファイル名を作ろうとした時に自動的にディレクトリ作成してくれない、 Hidden File からツリーを開いた時に自動的に C-I してくれない、など)のと、 IDE のように左側にツリー表示されているのはスペースの無駄な気がして、別の方法を試したいと思っていたところでした。

最初、 Vim/Neovim 標準の Netrw で頑張るのを試みましたが、操作方法が独特すぎて諦めました。謎にバッファが作られたり、 C-o で戻れないなど問題がありすぎでした。ただ Netrw の「ウィンドウスタイル」は良いなと思ったので、同様のことを実現できる lambdalisue/fern.vim を採用しました。

<Space>fd で Fern.vim が起動するようにマッピングしています。見た目も簡素で、これくらいで良さそう。

ただし、下記の課題を感じているので、いっそのことファイラーを自作しようかと考えています。

  • LuaTree みたいに、 kyazdani42/nvim-web-devicons のアイコン表示ができない。
  • gitignore をグレーに表示したい
  • Fern に入ったあと、ディレクトリ移動すると、前のファイルに C-o で戻れない
    • Buffer リストで移動したい

Fuzzy Finder

Neovim では Telescope が有名ですが、個人的にずっと FZF を愛用してきたので、junegunn/fzf.vim の後継的なポジションである ibhagwan/fzf-lua を利用しています。機能もかなり豊富で助かっています。

余談ですが、 fzf-lua の作者である ibhagwan さんはめちゃめちゃ良い人で、私も一つ Issue を立てましたが、すぐに返信を丁寧にくれました。また 寄付したいという人 に対しても、丁重に断っていて OSS に対する信念を感じました。

一点、 git_files() 時に自動的に現在のディレクトリを入力してくれるようなコマンドを追加しました。モノレポで便利になります。

local function git_files_cwd_aware(opts)
	opts = opts or {}
	local fzf_lua = require("fzf-lua")
	local git_root = fzf_lua.path.git_root(opts)
	if not git_root then
		return
	end
	local relative = fzf_lua.path.relative(vim.loop.cwd(), git_root)
	opts.fzf_opts = { ["--query"] = git_root ~= relative and relative .. "/" or nil }
	return fzf_lua.git_files(opts)
end

LSP

もともと neoclide/coc.nvim を使っていましたが、標準機能でまかないたいという大方針、そして Neovim の標準 LSP と補完フレームワークを使うのがトレンドっぽいので、 neovim/nvim-lspconfighrsh7th/nvim-cmp を使うことにしました。

LSP 関連の設定は下記です。これだけで、 COC に劣らない操作性を実現できることに感動しました。また、 fzf-lua と連携できるっぽいので、後でそれも試そうと思っています。

Python と TypeScript だけ設定してますが、ほかも neovim/nvim-lspconfig のページを見ながら簡単に設定できます。

local lsp = require("lspconfig")
lsp.pyright.setup({})
lsp.tsserver.setup({})

local cmp = require("cmp")
cmp.setup({
	sources = {
		{ name = "nvim_lsp" },
		{ name = "buffer" },
		{ name = "path" },
		{ name = "cmdline" },
		{ name = "snippy" },
	},
	mapping = {
		["<C-p>"] = cmp.mapping.select_prev_item(),
		["<C-n>"] = cmp.mapping.select_next_item(),
		["<C-l>"] = cmp.mapping.complete(),
	},
})
cmp.setup.cmdline(":", {
	mapping = cmp.mapping.preset.cmdline(),
	sources = {
		{ name = "path" },
		{ name = "cmdline" },
	},
})

ただ、 GraphQL だけはなぜか設定できませんでした。 ctag とかで代用しようか考え中。

スニペット

もともと UltiSnip → LuaSnip と変遷しましたが、もっとシンプルなものを探しており、 dcampos/nvim-snippy に出会いました。完全 Lua 製なのと、シンプルな使い勝手という理由で採用しました。 LuaSnip 時代は Lua のみでスニペットを書いてみたが辛かった...

nvim-cmp と連携できますが、しなくても十分使えます。

require("snippy").setup({
	mappings = {
		is = {
			["<Tab>"] = "expand_or_advance",
			["<S-Tab>"] = "previous",
		},
	},
})

スクショは React で useState 書く時。 ust<C-s> で起動するようにしました。

フォーマッター系

もともと Prettier.vim や Stylua.nvim など、言語・フォーマッターに合わせたプラグインを入れていました。が、下記の問題があり、どうにかしたいと思っていました。

  • 不要なカーソルの位置が記憶されたり、履歴が残ったりすることがある。特に Prettier.vim
  • コマンドを発見できなくて失敗したり、実行ファイルが明示的ではない場合がある。 node_modules/.bin/prettier なのか /home/kazuya/.local/share/nvm/v18.0.0/bin/prettier なのか /bin/prettier なのか。
  • バージョンの不一致など

その結果、バッファを標準出力として読み込ませ、結果をバッファに反映するだけのプラグインを自作しました。

https://github.com/acro5piano/nvim-format-buffer

これを使うと、下記のように autocmd で柔軟に対応できます。

vim.api.nvim_create_autocmd({ "BufWritePre" }, {
	pattern = { "*.lua" },
	callback = require("nvim-format-buffer").create_format_fn("stylua -"),
})

vim.api.nvim_create_autocmd({ "BufWritePre" }, {
	pattern = { "*.py" },
	callback = require("nvim-format-buffer").create_format_fn("yapf"),
})

vim.api.nvim_create_autocmd({ "BufWritePre" }, {
	pattern = { "*.js", "*.jsx", "*.ts", "*.tsx" },
	callback = require("nvim-format-buffer").create_format_fn("prettier --parser typescript"),
})

TODO

ほぼ完成しましたが、下記の作業が残っています。

  • 普段そこまで使わない LSP の設定
    • Ruby
    • Go
    • PHP

参考

Discussion

ログインするとコメントできます