VimでLSPを使う vim-lsp-settingsとyegappan/lspの違い
「vim lsp」で探すと、出てくる設定例の多くはNeovimのvim.lspです。NeovimはLSPクライアントを本体に持っていますが、Vimは9.1になっても持っていません。Vimで定義ジャンプや診断を使うには、LSPクライアントのプラグインを1つ選んで入れることになります。候補として今も手入れが続いているのは、prabirshrestha/vim-lspと、そのサーバー設定を肩代わりするmattn/vim-lsp-settingsの組み合わせ、そしてVim9 scriptで書かれたyegappan/lspの2系統です。
この記事では、両方を同じCのファイルとclangdで動かし、キーの割り当て・診断の出方・補完の3点で何が違うかを並べます。最後に、補完プラグインを足さずにVim 9.1の標準の自動補完(autocompleteオプション)へLSPの候補を流せるかも確かめました。結論から言うと、どちらも流せますが、vim-lspのほうはcompleteoptを直さないと入力が壊れます。
検証環境:Vim 9.1.1752(macOS、パッチ1-1752)、clangd は /usr/bin/clangd(Apple clangd 21.0.0)。プラグインは2026-10-13に取得したvim-lsp(8148119)・vim-lsp-settings(adbd969)・yegappan/lsp(58eac06)。一時ディレクトリをHOMEにしてtmuxの中でVimを起動し、14行のmain.c(わざとtotlという未宣言の変数を1つ入れたもの)を開いて確かめました。掲載している画面と出力はすべてその実行結果です。Linux・Windowsと、clangd以外のサーバーは試していません。
目次(9項目)
VimにはNeovimのような標準LSPが無い
NeovimはLSPクライアントを本体に内蔵していて、サーバーの起動コマンドを書いてvim.lsp.enable()を呼べば動きます(Neovimの標準LSPの設定で扱いました)。Vimの:helpにはLSPのクライアント機能が無く、本体にあるのはチャンネルとジョブ、それにLSPのメッセージ形式を読み書きするch_open()のlspモードまでです。サーバーを起動し、どのファイル種別にどのサーバーをつなぐか決め、返ってきた診断をサインや行内のテキストに描くのはプラグインの仕事になります。
この役を担うプラグインとして、ここでは次の2系統を比べます。
| 項目 | vim-lsp + vim-lsp-settings | yegappan/lsp |
|---|---|---|
| 書かれている言語 | Vim script(旧来の書き方) | Vim9 scriptだけ |
| 必要なVim | 8系でも動く | 9.0以上(plugin/lsp.vimの1行目でv:version < 900なら読み込まない) |
| サーバーの登録 | vim-lsp-settingsがファイル種別ごとの定義を持っている | 自分でLspAddServer()に書く |
| サーバーの導入 | :LspInstallServerで入れられる | 自分で入れる |
| 状態の確認 | :LspStatus | :LspShowAllServers |
どちらも、ふつうのプラグインと同じく~/.vim/pack/*/start/にgit cloneすれば読み込まれます。入れ方そのものはVimにプラグインを入れる方法にまとめてあるので、ここでは置き場所だけ書きます。:LspHoverのように同じ名前のコマンドを両方が定義しているので、試すときはどちらか一方にしてください。
# vim-lsp と vim-lsp-settings
git clone https://github.com/prabirshrestha/vim-lsp ~/.vim/pack/lsp/start/vim-lsp
git clone https://github.com/mattn/vim-lsp-settings ~/.vim/pack/lsp/start/vim-lsp-settings
# yegappan/lsp(こちらを使うなら上の2つは入れない)
git clone https://github.com/yegappan/lsp ~/.vim/pack/lsp/start/lsp
vim-lsp-settingsで:LspInstallServerを使う
vim-lspとvim-lsp-settingsを入れ、vimrcにはfiletype plugin indent onしか書かずにmain.cを開いたところ、何も設定していないのにclangdが起動し、未宣言のtotlに診断が付きました。vim-lsp-settingsが「C のファイルにはclangd」という定義を最初から持っていて、clangdがPATHにあればそのまま使うからです。
int main(void)
{
int total = add(1, 2);
E> printf("%d\n", totl);
Use of undeclared identifier 'totl'; did you mean 'total'? (fix available)
return 0;
}
動いているかは:LspStatusで見ます。返ってきたのはclangd: runningの1行でした。どの実行ファイルが使われているかは:LspStatusには出ないので、サーバー情報からコマンドを取り出して確かめました。
:echo call(lsp#get_server_info('clangd').cmd, [{}])
['/usr/bin/clangd']
PATHにclangdが無い環境だとどうなるかも見ておきます。PATH=/bin:/opt/homebrew/binに絞ってVimを起動すると、開いた直後に次の1行が出ました。
Please do :LspInstallServer to enable Language Server clangd
ここで:LspInstallServerを打つとInstall clangd ?と[Y]es, (C)ancel:で確認され、yでInstalled clangdと出て終わります。入った先は~/.local/share/vim-lsp-settings/servers/clangd/です。ただしmacOSで中を開くと、LLVMを丸ごと落としてきたわけではありませんでした。
# ~/.local/share/vim-lsp-settings/servers/clangd/clangd の中身
#!/bin/sh
exec xcrun --run clangd "$@"
インストーラ(installer/install-clangd.sh)は、xcrun --find clangdが通るMacではXcodeのclangdを呼ぶ3行のラッパーを置くだけです。それが無いときにPATHのclangdやclangd-18のような版付きの名前を探し、どれも無いときに初めてLLVMのリリースからダウンロードします。
入れたあとにPATHを元へ戻して開き直すと、使われるのは/usr/bin/clangdのままでした。lsp_settings#exec_path()はPATHを先に探し、見つからなかったときだけservers/の下を見る順番になっているためです。システムにclangdがあるなら:LspInstallServerは打たなくても動き、打っても使われるのはシステム側、ということになります。なお macOS の/usr/bin/clangdは実体ではなく、プロセスを見るとXcodeのツールチェーンの中のclangdが動いていました。
定義ジャンプとホバーのキーは自分で割り当てる
サーバーが動いても、vim-lspは通常モードのキーを何も変えません。起動直後に:verbose nmap gdと:verbose nmap Kを打つと、どちらもNo mapping foundでした。gdはVim本来の「関数内で宣言を探す」動き(変数の宣言位置へジャンプするgdとgD)、Kはkeywordprg=manのままで、omnifuncもCの標準のccomplete#Completeから変わっていません。LSPの機能はコマンドと<plug>のマッピングとして用意されていて、どのキーに付けるかは使う側が決める作りです。
READMEが勧めているのは、vim-lspがバッファにつながった瞬間に飛ぶUser lsp_buffer_enabledイベントで、そのバッファにだけマッピングを張る書き方です。LSPがつながらないファイルではgdもKも元の動きのまま残ります。
function! s:on_lsp_buffer_enabled() abort
setlocal omnifunc=lsp#complete
setlocal signcolumn=yes
nmap <buffer> gd <plug>(lsp-definition)
nmap <buffer> gr <plug>(lsp-references)
nmap <buffer> K <plug>(lsp-hover)
nmap <buffer> <leader>rn <plug>(lsp-rename)
nmap <buffer> [g <plug>(lsp-previous-diagnostic)
nmap <buffer> ]g <plug>(lsp-next-diagnostic)
endfunction
augroup lsp_install
autocmd!
autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END
これを入れて11行目のadd(1, 2)のaddでgdを押すと、カーソルは4行目5桁目(int add(int a, int b)のadd)へ移り、Retrieved definitionと出ました。同じ位置でKを押すと、関数の上に書いたコメントまで含めたポップアップが出ます。
┌─────────────────────────────────┐
│### function add │
│ │
│--- │
│→ int │
│Parameters: │
│- int a │
│- int b │
│ │
│2つの値を足す │
│ │
│--- │
└─────────────────────────────────┘
yegappan/lspも同じ考え方で、gdとKは起動直後にNo mapping found、omnifunc・tagfunc・keywordprgも元のままでした。こちらはUser LspAttachedイベントで<cmd>LspGotoDefinition<cr>などを張る例がヘルプ(:help lsp-quick-start)に載っています。キーを自分で決める点で2つに差はありません。
yegappan/lspにclangdを登録する
yegappan/lspにはvim-lsp-settingsに当たるものが無く、どのファイル種別にどのサーバーをつなぐかをvimrcに書きます。サーバーも自分で入れておく必要があり、ここではmacOSにもとからあるclangdをPATHから呼んでいます。
vim9script
filetype plugin indent on
syntax on
var lspServers = [{
name: 'clangd',
filetype: ['c', 'cpp'],
path: 'clangd',
args: ['--background-index'],
}]
autocmd User LspSetup g:LspAddServer(lspServers)
main.cを開いて:LspShowAllServersを打つと、分割したウィンドウに状態が出ました。filetypeにcppも書いたので、Cのファイルしか開いていなくても2つ並びます。
Filetype Information
====================
Filetype: 'c'
Server Name: 'clangd'
Server Path: 'clangd'
Status: Running
Filetype: 'cpp'
Server Name: 'clangd'
Server Path: 'clangd'
Status: Running
:LspGotoDefinitionで4行目5桁目へ移ること、:LspHoverでfunction add・引数・2つの値を足すのポップアップが出ることは、vim-lspと同じ結果でした。どちらもclangdの返事をそのまま表示しているので、中身は同じになります。
診断の表示の違いと切り替え
差がはっきり出たのは診断の見せ方です。同じmain.cを開いた直後の画面を比べると、vim-lspは行の下にメッセージを常に出し、yegappan/lspはサイン列にE>を付けるだけで本文には何も出しません。yegappan/lspのほうは11行目にもI>が付き、:LspDiag currentで見ると'total' declared hereという関連情報でした。
| 出し方 | vim-lsp(既定) | yegappan/lsp(既定) |
|---|---|---|
| サイン列 | E>。修正候補がある行にカーソルを置くとA>に変わる | E>、関連情報の行にI> |
| 行内のテキスト | 出る。行の下(below) | 出ない。有効にすると行の上(above) |
| カーソル位置のポップアップ | 出ない | :LspDiag currentで出る |
| 一覧 | :LspDocumentDiagnosticsでロケーションリスト | :LspDiag showでロケーションリスト |
vim-lspで行の下の表示が邪魔なら、g:lsp_diagnostics_virtual_text_enabledを0にします。代わりにg:lsp_diagnostics_float_cursorを1にすると、診断の範囲にカーソルを置いたときだけポップアップが出ます。試したときは、12行目のprintfの上ではポップアップが出ず、totlの上まで動かして初めて出ました。行単位ではなく、診断が指している桁の範囲で判定しているようです。
let g:lsp_diagnostics_virtual_text_enabled = 0
let g:lsp_diagnostics_float_cursor = 1
行内に出したまま位置だけ変えたいときはg:lsp_diagnostics_virtual_text_alignを'after'にすると、行末の続きに表示されます。画面幅を超える分は次の行へ折り返されました。
yegappan/lspで行内に出したいときは、LspOptionsSet()でshowDiagWithVirtualTextを有効にします。行の上に┌─付きのメッセージが差し込まれ、vim-lspとは上下が逆になります。
autocmd User LspSetup g:LspOptionsSet({showDiagWithVirtualText: true})
補完プラグイン無しで標準のautocompleteに流せるか
vim-lspの設定例では、補完にasyncomplete.vimを組み合わせるのが定番です。一方Vim 9.1.1752には、打っている途中で候補を出すautocompleteオプションが本体に入っています(Vim/Neovimの標準自動補完で扱いました)。候補の出どころはcompleteオプションで決まり、そこにoを入れるとomnifuncが候補の出どころに加わります。LSPのomnifuncをここへ流せば補完プラグインはいらないはずなので、両方で試しました。
vim-lsp:completeoptを直さないと入力が壊れる
上のlsp_buffer_enabledでomnifunc=lsp#completeを設定済みの状態で、:set autocompleteと:setlocal complete=.,oを打ち、空の行でsを押しました。clangdの候補が出たのはよいのですが、先頭のstatic_assertがそのまま行に挿入され、続けてtを押すとstatic_asserttになって補完が閉じました。打った文字が候補に食われる形で、これでは使えません。
原因はcompleteoptです。vim-lspのlsp#completeは候補を返すのではなく、サーバーの返事を待ってからcomplete()を自分で呼んで候補を出します。complete()はcompleteoptに従うので、Vimの既定値menu,previewのままだと先頭の候補を選んで挿入してしまいます。noselectを入れると直りました。
set autocomplete
set completeopt=menuone,noselect,popup
" omnifunc=lsp#complete は lsp_buffer_enabled の中で設定済み
autocmd FileType c setlocal complete=.,o
この設定でs、t、rと打つと、候補はstatic_assert・static・structからstructだけへ絞られていき、文字が勝手に挿入されることはなくなりました。もう1つ気づいたのは、バッファの中の単語が候補から消えることです。complete=.だけでtoと打つとtotlとtotalが出ますが、.,oにするとLSPが返したtotalだけになりました。complete()を後から呼ぶので、Vimが集めた候補を上書きしてしまうのだと読めます。
yegappan/lsp:既定でも、標準の補完に流しても動く
yegappan/lspは、何も設定しなくても入力中に候補を出します。autoCompleteというオプションが既定で有効で、TextChangedIのたびにサーバーへ問い合わせる自前の自動補完が動くためです。このときバッファのcompleteoptはmenuone,popup,noinsert,noselectに書き換えられていて、Vimのautocompleteは0のままでした。補完プラグインを足さなくてよい、という点では最初から満たしています。
標準のautocompleteに寄せたいときは、自前の補完を切ってomnifuncだけを使わせます。
autocmd User LspSetup g:LspOptionsSet({autoComplete: false, omniComplete: true})
autocmd User LspSetup g:LspAddServer(lspServers)
set autocomplete
set completeopt=menuone,noselect,popup
autocmd FileType c setlocal complete=.,o
開いて&l:omnifuncを見るとg:LspOmniFuncが入っていました。toと打った結果がこれです。
to
totl
total
total v int
•toascii(int _c) f int
•tolower(int) f int
•toupper(int) f int
バッファのtotl・totalのあとにclangdの候補が続き、種類(vは変数、fは関数)と型も付きます。vim-lspと違ってバッファの単語が消えないのは、g:LspOmniFuncが候補を返り値として渡し、Vimの側でまとめているからだと読めます。その代わりtotalは2か所から来るので2回並びます。complete=oにしてバッファの単語を外すと、候補はclangdのtotalからの1つだけになりました。
どちらを選ぶか
決め手になるのは、サーバーの用意を誰がやるかです。vim-lsp-settingsは言語ごとの定義を抱えていて、ファイルを開けば足りないサーバーを:LspInstallServerで案内してくれます。いろいろな言語のファイルを開く人、サーバーの起動コマンドを調べたくない人に向きます。yegappan/lspは1つずつLspAddServer()に書く必要がありますが、書いたもの以外は動かないので、何がどのファイルにつながっているかが全部vimrcで読めます。
補完まで含めて設定の量を比べると、yegappan/lspのほうが少なく済みます。自前の自動補完が既定で動き、completeoptも自分で整えてくれるからです。vim-lspで標準のautocompleteを使うなら、noselectを忘れないことと、バッファの単語が候補から消えることを受け入れる必要があります。VimのバージョンがVim 9.0より古い環境が残っているなら、選べるのはvim-lspのほうだけです。
yegappan/lspのヘルプにあるクイックスタートを、autocmd User LspSetup g:LspAddServer([{で始めて辞書を複数行に書く形のままvimrcへ写したところ、起動した瞬間にE492: Not an editor command: name: 'clangd',で止まりました。Vim9 scriptでもautocmdの後ろの式は次の行へ続けてくれないようで、手元の9.1.1752ではリストを先にvarへ入れてからautocmdで渡す形に直して通しました。もう1つ、vim-lspでstatic_asserttになったときは、しばらくclangdの候補の並び順を疑っていました。Vimのcompleteoptの既定値を見に行くまでに思ったより時間がかかっています。
まとめ
Vim 9.1には標準のLSPクライアントが無いので、vim-lsp+vim-lsp-settingsか、yegappan/lspを入れます。clangdでは両方とも定義ジャンプ・ホバー・診断が同じように動き、キーはどちらも自分で割り当てます。vim-lsp-settingsの:LspInstallServerはPATHにサーバーが無いときに案内され、PATHにあればそちらが優先されます。
補完プラグインは、どちらでも省けました。yegappan/lspは既定のままで自動補完が動き、omniCompleteに切り替えれば標準のautocompleteに流してバッファの単語とも混ぜられます。vim-lspでcompleteにoを入れるなら、completeoptにnoselectを入れないと候補が勝手に挿入されます。Neovimを使っているなら、これらを入れずにNeovimの標準LSPで済みます。