Neovimでプロジェクトごとに設定する(.nvim.lua / exrc / :trust)
Neovimには、作業しているディレクトリに置いた.nvim.luaを起動時に読む仕組みがあります。'exrc'というオプションで、このリポジトリだけインデントを2にする、このプロジェクトだけLSPの設定を変える、といった使い分けをユーザー全体のinit.luaを汚さずに書けます。
ただし、置いてあるファイルを黙って実行するわけではありません。初めて見つけたファイルは中身を確認するまで実行されず、確認したファイルは中身のハッシュで覚えられます。0.12でこの確認の手順と、読むディレクトリの範囲が変わりました。0.11までの手順で書かれた解説だと「aで許可」となっていますが、0.12.4の確認画面にはその選択肢がありません。
検証環境:NVIM v0.12.4(macOS)とVim 9.1.1752。HOMEとXDG_CONFIG_HOME・XDG_STATE_HOMEを一時ディレクトリに向けた素の環境で、tmuxの中でNeovimを起動して確かめました。LSPはgoplsで試しています。掲載している確認画面・trustファイルの中身・:echoの結果はすべて実行結果で、パスは/tmp/exrc-demoの下で撮り直したものです。
目次(9項目)
| やりたいこと | 操作 |
|---|---|
| exrcを使う | init.luaにvim.o.exrc = true |
| 中身を見て信頼する | 確認でvを押し、開いた窓で:trust |
| 二度と読ませない | 確認でd、または:trust ++deny |
| 信頼を取り消す | :trust ++remove |
| 親をさかのぼらせない | .nvim.luaの中でvim.o.exrc = false |
exrcを有効にして最初の確認を読む
'exrc'は既定でオフです。ユーザー全体の設定に1行足します。
-- ~/.config/nvim/init.lua
vim.o.exrc = true
読まれるファイル名は.nvim.lua・.nvimrc・.exrcの3つで、同梱ヘルプの:help 'exrc'に並んでいます。Luaで書くなら.nvim.luaです。試しに、あるディレクトリへ次の1行だけの.nvim.luaを置き、そこでnvimを起動しました。
vim.opt.shiftwidth = 2
起動すると、画面の下にこう出て入力待ちになります。
exrc: Found untrusted code. To enable it, choose (v)iew then run `:trust`:
/private/tmp/exrc-demo/myapp/.nvim.lua
[i]gnore, (v)iew, (d)eny:
選択肢は3つです。角括弧の付いたiが既定で、Enterだけ押すと無視になりました。無視したときは:set sw?がshiftwidth=8のままで、ファイルは実行されていません。何も記録されないので、次に起動するとまた同じ確認が出ます。dは拒否で、こちらは記録が残り、以後聞かれなくなります(後の節)。
0.12のリリースノート(:help news)には、選択肢から(a)llowが消え、(v)iewで中身を見てから:trustを打つ形になったと書かれています。中身を見ずに一発で許可する道を塞いだ、という変更です。
:trustで信頼する 中身のハッシュで覚えている
確認でvを押すと、.nvim.luaが上の窓に読み取り専用(ステータス行に[RO])で開きます。ここで中身を読み、問題なければその窓で:trustを打ちます。
Allowed in trust database: "/private/tmp/exrc-demo/myapp/.nvim.lua"
記録はstdpath('state')の下のtrustというファイルに書かれます。素の環境なら~/.local/state/nvim/trustです。中身は1行1ファイルで、ハッシュとフルパスが並んでいるだけでした。
dee7f88685ee337a7f6e36bf1dfd04f87cc38fb902d41d277e64616b8d72ddc7 /private/tmp/exrc-demo/myapp/.nvim.lua
この値はshasum -a 256でファイルを計算した結果と一致しました。SHA-256で中身を覚えているので、ファイルを1文字でも変えると別物として扱われます。実際にshiftwidth = 2を4に書き換えて起動し直すと、さっきと同じ確認がもう一度出ました。trustファイルの行は古いハッシュのまま残っています。逆に、中身を元の2に戻すと確認は出ず、そのまま読み込まれました。記録しているのは「このパスにこの中身」の組です。
パスも組の片方なので、同じ中身のファイルを別のディレクトリへコピーすると、そちらでは改めて聞かれます。ブランチを切り替えて.nvim.luaの中身が変わったときや、git pullで誰かが書き換えたときに確認が出るのは、この仕組みのとおりの動きです。
もう1つ気づいたのは、:trustを打ったその起動ではファイルが実行されないことです。確認の時点で実行は見送られていて、記録が効くのは次に起動したときからでした。設定がすぐ反映されないので打ち間違えたかと思いますが、起動し直せば効いています。
:help :trustには:trust [file]という形も載っていて、開いて確かめた中身と信頼する中身がずれる(TOCTOU)おそれがあるので勧めない、と注意書きが付いています。手元の0.12.4では、引数にファイルを付けた:trust myapp/.nvim.luaはE475: Invalid argument: myapp/.nvim.luaで通りませんでした。++denyや++removeにはファイルを渡せるので、許可だけはvで開いた窓で打つ前提になっています。
親ディレクトリのexrcも読まれる(0.12から)と止め方
0.12からは、カレントディレクトリだけでなく親ディレクトリもさかのぼって探すようになりました(:help news)。リポジトリの直下と、その中のサブディレクトリの両方に.nvim.luaを置いて確かめます。
-- proj/.nvim.lua
vim.g.order = (vim.g.order or "") .. " proj"
-- proj/app/.nvim.lua
vim.g.order = (vim.g.order or "") .. " app"
proj/appで起動すると、まずappの確認が出て、答えると続けてprojの確認が出ました。両方にvを押すと窓が2つ開くので、それぞれの窓で:trustを打ちます。起動し直して:echo g:orderを見た結果がこれです。
app proj
近いほうから順に、上へ向かって読んでいます。ヘルプの言い方では「ordered upwards」です。後から読まれるのは親なので、子と親で同じオプションを設定すると親の値が残ります。サブディレクトリ側で上書きするつもりで書くと逆になるので、ここは直感と合いません。
さかのぼりを止めるには、途中の.nvim.luaで'exrc'を外します。proj/app/.nvim.luaの末尾に1行足しました。
vim.g.order = (vim.g.order or "") .. " app"
vim.o.exrc = false
中身が変わったので確認が出直し、信頼し直してから起動すると:echo g:order .. " exrc=" .. &exrcは app exrc=0になり、親のprojは読まれませんでした。さらに.nvim.luaの無いproj/app/srcで起動しても結果は appで、srcから上へ探してappで止まっています。リポジトリの直下に置く.nvim.luaには、この1行を入れておくとホームディレクトリ側の.nvim.luaまで拾いにいかなくなります。
:trust ++denyと++removeの違い
確認でdを押すか、vで開いた窓で:trust ++denyを打つと、trustファイルにハッシュの代わりに!が書かれます。
dee7f88685ee337a7f6e36bf1dfd04f87cc38fb902d41d277e64616b8d72ddc7 /private/tmp/exrc-demo/myapp/.nvim.lua
! /private/tmp/exrc-demo/cloned/.nvim.lua
拒否はパスだけで覚えていて、中身を書き換えても確認は出ず、ずっと無視されました。許可は中身が変わると取り消されるのに、拒否は中身に関係なく続きます。信頼できない場所のファイルを黙らせるには都合のよい非対称です。
:trust ++removeは記録の行そのものを消します。
Removed from trust database: "/private/tmp/exrc-demo/cloned/.nvim.lua"
消したあとは何も覚えていない状態に戻るので、次の起動でまた確認が出ます。許可した覚えのあるファイルを見直したいときも、拒否したファイルを使うことにしたときも、まず++removeで白紙に戻してからvで確認し直す流れになります。どちらもファイル名を付けて:trust ++remove cloned/.nvim.luaのように打てました。引数を付けずに空のバッファで打つとE5570: Cannot update trust file: buffer is not associated with a fileになります。
| 操作 | trustファイル | 中身を変えたら | 次の起動 |
|---|---|---|---|
| i(無視) | 書かない | 関係なし | また聞かれる |
:trust | ハッシュとパス | また聞かれる | 読み込む |
++deny | !とパス | 無視のまま | 聞かれない |
++remove | 行を消す | 関係なし | また聞かれる |
プロジェクトだけLSPの設定を変える(.nvim/lsp/)
0.12の:help 'exrc'には、プロジェクト単位のLSP設定の作り方が3手順で載っています。'exrc'を有効にし、プロジェクトの直下に.nvim/lsp/*.luaを置き、.nvim.luaに次の1行を書く、というものです。
vim.cmd[[set runtimepath+=.nvim]]
lsp/ディレクトリとvim.lsp.enable()の仕組みはNeovimの標準LSP設定(lsp/とvim.lsp.enable)で書いたとおりで、runtimepathの中にあるlsp/<名前>.luaが集められます。.nvimをruntimepathに足せば、プロジェクトの.nvim/lsp/もその対象に入ります。goplsで確かめました。ユーザー側の~/.config/nvim/lsp/gopls.luaにはこう書いておきます。
return {
cmd = { 'gopls' },
filetypes = { 'go' },
root_markers = { 'go.mod' },
settings = { gopls = { gofumpt = false, staticcheck = false } },
}
プロジェクト側の.nvim/lsp/gopls.luaには、変えたい項目だけを書きます。
return {
settings = { gopls = { gofumpt = true } },
}
このプロジェクトでmain.goを開き、つながったクライアントの設定を:lua for _,c in ipairs(vim.lsp.get_clients({ bufnr = 0 })) do print(c.name, vim.inspect(c.config.settings, { newline = ' ', indent = '' })) endで出した結果です。
gopls { gopls = { gofumpt = true, staticcheck = false } }
gofumptはプロジェクト側の値に変わり、プロジェクト側に書いていないstaticcheckはユーザー側の値が残っています。表は丸ごと差し替えではなく、キーごとに重ねられていました。.nvim.luaを退かして同じことをするとgofumpt = falseに戻ります。プロジェクト側にcmdを書く必要が無いので、サーバーの場所はユーザー側に1か所だけ持たせておけます。
1つ注意があります。:set runtimepath?を見ると、末尾に足されたのは.nvimという相対パスのままでした。カレントディレクトリを基準に解決される形なので、起動したあとに:cdで別のディレクトリへ移ったときの扱いは、この書き方に頼りすぎないほうが安心です。ここは移動したあとの挙動までは確かめていません。
editorconfigとexrcの使い分け
インデント幅や改行コードを揃えたいだけなら、.editorconfigでも足ります。Neovimは0.9から.editorconfigを標準で読みます。[*.py]にindent_style = spaceとindent_size = 4を書いたディレクトリでa.pyを開くと、確認は何も出ずにshiftwidth=4・expandtabになりました。
:help 'exrc'は両者の違いを2行で挙げています。exrcは任意のコードを実行できるが、editorconfigは設定を指定するだけ。exrcはNeovim専用だが、editorconfigは他のエディタでも効く。確認が要らないのも、何かを実行する余地が無いからです。
| 観点 | .editorconfig | .nvim.lua |
|---|---|---|
| 書けること | インデント・改行・文字コードなど決まった項目 | Luaで何でも |
| 他のエディタ | 対応したエディタでも効く(VS Codeは拡張機能) | Neovimだけ |
| 起動時の確認 | なし | 信頼するまで実行しない |
| リポジトリに入れる | チームで共有する前提 | 個人の好みなら入れない |
インデントのようにチーム全員で揃えるものは.editorconfigに書いてリポジトリに入れ、LSPの設定やキーマップのような自分の作業環境の話だけを.nvim.luaに置くと、他の人に確認画面を押し付けずに済みます。タブとスペースの話そのものはVimのタブをスペースに変換する :retab と expandtab の違いにまとめています。
cloneしたリポジトリの.nvim.luaに気をつけること
.nvim.luaは普通のLuaなので、ファイルを書く・外部コマンドを起動する、といったことも書けます。確かめるために、実行されたら印のファイルを作るだけの.nvim.luaを置いたディレクトリを用意しました。そこで起動して確認にEnter(無視)で答えると、印のファイルは作られませんでした。nvim --headlessで起動した場合も、確認の文面を出しただけで実行せずに終了しています。信頼の記録が無い限りは実行されない、という作りは手元の範囲では守られていました。
危ないのは確認を流れ作業で通すことです。vを押すと窓が開くので、つい:trustまで手が動きます。他人のリポジトリで確認が出たら、開いた窓でrequireしているモジュール、vim.fn.systemやio.popen、os.executeの行が無いかを読んでから決めます。読む気が無いものはdで拒否しておけば、中身が変わっても二度と聞かれません。
親ディレクトリまでさかのぼるようになったことも、ここに関わります。たとえば~/src/.nvim.luaを信頼していると、~/srcの下にcloneしたすべてのリポジトリでそれが読まれます。自分で書いたファイルなら害はありませんが、そこに置いた設定がどこまで届くかは把握しておく必要があります。前の節のvim.o.exrc = falseで止めておくと範囲がはっきりします。
比較のために、Vim 9.1.1752でも~/.vimrcにset exrcを書いて、let g:x = "local .vimrc"だけの.vimrcを置いたディレクトリで起動しました。確認は何も出ずに:echo g:xがlocal .vimrcを返し、黙って実行されています。Vimには信頼の記録が無く、:help 'exrc'は'secure'の併用を勧めたうえで「BETTER NOT SET THIS OPTION!」とまで書いています。同じ'exrc'という名前でも、Neovimのものは別物と考えたほうが安全です。なお、Vimのほうは1つ下のサブディレクトリで起動すると読まれず、親はさかのぼりませんでした。
親と子の両方に置いて試したとき、子の.nvim.luaでshiftwidthを変えたのに効かず、しばらく子のファイルが読まれていないのだと思い込んでいました。vim.gに名前を足していく仕掛けを入れて初めて、両方とも読まれていて、後から来た親が上書きしていたと分かりました。もう1つ戸惑ったのは、vで開いて:trustを打ったのに設定が変わらないことで、打ち直しても同じでした。その起動では実行されないと知るまで、trustファイルを3回開いて見比べています。
まとめ
Neovim 0.12の.nvim.luaは、vim.o.exrc = trueで有効にし、初めての確認ではvで中身を読んでから:trustを打ちます。記録はSHA-256とパスの組なので、中身が変わればまた聞かれ、拒否した!の行は中身に関係なく効き続けます。親ディレクトリも上へ向かって読まれ、後から読む親の設定が残るので、リポジトリの直下ではvim.o.exrc = falseで止めておくと迷いません。
チームで揃える項目は.editorconfig、自分の作業環境の話は.nvim.luaと.nvim/lsp/、と分けると確認画面が出る場面も減ります。0.12のほかの変更はNeovim 0.12の新機能 外せるプラグインと外せないものにまとめています。