戻る

vim-lsp解説

Jan 3, 2021

LSPとはLanguage Server Protocolの略であり, 構文解析や補完機能などをエディタと分離しlspサーバーとしてエディタ(クライエント)と通信を行いエディタからの編集情報を元にlspサーバーは構文エラーや補完候補を求め, エディタにその結果を渡すというような通信の規格だ, この通信という言葉はローカルでの通信, つまり自分のパソコン内で完結している通信のことであり, アプリケーションからアプリケーションに情報を渡していくものであり決してhttp通信などのようなものではない. 従来は言語ごとに補完機能を実装する必要があったが, エディタ側はlspクライエントさえ実装すればほぼ全ての言語に対応できるようになった.

vim-lspはvimのLSPクライエントであり, 一番メジャーでありこれを使わない理由もない. インストールは各自の好きなプラグインマネージャーを使って簡単にできる.

しかしながら, vimにおいてLSP環境の導入は比較的ハードルが高い. vim-lspを取り巻くプラグインのドキュメントが貧弱なことに起因している. Vim をモダンな IDE に変える LSP の設定 ではサーバーをクライエントに登録するのが面倒だ, という理由で vim-lsp-settings を導入すれば簡単になる, と書いてはいるがそんなところよりも, lspクライエントが得た情報を使って実際に機能として実装したvimのプラグインをどのように, どれくらい導入すれば良いかだとか, プラグイン同士の衝突の心配などの方が強い.

vim-lsp自体が補完機能を持っているのか, それともvim-lspはバックエンドとして作動しているだけでフロントエンドには別のプラグインが必要なのか, そのプラグインはvim-lspとどのように連携するのか, など説明が悉く足りていない.

vimユーザーが求めているものは, vim-lsp-settingsのような隠蔽ツールではなく, 自分でエディタのカスタマイズをどれだけ理解して制御できるか, ということでありそれが嫌いならVSCodeという素晴らしい選択肢がある. vimscriptで色々設定を書くのが大変なのは, vimscriptのせいではなくプラグインのReadmeが説明不足であること, プラグインがLSPにおいてどの位置づけでの役割を持っているのか明示しないことに起因する.

neovimとLSP

LSPを語るにおいて, 今まで使われていたプラグインがどのような位置付けで役割を持っていたのか知る必要がある. その上でneovimについて語らざるを得ない.

従来のvimは非同期通信機能が標準で搭載されておらず, neovimがそれを売りにしていた. LSPという概念が普及する前からも, サーバーやクライエントにあたる概念がneovimのプラグインにはあった. そしてdeoplete.nvimというneovimプラグインが補完機能プラグインとして人気であった. これはLS(Language Source)が得た情報を実際に画面に表示するインターフェースであった. LSというのはLSPにおけるクライエントのようなものだったが, サーバーに依存した実装であり言語ごとにLSプラグインを用意する必要があった.

deoplete.nvimがやったことは補完情報から画面表示へのインターフェースを標準化することであった. LSPが登場する前はLSを標準化する術はなかった.

LSPが登場したことにより, LSが標準化され, LSPクライエントvim-lspが生まれた. vim-lspはvim8, neovim両方で使えるため人気となった. インターフェースを標準化するのではなく, クライエントを標準化することになったため, インターフェースであるdeoplete.nvimは逆にクライエントごとに合わせる立場となった. その結果deoplete-vim-lspという, vim-lspから得た情報をdeoplete.nvimに渡すためのプラグインが生まれた. そしてneovimではまた別にLanguageClient-neovimというクライエントが生まれたが, このクライエントはdeopleteに標準対応しているため新たなプラグインは必要ない. さらにneovimが標準でLSPクライエントを搭載したため, deoplete.nvimインターフェースに情報を渡すためにdeoplete-lspが生まれた. deoplete.nvimとはあくまでもneovimの補完インターフェースなのだ. vim-lspと並列する存在ではない.

vim-lspはneovimやvim8で使える. ただインターフェースが標準的でなくなったため各自色々集める必要があるが, 有名なのはasyncomplete.vim + asyncomplete-lsp.vim を使う組み合わせだったりする. asyncomplete.vimはdeoplete.nvimのような立ち位置であり, vim-lspから情報を得るために, asyncomplete-lsp.vimが必要となる. asyncomplete-lsp.vimはdeopleteにおけるdeoplete-vim-lspのような立ち位置である.

NeovimでモダンなPython環境を構築するv2(LSPを添えて) を参考に以下のテーブルを作成した. vimのLSPの解説としては最もわかりやすい. vimのLSPを語るにおいて, neovimは必ず言及されるべきだろう. ともかく, このサイトのテーブルを参考にすると以下のようなプラグインの関係がある.

LSPクライエント <-橋渡し> 補完機能のインターフェース
vim-lsp asyncomplete-lsp.vim asyncomplete.vim
vim-lsp deoplete-vim-lsp deoplete.nvim
coc.nvim 必要なし coc.nvim
LanguageClient-neovim 必要なし deoplete.nvim
neovimのビルトインLSP deoplete-lsp deoplete.nvim

vim-lspの導入

ともかく, vim-lspをインストールしたら次のような設定を書く.

vim-lspには使うサーバーを登録しなくてはならない. ともかく, https://github.com/prabirshrestha/vim-lsp/wiki/Servers の指示に従えば良いが, ここでは解説もいれる. 例えばGo言語のgoplsというLSPサーバーを登録する場合次のように書く. もちろん, goplsはvim-lspとは別にインストールしなくてはならないが, これも簡単にできる. 一般の言語はhttps://github.com/prabirshrestha/vim-lsp/wiki/Serversを参照すれば大体良いが, 少し内容が古いので参考程度にすること.


if executable('gopls')
    au User lsp_setup call lsp#register_server({
        \ 'name': 'gopls',
        \ 'cmd': {server_info->['gopls']},
        \ 'allowlist': ['go'],
        \ })
    autocmd BufWritePre *.go LspDocumentFormatSync
endif

vimscriptの基本的な構文ではあるが, au はautocmdの略で, この場合lsp_setupコマンドを定義している. このように au User {コマンド名} call {プラグイン固有の関数(なんらかの設定)} というような形式はよく見るので, 実際にはなんらかの設定の部分だけ気にすれば良い. このコマンドはvim-lspがenabledとなったときに呼ばれる. 'cmd'キーには関数か配列を対応させる. 関数の場合はserver_infoを引数として, executableを返す関数を渡す. 配列の場合は, 実行コマンドと引数を渡す. 結局のところは['']の中身を変えればいいだけ. allowlistは古い記事ではwhitelistとなっていることが多い. 元々whitelistだったのがallowlist, blacklistがblocklistに変わってしまった. autocmd BufWritePre *.go LspDocumentFormatSync とは, .goという拡張子のファイルを開いている場合のみ, BufWritePreイベント, つまり保存時にこのコマンドを発動し, 整形するが動作が重いという評判がある.

具体的にコマンドの割り当てなどを設定する. この設定はバッファーでvim-lspが有効になっているとき, つまり開いているファイルの拡張子に当てはまるような登録したサーバーがある場合に適用される. signcolumnというのは, これを有効にすると左の1列はsign、つまり警告サインやエラーサインなどを表示するために使われる. 警告はW(Warning), エラーはXというサインになっている.


function! s:on_lsp_buffer_enabled() abort
    setlocal omnifunc=lsp#complete
    setlocal signcolumn=yes
    if exists('+tagfunc') | setlocal tagfunc=lsp#tagfunc | endif
    nmap <buffer> gd <plug>(lsp-definition)
    nmap <buffer> gr <plug>(lsp-references)
    nmap <buffer> gi <plug>(lsp-implementation)
    nmap <buffer> gt <plug>(lsp-type-definition)
    nmap <buffer> <leader>rn <plug>(lsp-rename)
    nmap <buffer> [g <Plug>(lsp-previous-diagnostic)
    nmap <buffer> ]g <Plug>(lsp-next-diagnostic)
    nmap <buffer> K <plug>(lsp-hover)

    let g:lsp_format_sync_timeout = 1000
    autocmd! BufWritePre *.rs,*.go call execute('LspDocumentFormatSync')
    
    " refer to doc to add more commands
endfunction

augroup lsp_install
    au!
    " call s:on_lsp_buffer_enabled only for languages that has the server registered.
    autocmd User lsp_buffer_enabled call s:on_lsp_buffer_enabled()
augroup END

基本的にデフォルトで十分なのであまりオプションの値を設定することはないが, 次のようなオプションがある.

g:lsp_diagnostics_signs_enabled : サインを表示する. デフォルトは1. 古い記事だとlsp_signs_enabledのようにオプション名にdiagnosticsが入っていないものが多いので注意しよう.

g:lsp_diagnostics_virtual_text_insert_mode_enabled : エラーや警告の内容は該当箇所の右側に表示され, これをvirtual textという. 挿入モードでも表示するかというオプションであり, デフォルトでは0となっている. 挿入モードでは表示されなくなるが, これは編集中は一時的にエラーになるのは当たり前なので, 表示が鬱陶しくなることも多い.

g:lsp_diagnostics_echo_cursor : よくわからない.デフォルトは0.

<Plug>についてはVim の <Plug> ってなんだ? を読むとよくわかる.

vim-lsp周辺のプラグイン

vim-lspでできること でvim-lspのコマンドの機能がよくわかる. vim-lspはあくまでもクライエントであって, LSPの一部の機能しかインターフェースとして実装していない. vim-lspだけでは定義ジャンプやエラー, 警告表示などはできるが, 補完機能がないというように. 実はこれは意図的にそうしている. 補完機能がvim-lspに標準搭載されていない理由は, 補完機能のインターフェースをLSPサーバーを実装するまでもない簡易的な言語(アセンブリや, cmake, make)などに対しても使えるようにするためだ. 例えばdeoplete.nvimはLSP以外のインターフェースとしても使える, というか元々そうだった. asyncompleteもLSP以外のLS(Language Source)から情報を受け取れるインターフェースとなっている. vim-lspがインターフェースとして実装していないLSPの機能は別のインターフェースを対応させる必要がある. 具体的に, LSPのどの機能をvim-lspがインターフェースとして標準実装してどの機能を別のインターフェースに任せるのかは次の表のようになる.

vim-lspのクライエント機能 <-橋渡し-> インターフェースとして使うプラグインの例
定義ジャンプ 必要なし 必要なし
エラー, 警告診断 必要なし 必要なし
補完 asyncomplete-lsp.vim asyncomplete.vim
deoplete-vim-lsp deoplete.nvim
スニペット vim-vsnip-integ vim-vsnip
vim-lsp-ultisnips UltiSnips
vim-lsp-neosnippet neosnippet.vim
折り畳み(Folding) 必要なし 必要なし

各自好きなインターフェースを選んで, プラグインとその橋渡しのプラグインをインストールするだけで大体すむ.