Vim/Neovimの標準自動補完(autocomplete)
Vimの補完は昔から強力でしたが、必ずCtrl-nやCtrl-x Ctrl-oを自分で押す必要がありました。VSCodeのように打っているそばから候補が出てほしい、という理由でnvim-cmpのような補完プラグインを入れていた人は多いはずです。'autocomplete'オプションは、この「自動でポップアップを出す」部分だけを本体に取り込んだものです。1行の設定で、プラグインなしに入力中の補完メニューが出るようになります。もともとVim側で実装された機能で、Vim 9.2とNeovim 0.12の両方で使えます。
このオプションを含めて、0.12で標準になった機能とプラグインの対応はNeovim 0.12の新機能まとめに並べてあります。
検証環境:macOS上のNeovim 0.12.4と、macOS同梱のVim 9.1.1752で確認しました。'autocomplete'はVim同梱のversion9.txtでは9.2の新機能として案内されていますが、9.1のパッチ列の時点ですでに使えます。このページの既定値・補完メニューの中身・LSPが返した候補は、どれもtmuxの中で対話的に起動した実機から取った実物です。LSPの確認にはgopls v0.16.2を使いました。打鍵のリズムのような体感だけは機械で測れないので、使用感として本文とは分けています。
目次(8項目)
| 設定 | これだけ覚える |
|---|---|
set autocomplete | 入力中に補完メニューを自動で出す |
set complete=... | 候補をどこから集めるかを決める |
set complete+=o | LSPの候補(omnifunc)を混ぜる |
set autocompletedelay=200 | 出るまでの待ち時間(ミリ秒) |
setlocal noautocomplete | このバッファだけ自動補完を止める |
1行で自動補完になる
まず既定値を確認しておきます。設定ファイルを何も持たない一時ディレクトリをホームに指定してNeovim 0.12.4を起動し、そのまま読み取った値がこれです。
autocomplete=0
autocompletedelay=0
autocompletetimeout=80
completeopt=menu,popup
complete=.,w,b,u,t
同じ手順でVim 9.1.1752を起動すると、同じ名前のオプションのうち2つが違う値でした。
autocomplete=0
completeopt=menu,preview
complete=.,w,b,u,t,i
Vimは選択中の候補の説明を昔ながらのプレビューウィンドウに出し、Neovimはポップアップに出します。もうひとつ、'complete'の末尾にあるVimだけのiは、インクルードしたファイルまで走査する指定です。Cのヘッダのように連鎖して読み込むファイルが多い言語では、この1文字のぶんだけ候補集めが重くなります。
'autocomplete'はどちらも既定で無効なので、有効にするだけで挙動が変わります。
-- init.lua(Neovim)
vim.o.autocomplete = true
" vimrc(Vim)
set autocomplete
これで挿入モードで文字を打った時点でポップアップが出ます。Ctrl-nと同じ仕組みが自動で走っているだけなので、候補の選択は今までどおりCtrl-nとCtrl-p、確定はCtrl-y、取り消しはCtrl-eです。Vim標準の入力補完を使ったことがあれば、覚え直すことはほとんどありません。
既定のままだとうるさく感じる理由もはっきりしています。候補集めは1文字目から走ります。alphabetとalphanumericの2語だけを置いたバッファでaを1回押したところ、その時点で2件のメニューが開きました。単語の頭を打つたびに必ず何かが出るということなので、視界が落ち着きません。
これを抑えるのが'autocompletedelay'です。既定は0ミリ秒、つまり打鍵と同時に出ます。
vim.o.autocompletedelay = 200 -- 打鍵が止まって0.2秒後に出す
効き方も測りました。autocompletedelay=2000にしてalpまで打ち、0.6秒後に画面を取るとメニューは開いておらず、3.1秒後に取ると3件が並んでいました。最後の打鍵から指定した時間ぶん待ってから開く形です。
名前が似ている'autocompletetimeout'のほうは待ち時間ではなく、候補を集める側に割り当てる時間の初期値です。既定は80ミリ秒で、ここから始めて遅いソースほど持ち時間を半分ずつ削る減衰方式になっています。'complete'にFやoのような関数ソースが入っているときだけは例外で、言語サーバーの都合を見込んで1秒程度まで許されます。ヘルプには80以上のみ有効と書かれていますが、手元の0.12.4でset autocompletetimeout=40はエラーにならず、値は40のまま返りました。下げる方向には使わないほうが無難です。
適用範囲も押さえておくと後で困りません。'autocomplete'と'completeopt'はグローバルとバッファローカルの両方を持ち、'complete'はバッファローカルです。おかげで、書きもののバッファだけ自動補完を止められます。
-- after/ftplugin/markdown.lua
vim.opt_local.autocomplete = false
この1行を置いたうえでMarkdownのバッファを開くと、ローカルの値が0、グローバルの値が1になり、文字を打ってもメニューは出ませんでした。文章やコミットメッセージを書くときだけ単語の断片が飛び出す煩わしさは、これで切り離せます。逆に「有効にしたはずなのに出ない」ときも、:echo &l:autocomplete &g:autocompleteでこの2つを見比べるのが先です。
候補はどこから来るのか
自動補完が何を候補にするかは'complete'オプションで決まります。これはCtrl-nと共通の設定で、既定の.,w,b,u,tが指しているのは最初の5つです。混ぜられるソースをまとめて並べておきます。
| フラグ | 探す場所 |
|---|---|
. | 今のバッファ |
w | 他のウインドウのバッファ |
b | バッファリストにある読み込み済みバッファ |
u | バッファリストにある未読み込みバッファ |
U | バッファリストに載っていないバッファ |
t | タグ(]も同じ意味) |
i | インクルードしたファイル |
d | インクルード先のマクロ・定義名 |
f | バッファ名(中身ではなくファイル名) |
k | 'dictionary'に指定した辞書ファイル |
kspell | 有効なスペルチェックの辞書 |
s | 'thesaurus'に指定した類語ファイル |
o | 'omnifunc'(LSPが接続していればLSPの候補) |
F | 自分で書いた関数 |
並べた順序にも意味があります。さきほどの減衰は、リストの前にあるソースほど持ち時間が長いという形で効きます。どのソースにも最低限の時間は確保されるので、遅いものを混ぜても入力が固まりません。
順序より体感に効くのが件数の上限です。フラグの後ろに^と数字を付けると、そのソースから取る候補の数を絞れます。ヘルプが例として挙げているのはこの形です。
set autocomplete
set complete=.^5,w^5,b^5,u^5
set completeopt=popup
どのくらい違うのかを目に見える形にしました。alpha00からalpha11まで12語を置いたバッファでalpまで打つと、12件がカーソルの上に積み上がり、21行ある本文領域のうち12行がメニューで埋まります。同じバッファでcomplete=.^3にすると、出たのはalpha11、alpha10、alpha09の3件だけでした。手動のCtrl-nなら押した瞬間だけの話ですが、自動補完は打つたびに起きるので、この差がそのまま視界の差になります。
候補を捨てたくないなら'pumheight'という手もあります。同じ12語のバッファでset pumheight=5にすると、候補は12件のままメニューの表示だけが5行に収まりました。^が候補そのものを減らすのに対し、こちらは見える高さだけを抑えます。
上限にはひとつ注意があります。^の件数指定が効くのは前方向の補完(Ctrl-n)だけで、Ctrl-pでは無視されます。メニューを閉じてからCtrl-pで遡ると件数が急に増えるのはこのためです。
completeoptで実際に変わるのは5つだけ
'completeopt'は候補の見せ方と入り方を決めるオプションで、指定できる値が11個もあるのが厄介です。ところが'autocomplete'を有効にすると、このうち意味を持つ値はごく一部に減ります。Neovim 0.12.4のヘルプははっきりそう書いています。
Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an effect when 'autocomplete' is enabled.
効かない側にmenuoneとnoselectが入っているのが読みどころです。menuoneを外してcompleteopt=menuだけにし、一致する単語が1件しかないバッファでzepまで打つと、それでもメニューは開きました。'autocomplete'を無効にして同じ設定でCtrl-nを押すと、今度はメニューは出ずに単語がそのまま入ります。自動補完では候補が1件でも必ずメニューが出る、つまりmenuoneは書いても書かなくても同じです。
noselectも同じで、'autocomplete'が有効なあいだは自動的に入ります。上のどの試行でも、メニューが開いた時点のバッファは打ったzepのままで、何も先に挿入されていませんでした。menu自体も外してcompleteopt=popupだけにしてみましたが、それでもメニューは出ます。ネットで見かける設定例はmenuone,noselect付きが多いものの、標準の自動補完に持ち込むなら削って構いません。
| 値 | autocomplete有効時 | 打鍵でどう変わるか |
|---|---|---|
fuzzy | 効く | 綴りを飛ばして当たる |
longest | Neovimのみ | 共通の前置きが先に入る |
preinsert | 効く | 先頭候補の残りが先に入る |
popup | 効く | 説明が別枠で出る |
preview | 効く | 説明がプレビューウィンドウに出る |
menu | 効かない | 外してもメニューは出る |
menuone | 効かない | 1件でも常にメニューが出る |
noselect | 効かない | preinsert時を除き未選択で始まる |
noinsert | 効かない | 同上 |
nearest | 効かない | 効く5つに入っていない |
nosort | 効かない | 効く5つに入っていない |
体感がいちばん変わるのはfuzzyです。alphabet、alphanumeric、alpineが並ぶバッファでapnと打つと、alpineとalphanumericが出ました。頭から正確に打たなくても当たるので、長い識別子ほど効きます。
preinsertは、先頭候補の残りをその場に書き込んでしまう表示です。同じバッファでalpまで打つと行がalphabetになり、メニューには3件が並びました。カーソルは動かないので、続けて打てば上書きされます。そのまま採るならCtrl-yです。
longestには落とし穴があります。completeopt=menu,longestでalまで打つと、3件に共通するalpまでがカーソルの後ろに差し込まれます。ここでCtrl-yを押して入るのはalpだけです。メニューの先頭が選ばれているように見えて実際は未選択なので、候補を確定したつもりが共通の前置きだけを確定してしまいます。候補そのものを入れるならCtrl-nで一度選んでからCtrl-yを押します。
そしてこのlongestが、VimとNeovimでいちばんはっきり分かれるところです。同じ位置にあるヘルプの一文が、Vim 9.1.1752ではfuzzy、popup、popuphidden、preinsert、previewの5つになっていて、longestが入っていません。実際にVimでset autocomplete completeopt=menu,longestにしてalまで打つと、行はalのままで何も差し込まれませんでした。逆にpopuphiddenはNeovimには存在せず、set completeopt=popuphiddenはエラーになります。オプション名が同じでも、自動補完と組んだときの結果まで同じとは限りません。
LSPの候補を混ぜる
バッファ内の単語だけでは補完として物足りません。Neovimの標準LSPを有効にしていれば、言語サーバーが返す候補も同じ仕組みに流し込めます。LSPが接続したバッファでは'omnifunc'が差し替えられるので、'complete'にoを足すだけです。
vim.o.autocomplete = true
vim.o.complete = '.,o' -- 現在のバッファ + LSP
goplsを接続したGoのバッファで設定を読み直したところ、次の状態になっていました。
complete=.,o ac=1 omnifunc=v:lua.vim.lsp.omnifunc
この状態でfmt.Sprまで打つと、goplsからの候補がそのままメニューに並びます。'completeopt'にpopupが入っているので、選択中の候補のシグネチャと説明文が横の枠に出ます。
Sprint Function func(a ...any) string
Sprintf Function func(format string, a ...any) string
Sprintln Function func(a ...any) string
ひとつ引っかかったのが起動直後です。ファイルを開いて数秒でstrings.Splまで打ったときは候補がゼロでした。少し待ってから同じキーを打ち直すと、Split、SplitAfter、SplitAfterN、SplitNの4件が(from "strings")付きで出ます。goplsがワークスペースを読み終わるまでは時間制限に間に合いません。'autocompletetimeout'がoのときだけ1秒近くまで伸びるのは、この遅さを見込んだ作りです。
驚いたのは確定したときの挙動です。'complete'にoを足しただけの設定で、言語サーバーが付けてきた「別の行への編集」まで適用されます。import文の自動追加で確かめました。import "fmt"しか書いていないGoのファイルでstrings.SplからSplitAfterを選び、Ctrl-yを押した結果がこれです。
package main
import (
"fmt"
"strings"
)
func main() {
_ = strings.SplitAfter
fmt.Println("hi")
}
1行だったimportがブロックに書き換わり、"strings"が足されています。候補を選んだ場所とは別の行が動くので、補完プラグインだけの機能だと思い込んでいました。
スニペットも同じ経路で展開されます。goplsにusePlaceholdersを渡した状態でfmt.SprinからSprintfを選びCtrl-yを押すと、行はfmt.Sprintf(format string, a ...any)になり、vim.snippet.activeがtrueを返しました。Neovimは既定でTabにプレースホルダ間の移動を割り当てているので、そのまま引数を埋めていけます。
vim.lsp.config('gopls', {
settings = { gopls = { usePlaceholders = true } },
})
この2つにvim.lsp.completion.enable()は要りません。呼ぶ設定と呼ばない設定を用意して、違いをLspAttachのautocmdだけにしたうえで同じ操作をしましたが、importの追加もスニペットの展開もどちらでも同じでした。仕掛けは$VIMRUNTIME/lua/vim/lsp/completion.luaにあります。oが呼ぶv:lua.vim.lsp.omnifuncは内部のtrigger関数を通り、そこで確定時の後始末が登録されます。omnifuncを通った時点で有効になっている、というだけのことでした。ヘルプは確定時の副作用をenable()の効果として並べていますが、その文自身が「autotrigger、'omnifunc'、vim.lsp.completion.get()のいずれで呼び出した補完でも働く」と断っています。
ではvim.lsp.completion.enable()は何のためかというと、主なところはトリガー文字での自動発火と、複数の言語サーバーをまたいだ候補の並べ替えです(ほかに候補の整形を差し替えるconvertもあります)。autotriggerを有効にすると、言語サーバーが「ここで補完を出すべき」と申告した文字(Goなら.など)に反応して候補が出ます。
vim.api.nvim_create_autocmd('LspAttach', {
callback = function(args)
vim.lsp.completion.enable(true, args.data.client_id, args.buf, {
autotrigger = true,
})
end,
})
2つの入口は役割が違います。'autocomplete'は入力そのものに反応して'complete'の全ソースから集める仕組み、autotrigger付きのvim.lsp.completion.enable()はLSPのトリガー文字だけに反応してLSPの候補を出す仕組みです。両方入れても競合はしませんでした。'autocomplete'とcomplete+=oで足りることが多いので、まずはそこから始めるのが分かりやすいと思います。
候補が出ないときの切り分け
自動補完が黙っているときの原因は数が限られています。上から順に潰すのが速いです。
最初に見るのは値そのものです。:set completeopt?と:verbose set complete?を続けて打つと、いまの値と、それを最後に書き換えた場所まで分かります。
:verbose set complete?
complete=.^5,w^5
Last set from ~/.config/nvim/init.lua (run Nvim with -V1 for more details)
ここに身に覚えのないファイル名が出たら、プラグインかftpluginが上書きしています。'complete'はバッファローカルなので、ファイルタイプごとの設定に負けやすいオプションです。読み込まれた設定ファイルの一覧は:scriptnamesの使い方、オプションの現在値の眺め方は:set allの読み方にまとめてあります。
いちばん気づきにくいのが、'complete'に書いたoが空振りしている場合です。oは'omnifunc'を呼ぶという指定でしかないので、'omnifunc'が空なら何も起きません。実際にcomplete=oだけにしてLSPの付いていないテキストファイルで打つと、エラーも警告も出ないまま候補が0件になりました。
:echo &omnifunc
:lua vim.print(vim.lsp.get_clients({ bufnr = 0 }))
前者が空文字ならoは無意味ですし、後者が空のテーブルならそもそも言語サーバーが接続していません。
最後に、範囲外のものを期待していないかです。'autocomplete'が面倒を見るのは挿入モードだけで、:や/のコマンドラインには効きません。コマンドライン側の自動補完は別系統になっていて、wildtrigger()を自分で呼ぶ形です。
vim.api.nvim_create_autocmd('CmdlineChanged', {
pattern = { ':', '/', '?' },
callback = function() vim.fn.wildtrigger() end,
})
vim.o.wildmode = 'noselect:lastused,full'
vim.opt.wildoptions:append('pum')
この設定を入れたNeovimで:e alphまで打つと、alpha.txtとalphabet.txtがポップアップに並び、Tabで選べました。検索側で候補が要らないならpatternから/と?を外します。検索コマンドラインでの単語補完はカーソル下の語を取り込む別の操作です。
nvim-cmpの代わりになるか
ここは正直に線を引いておきます。標準の自動補完でまかなえるのは「入力に応じて候補を出し、選んで確定する」という中心部分です。ただし前の節のとおり、確定したあとの後始末はcomplete+=oだけで標準でも動きました。
| 機能 | 標準のautocomplete | nvim-cmp等 |
|---|---|---|
| 入力中のポップアップ | できる | できる |
| バッファ・タグ・LSPの候補 | できる | できる |
| あいまい一致 | fuzzyで可能 | できる |
| import文の自動追加 | complete+=oだけで可能 | できる |
| LSPのスニペット展開 | 同上 | できる |
| ソースごとの並び替えルール | 順序と件数上限のみ | 細かく制御できる |
| 標準に無いソースの追加 | できない | 自由に足せる |
| 候補のアイコン・整形 | 'completeitemalign'程度 | 自由に書ける |
スニペットプラグインの候補やパス補完、AIの提案のようなものを同じメニューに混ぜたいなら、いまのところ補完プラグインの仕事です。逆に「候補が自動で出て、確定したらimportまで付いてくればよい」という使い方なら、プラグイン1つと設定数十行がオプション3つに置き換わります。vim.packで入れるプラグインを減らしたいときの候補として現実的です。
切り替えの手間が小さいのも利点です。'autocomplete'を無効に戻せば挙動は元どおりですし、自動補完中でもCtrl-xを押せば一時的に止まって任意の補完モードに入れます。ただしメニューが開いているあいだのCtrl-nは選択の移動になるので、素のCtrl-nを走らせたいときは先にCtrl-eでメニューを閉じるようヘルプが案内しています。
最初にset autocompleteだけ入れて書き始めたら、2文字打つたびにメニューが開いて正直うるさく感じました。次にやらかしたのがlongestで、共通部分が先に入るのが気持ちよくて足したまま使っていたら、メニューの先頭が選ばれていると思い込んでCtrl-yを押し、alpだけが残った行を何度も作りました。Goのファイルでcomplete+=oを入れたのに候補がゼロのままで延々と設定を疑い、結局はそのバッファに言語サーバーが付いていなかっただけ、ということもありました。あとは確定がCtrl-yなのを忘れてEnterで改行してしまう癖が、しばらく抜けませんでした。
まとめ
'autocomplete'を有効にすると、VimもNeovimもプラグインなしで入力中の補完メニューを出します。候補の集め先はCtrl-nと同じ'complete'で決まり、oを足せばLSPの候補も混ざります。既定のautocompletedelay=0は1文字目から反応するので、遅延と件数上限を調整するところまでを1セットと考えるのが現実的です。'completeopt'は、自動補完と組むと効く値がfuzzy、longest、popup、preinsert、previewの5つに減り、そのうちlongestはNeovimだけです。候補が出ないときは値・範囲・'omnifunc'の3点を順に見ます。complete+=oは候補を混ぜるだけでなく、確定したときのimport追加やスニペット展開まで連れてきます。vim.lsp.completion.enable()はそこには要らず、トリガー文字での自動発火と並べ替えのための別口です。