ヘッドレスな補完エンジンをつくりたい話
はじめに
この記事は、nvim-cmp という neovim のプラグインををメンテしている間に出てきたいろいろな課題や、いま考えていること(やるとは言っていない)を説明することを目的としています。
nvim-cmp とは?
neovim における、補完プラグインです。
Pure Lua で書かれており、カスタマイズ性が高いことが特徴の補完プラグインです。
「いろいろな課題」とは?
大別すると 3 つの課題があると考えています。
▼ UI を内蔵してしまっている
nvim-cmp は UI 関連の実装を内蔵しています。
例えば、custom_entries_view.lua や wildmenu_entries_view.lua などが実装として存在しており、いくつかのカスタマイズ向けのオプションが提供されています。
- 補完メニューの表示位置をカーソルに合わせるか、キーワード位置に合わせるか
- ドキュメント表示を自動的に出すか出さないか
- 「次の候補を選択」をカーソル位置基準にするか、メニュー上の並びを基準にするか
- カーソル位置基準で「次の候補を選択」すると、メニューが上方に表示されている場合は、補完メニューの最も下のアイテムが選ばれます
- 各種 UI のボーダー・ハイライト・最大幅の設定
なかなかいろいろなオプションがありますが、正直いってユーザの期待に答えるのには不十分です。
対応されていない要望はいくつもあります。
- シェルの補完のように、メニューを出さずにインラインで候補を表示する
- 常にカーソルの上方にメニューを表示する
- メニュー内のパディング量を調整する
正直、ユーザの「見た目に関する欲求」を完全に過小評価していました。
(相当カスタマイズしやすいと思っていたんですけどね...)
▼ カスタムソースの API が独自形式になっている
実装当初は「なるべく簡単にカスタムソースを作れるようにするか」くらいの軽いノリで考えていました。
しかし、ここまでシェアが広がってしまうと「実装が簡単なこと」よりも「エコシステムの資産として活用しにくいこと」というデメリットが大きいです。
最近だと nvim-cmp 以外の補完エンジンがいくつか存在していますが、どの補完エンジンも nvim-cmp の互換レイヤーを実装する羽目になっています。
これは単純に迷惑なことをしてしまったと反省しています。
▼ 設定 API が複雑になってしまっている
nvim-cmp はあくまでも「コアとなる補完エンジン」であり、実際の補完処理は「ソース」と言われる追加のプラグインで実現されます。
このような設計になっているため、多数のプラグインをインストールする必要があり、それを敬遠するユーザもいるようです。
また、「高度なカスタマイズ性を確保する」という目標を設定したため、様々なオプションが設定可能です。
表層的な API サーフェスに様々なオプションが存在しているので、初心者にとっては「難しい」と感じることがあるようです。
(長大な文章やでかいコードレビューを与えられて読むのを諦めるのに近いものを感じています)
ヘッドレスな補完エンジンを提供するという構想
ここまでにあげた課題を解決するために「どうしたらよいか」を考え続けてきました。
現在では、記事タイトルにもあるように「ヘッドレスな補完エンジンを提供する」のがエコシステムのために必要なことだと考えています。
ヘッドレスな補完エンジンを用意すれば、下記のような世界観を実現できると考えています。
- 好みの見た目でコード補完を利用することができる
- ヘッドレスな補完エンジンが提供されれば、UI 部分をエコシステムが多数提供してくれると期待しています
- 前述の「シェル補完のような見た目」だったり、「コマンドライン向けの特殊な見た目」だったりは別プラグインとして実装される未来がくるかもしれません
- 新規実装となるのでカスタムソースの API を見直すことができる
- 独自のものではなく、完全に LSP 仕様のみを踏襲する形で定義しなおすことができます
- これにより、他の補完エンジンとの互換性が向上し、エコシステム全体が活性化することが期待できます
- 「全部入りの補完プラグイン」「カスタムに特化した補完プラグイン」などのように選択肢を増やしやすい
- 「自分は一般的な挙動が設定なしで実現できればそれでいい」という人は、そういった補完プラグインを探してインストールすることができるかもしれません
いろいろな人が補完向けのプラグインを独自に実装すればいいだけなのでは?
正直、それっぽく動作する補完プラグインを作成するのは難しくありません。
しかし、LSP 仕様を尊重し、VSCode などの挙動を研究し、多数の LSP サーバで適切に動作する補完エンジンを作成するのは難易度が高いと考えています。
例えば、nvim-cmp は ghost_text という機能を experimental として提供していますが「なぜ永遠に experimental なのか?」の理由を把握している人は少ないのではないでしょうか?
この理由は、LSP に textDocument/inlineCompletion という仕様が追加されることを予見していたからです。
ghost_text は、選択中のアイテムのプレビューをバッファ上にインラインで表示する機能です。
textDocument/inlineCompletion が正式仕様に採用された場合、ghost_text と inlineCompletion が UI 的にコンフリクトする可能性が非常に高いです。
また、completionItem/resolve や additionalTextEdits の適用、Vim の世界観に合わせた特殊実装などに関しても、仕様には明記されていない暗黙的な知識が多数存在しています。
- completionItem/resolve のマージ処理は JavaScript の Object.assign 相当
- additionalTextEdits は同期的に解決可能な場合とそうでない場合で適用タイミングを分ける必要がある
- 候補のフィルタリングに利用するテキストは、textEdit.start.character の位置に合わせて補正する必要がある(また、clangd/vscode-html-language-server の場合は特殊な補正が必要)
- 補完リクエストの送信は「キーワード補完」「トリガー文字補完」「isIncomplete 補完」「マニュアル補完」でタイミングを分ける必要がある
- Vim の補完では「候補選択時に挿入される一時的なテキスト」が存在するが、LSP には仕様上の定義がないので独自実装が必要
- Vim の補完では「補完メニューはキーワード位置に固定表示される」が「VSCode はカーソルに追従して補完メニューが移動する」ので、「キーワード位置」の検出が必要
- Vim の補完における undo/redo の適切な管理
- Vim のマクロへの対応(nvim-cmp も実現できていない)
このような細かな実装詳細は LSP 仕様には明記されていなかったり、そもそも Vim 向けに独自に仕様策定が必要だったりします。
(上記で書いたのは「特殊実装」の部分のみです。これら以外にも insertReplaceTextEdit, PositionEncodingKind, client/registerCapbility など、そもそも実装がややこしいものも多数あります。)
なるほど、それで?
実は こちら のリポジトリで実験的にヘッドレスな補完エンジンの実装を試してみています。
実装自体は進んできていますが「どこまでをコアの責務とするか」「どのような API とするか」といった部分の検討が難航しています。
(特に、API を安定化させるための検討が重要です。)
完遂できるかどうかもわかりませんが、地道にやっていこうと考えています。
おわりに
最近の考えについてつらつらと書いてみました。
もし、この方針で実装が進められたとしても、エコシステムが上記のように動いていく保証はありません。
また、最近では nvim-cmp 以外の補完エンジンの選択肢が増えてきています。
ユーザの選択肢が増えることはとてもよいことだと思っているので、気になる人はどんどん試したほうがいいと思っています。
自分は自分で高品質な補完体験をエコシステムに提供できるようにゆるゆるとやれればいいなと思います。
Discussion