🗡

ddc.vimのlsp機能を強くする with nvim-lsp

4 min read

はじめに

ddc.vimのLSPまわりの設定が良い感じになってきたので共有します。
前提として、補完ソースに関してはこちらを入れてください。

https://github.com/Shougo/ddc-nvim-lsp

最終的にはこんな感じで使えるようになります。

補完候補のプレビュー & signature help

以下のgifにあるような機能で、前者は、補完候補を選択したときにその詳細情報をfloating windowに表示します。signature helpは、補完候補を選択して関数の中身を書いているときに、引数などの情報を表示する機能です。

vim-lspでは、こうした機能はlanguage client側で実装されているのですが、nvim-lspにはそうした機能はなく、補完プラグインが行なう必要があります。

これらの機能に関しては、いい感じに使えるものがなかったので、自分で作りました。

https://github.com/matsui54/ddc-nvim-lsp-doc

ちなみに、deoplete.nvimのnvim-lspソースであるdeoplete-lspでは、プレビュー機能はソースの中に組込まれていました。これを単純に移植すれば、一応ddcでも使えるようになるのですが、ddc-nvim-lsp-docでは、その道はとらずdenopsプラグインとして実装することにしました。
この理由としては、deoplete-lspで使われているluaに比べて、denops.vimの体験がとても良く、signature helpなども実装する場合に書きやすくなるのではないかと考えたためです。

代替プラグインとしては、float-preview.nvimというプラグインもあります。ただ、lsp専用ではないために言語によってはプレビューを出してくれない上、プレビューの中身もハイライトされません。

https://github.com/ncm2/float-preview.nvim

あとsignature helpに関してはlsp_signature.nvimというのもあって、これはとても良いです。
ddc-nvim-lsp-docはsignature helpのみを無効化するといったこともできるので、こちらが好みなら使ってみてください。自分は、手元の環境であまりうまく動かせなかったのと、カーソルの下にfloating windowを出されるのが嫌で使っていません。

https://github.com/ray-x/lsp_signature.nvim

textEdit

textEditとは、language serverから補完候補と一緒に送られてくる情報で、補完候補を確定させたときにどのような変更をテキストに加えるかを決めるものです。
以下に例を挙げて、textEditの詳細となぜddc.vim+ddc-nvim-lspだけでは不十分かを説明します。
例えば、以下のようなCのコードを考えます。

typedef struct _foo {
  int hoge;
} foo;

int main(int argc, char *argv[]) {
  foo *var;
  var|    // |はカーソル位置
}

上のような位置にカーソルがあるとして、foo構造体の要素hogeにアクセスしたいとします。このときに.と誤って入力してしまったとしても、ちゃんとhogeという候補が出てきます。
しかし、ddc.vim+ddc-nvim-lspでは、これを選択するとvar.->hogeのようになってしまいます。
実はこのときlanguage serverからはtextEditとして、間違った演算子である.という文字を削除して、->hogeという文字列を代わりに挿入せよという情報が送られています。
しかし、ddc.vim (というよりはVim) の仕様として、補完候補を出している間は、一度決めた補完の開始位置を変えることができないというものがあります (この場合は、補完の開始位置はvar.の次の位置です)。この仕様により、補完開始位置をさかのぼって文字 (ここでは.) を置き換えるということはできないのです。

このあたりの説明はnvim-cmpの作者であるhrsh7thさんのこちらの記事がわかりやすいです。

https://zenn.dev/hrsh7th/scraps/565ac089dbaba1
VSCodeでは...

上の記事にあるようにVSCodeでは、補完候補を選択するだけでは文字列が自動で挿入されることはなく、Tabなどを押して確定させることで挿入します。したがって、Vimのような補完の開始位置の問題はありません。
このようにLSPの仕様は、VSCodeにかなり依存しています。ちなみにcoc.nvimは、補完候補を受け取ってから開始位置を決めているようで (多分)、候補を選択するときでもちゃんと補正されて出てきます(すごい)。

では、どのようにしてtextEditに対応すればよいのでしょうか。
これを解決するために導入するのがvim-vsnipvim-vsnip-integです。
欲しい機能はvim-vsnip-integにあるのですが、vim-vsnipに依存しているためにこちらも導入します。
このプラグインが何をしているのかというと、補完候補の選択が終了したら (正確にはCompleteDone イベントが起きたときに)、textEditを適用してくれます。つまり、候補を選択しているときは間違った文字列が挿入されることもありますが、選択が終わって次の文字を挿入しようとすると自動的に補正してくれるのです。

textEditはこの2つのプラグインを入れるだけで使えるようになります。

はじめにで示したgifの中でstrncmpを選択したときにstring.hというヘッダファイルが自動的に追加されていますが、これもtextEditの機能です。

スニペット

スニペットとは何かについてはこちらの記事を参照してください。

https://zenn.dev/shougo/articles/snippet-plugins-2020

language serverはスニペットにも対応しており、例えばfoo(int, int)という関数に対して、foo($1, $2)みたいなスニペットを送ってきます。
lspのスニペットの形式に対応しているのが先程紹介したvim-vsnipです。vim-vsnip-integも必要です。
これらのプラグインを入れた上で、以下のようにcapabilitiesを追加することで、CompleteDoneのタイミングでスニペットを展開してくれます。

local capabilities = vim.lsp.protocol.make_client_capabilities()
capabilities.textDocument.completion.completionItem.snippetSupport = true

-- language serverごとにcapabilitiesを追加する必要があります。
-- on_attachは必要に応じて
require'lspconfig'.clangd.setup{on_attach = on_attach, capabilities = capabilities}

おわりに

これらの設定によって、補完という機能に関していえば、coc.nvimと比べても遜色ないレベルになるのではないでしょうか。ddc.vimがより多くの方に使われるきっかけになれば幸いです。

追伸 LSPの比較

ddc-nvim-lsp-docを作る過程でvim-lspcoc.nvimも触ったので簡単に比較してみます。

vim-lsp

Vim Scriptで実装されているので、3つの中では一番遅いです。ただ、細かいところが丁寧に作りこまれていて使いやすいと感じました。あとはvim-lsp-settingsのおかげで、何も設定しなくても動いたので導入がとても楽です。

nvim builtin lsp

Luaで実装されているため速いです。ただ、他に比べるとデフォルトの機能がまだ荒削りという印象を受けました。lsp関係のapiは充実していて、ddc-nvim-lsp-docはその恩恵を受けました。

coc.nvim

完成度はこれが頭一つ抜けていると思います。実装もかなり参考にしたのですが、すべての機能においてきちんと作られていてすごいです。なおかつ速いし、設定もほとんどいりません。
欠点はnode依存であることと、良くも悪くも全部入りのプラグインであるということです。
私がcoc.nvimを使っていないのは、小さな機能をもったプラグインを組み合わせて使うことの方が好きだからです。ddc.vimの設定や拡張のしやすさと、実装がミニマルな点が気に入っています。

Discussion

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