NeovimビルトインのインデントとLSPのDocumentFormatの挙動の違いに辟易としたら
Table of Contents
Neovimビルトインのインデントの挙動とLSP経由でフォーマットしたときの挙動の違いがずっと気持ち悪かったので、対策を練った。
はじめに
vim(neovim)にはデフォルトでファイルのインデントを行う機能が付いている。
vim始めたての頃「gg=G
でファイルをインデントしようねー」と教わった方も多いかもしれない。
最近では、MicrosoftがLSPを作ってくれたり、NeovimでTreesitterがサポートされたりしたおかげで、 vimでの開発体験もIDE並に向上して、標準のインデント機能のみでコードを書くvimmerは少なくなったのではないかなと感じる。
それでもビルトインのインデント機能を完全に意識しないという瞬間は少なく、たとえば
o
で挿入モードに入るとき、適当な位置にカーソルを持っていってくれるのはこのビルトインのインデント機能のおかげなのだが、
近頃この機能によるインデント結果と、LSPでフォーマットをかけた時の結果の違いが気になるようになってきた。
例えば以下のコードのハイライトされた行でo
を入力すると、嬉しくない位置にカーソルが移動してしまったり。
...
switch (key) {
case value:
foo();
▏← この位置にカーソルが来る
break;
default:
bar();
break;
}
...
ファイル保存時に自動でフォーマットかければええやんというツッコミが飛んできそうだが、 そもそも自分がわざわざNeovimを好んで使っている理由の1つとして、ソフトウェアに余計なお節介を焼かれる(自分の把握していないところで勝手に何かされる)のが気持ち悪くて毎回マニュアル保存しているというのもあるので、それは嫌だった。
vimのインデントの仕組みもあまり良くわかっていなかったので、その調査も兼ねて自分が行った対応をログとして残していこうと思う。
前提
vimビルトインのインデントは、LSPと違ってプロジェクト固有のインデント方法を記述したファイル1を読み込んでインデント幅を変える、みたいな器用なことはできない。
そのため、ここでは何もオプションを与えない場合のLSPのインデントの挙動と、vimビルトインのインデントの挙動をできるかぎり揃えることを目的にする。
もし、グローバル設定と異なるインデント幅を用いる場合はプロジェクトルートないし(プロジェクトに.vimrcを含められない場合)その前段にローカルなvimrcを2用意すれば対応可能だ。
この記事ではLSPクライアントとしてcoc.nvimの利用を想定している。
対応
不要なプラグイン削除
副作用をなくすため、インデントに影響を与えるプラグインを必要最低限にして、LSPとTreesitterにインデント機能を集約する。
自分はtwig
ファイルのハイライトのためにvim-polyglot
といういろんな言語のハイライト・インデントをまとめたプラグインを入れていたが、Treesitterがtwig
に対応していたので、削除することにした。
yioneko/nvim-yati導入
いつもぶっ壊れているTreesitterのインデントをなんとかしてくれるやつ。
.vimrc設定
set expandtab " タブ入力を複数の空白入力に置き換える
set tabstop=2 " 画面上でタブ文字が占める幅(この値のみ変更)
set cindent
set shiftwidth=0 " smartindentで増減する幅(0の場合tabstopに従う)
set softtabstop=-1 " 連続した空白に対してタブキーやバックスペースキーでカーソルが動く幅(負の場合shiftwidthに従う)
augroup indent
autocmd!
au FileType go set noexpandtab tabstop=2
au FileType python set tabstop=4
augroup END
ビルトインのインデント周りの設定を行う。ポイントはshiftwidth=0
とsofttabstop=-1
の部分。
こうしておくと、各言語の設定が
- インデント幅(
tabstop
) - ソフト/ハードタブ(
expandtab/noexpandtab
)
だけで済むようになる。
ここまでで殆どのインデントの問題が解消されるが、一部素直に言うことを聞いてくれないやつがいる。
たとえば、上記.vimrc
で10行目を以下のように書いても、Pythonのインデント幅は2にならない。
au FileType python set tabstop=2
これはデフォルトで以下のランタイムが動くことに起因する。
...
setlocal expandtab shiftwidth=4 softtabstop=4 tabstop=8
...
マニュアル3通り.vimrc
に以下のような記載をすることでインデント幅の変更が反映される。
let g:python_recommended_style = 0
これ以外にも/usr/share/nvim/runtime
配下を調べた限り、以下の言語はランタイムが意図しないインデントの挙動の原因になりそうだった。
- ruby
- meson
- rust
- yaml
- markdown
必要に応じてlet g:{言語名}_recommended_style=0
してやる必要がありそう。
C/C++のswitch
cのswitchはcaseをインデントするかしないかで流儀が分かれる。
...
switch (foo) {
case 1:
bar();
break;
default:
baz();
}
...
...
switch (foo) {
case 1:
bar();
break;
default:
baz();
}
...
ビルトインのインデントは前者、clangd
のオプションなし/設定ファイルなし時のデフォルトの挙動は後者だった。
これを前者に統一しようと思い、.clang-format
が存在しない時のデフォルトオプションを指定する設定をcoc-settings.json
に追記したが動作しなかった。
...
"clangd.arguments": ["--fallback-style='{ IndentCaseLabels: true }'"],
...
ログを見てもオプションはきちんと渡っているようだったので、疑問に思い調べてみると、
どうやらこれはclangd本体のバグ4らしく、仕方なく後者のcase
でインデントしない挙動に統一することにした。
.vimrc
に以下を追記
set cinoptions+=:0 "switch内のcaseのインデント幅
cinoptions
の細かい設定の仕方は:h cinoptions-values
で確認できる。
また、nvim-yati
が上記設定を上書いてしまうので、c
とcpp
を無視する設定を追記する。
require'nvim-treesitter.configs'.setup {
...
yati = {
enable = true,
disable = { "c","cpp","python" },
default_lazy = true,
default_fallback = "auto"
},
indent = {
enable = false,
},
...
}
おわりに
ひとまずこれでインデントの不一致は気にならないレベルになった。
それでもまだ改善の余地はありそうなので、新しい発見があり次第追記していきます。
参考文献
- https://hackmd.io/@kazuki-hanai/check-coc-ls-log
- https://clang.llvm.org/docs/ClangFormatStyleOptions.html
Cなら.clangd-formatみたいな ↩︎
自分はhttps://github.com/embear/vim-localvimrcというプラグインを利用している。 ↩︎
:h ft-python-plugin
↩︎