2021/09/15

ddc.vimとBuiltin LSPでサブ武器を錬成した

以前はcoc.nvimを用いて開発環境を構築していたが、オールインワン系プラグインならではの過剰性能に思うところがあったのでリプレイスを図ることにした。というのも、CoCが提供する機能のうち僕が絶対に必要としているのはせいぜい下記の3つ程度だったからだ。

・自動補完
・LSP
・セレクタ

したがって、上記の機能を満たす単機能のプラグインをそれぞれ見繕えば当座の目的は達成できたことになる。僕にとってのVimは小回りの利くサブ武器なので、さしあたり一通りの編集作業がこなせる形に持っていければよいものとした。

ddc.vim

ddc.vimは自動補完を行うためのプラグインで、広く人気を集めたdeoplete.vimの後継にあたる。わずか数ヶ月前に公開されたニューフェイスながら既に実用可能なクオリティに達している。ただし仕様上、補完ソースやスニペットの類はすべて分離されているので、各要素の導入と併せてユーザ自らの手で設定しなければらない。

これは作者の言葉通り確かに初心者向けの作りではないものの、かえってそのミニマル志向が僕の使い方には合っていると感じた。さっそく以下から導入および設定例を示していくが、プラグイン管理にdein.vimを用いている都合上、記述内容はそれに則る形をとる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#dein_lazy.toml これらのプラグインはもっぱら遅延読み込みで運用する。
[[plugins]]
 repo = 'Shougo/ddc.vim'
 on_event = 'InsertEnter'
 depends = ['denops.vim']
 hook_source = '''
 call ddc#custom#patch_global('sources', ['nvim-lsp', 'around', 'vsnip'])
 call ddc#custom#patch_global('sourceOptions', {
      \ '_': {
      \ 'matchers': ['matcher_head'],
      \ 'sorters': ['sorter_rank'],
      \ 'converters': ['converter_remove_overlap'],
      \ },
      \ 'around': {'mark': 'A'},
      \ 'nvim-lsp': {
      \ 'mark': 'L',
      \ 'forceCompletionPattern': '\.\w*|:\w*|->\w*',
      \ },
      \ })

 call ddc#custom#patch_global('sourceParams', {
      \ 'around': {'maxSize': 500},
      \ })

 inoremap <silent><expr> <TAB>
      \ ddc#map#pum_visible() ? '<C-n>' :
      \ (col('.') <= 1 <Bar><Bar> getline('.')[col('.') - 2] =~# '\s') ?
      \ '<TAB>' : ddc#map#manual_complete()
 inoremap <expr><S-TAB>  ddc#map#pum_visible() ? '<C-p>' : '<C-h>'

 call ddc#enable()
'''

以上はddc.vimの設定として記述しているが、自動補完を働かせるには下記の補完ソースプラグインを別途要する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#dein_lazy.toml
[[plugins]]
 repo = 'Shougo/ddc-around'
 on_source = 'ddc.vim'

[[plugins]]
 repo = 'Shougo/ddc-matcher_head'
 on_source = 'ddc.vim'

[[plugins]]
 repo = 'Shougo/ddc-sorter_rank'
 on_source = 'ddc.vim'

[[plugins]]
 repo = 'Shougo/ddc-converter_remove_overlap'
 on_source = 'ddc.vim'

[[plugins]]
 repo = 'Shougo/ddc-nvim-lsp'
 on_source = 'ddc.vim'

[[plugins]]
 repo = 'hrsh7th/vim-vsnip'
 on_event = 'InsertEnter'
 depends = ['vim-vsnip-integ', 'friendly-snippets']
 hook_add = '''
 imap <expr> <C-j> vsnip#expandable() ? '<Plug>(vsnip-expand)' : '<C-j>'
 smap <expr> <C-j> vsnip#expandable() ? '<Plug>(vsnip-expand)' : '<C-j>'
 imap <expr> <C-f> vsnip#jumpable(1)  ? '<Plug>(vsnip-jump-next)' : '<C-f>'
 smap <expr> <C-f> vsnip#jumpable(1)  ? '<Plug>(vsnip-jump-next)' : '<C-f>'
 imap <expr> <C-b> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<C-b>'
 smap <expr> <C-b> vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<C-b>'
 let g:vsnip_filetypes = {}
 '''

[[plugins]]
 repo = 'hrsh7th/vim-vsnip-integ'

[[plugins]]
 repo = 'rafamadriz/friendly-snippets'

LSPを利用しないのであれば、この段階でとりあえずddc.vimを動作させることができる。実際に使ってみて、明示的に設定していない動作は一切行わない無骨さにかなりの好感を持った。後述のLSPに関しては入れ替えの余地もまだ残されているが、少なくとも自動補完プラグインはこのまま定住するつもりでいる。

ざっくばらんに各ソースの機能を説明すると、ddc-aroundはカーソル周辺の単語を検出するものでddc-matcher_headddc-sorter_rankが表示内容を決めるフィルタとして働いている。しかし、このままでは同じ単語を重複して補完してしまう恐れがあるためddc-converter_remove_overlapでそれを抑制している。

ddc-nvim-lspは言わずもがな、後述のNeovim Builtin LSPが提供する構文を引っ張ってくるソースだ。vim-vsnipと以降のスニペット群は補完を確定させた後の調整を上手くやってくれるプラグインで、これがないといわゆるtextEditに対応できない。

他にも多くのVimmerの手によって様々なソースが日々生み出されているが、誰にとっても入れておいて邪魔にならないソースは概ねこんなところだろう。

Neovim Builtin LSP

Builtin LSPとは名前の通り、Neovimの本体に組み込まれたLSPである。しかし動作させるには結局あれこれプラグインを導入したり設定しなければならないので、coc.nvimやvim-lspと比べると導入手順はむしろ面倒な部類に入る。Builtin LSPにもvim-lspにおけるvim-lsp-settingsのようなプラグイン(nvim-lspinstall)が存在するが、これもポン付けで全部よしなにやってくれるほど良心的ではない。肝心のLSPとしての品質もいささか荒削りな印象を受けた。

つまり現状、Neovimをメインでバリバリ使う人にとってわざわざ乗り換えるメリットは特にないと思われる。一応内蔵されている(Luaで書かれている)ということで実行速度に優れる利点はあるが、体感的にそこまで明瞭な差は感じられなかった。いずれは公式の強みを活かして競合を追い越す可能性も無きしもあらずとはいえ、今時分は個人の趣味性の範疇に留まると言わざるを得ない。僕がBuiltin LSPに乗り換えたのも将来性に期待して贔屓している部分が大きい。

■12月15日追記
前述のnvim-lspinstallはいつの間にか開発が終了していたのでnvim-lsp-installerに乗り換えた。コマンドに目立った差異はほとんど見られないが、Language Serverのインストール画面が多少グラフィカルになっていたり、バージョンを指定する機能(例::LspInstall [email protected])が追加されている。また、かつては対応が疎かだったWindows環境もフルサポートしているなど、およそ上位互換品と見て間違いないと考えられる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#dein_lazy.toml
[[plugins]]
 repo = 'neovim/nvim-lspconfig'
 on_event = 'BufEnter'
 hook_source = '''
 lua << EOF
 local nvim_lsp = require('lspconfig')
 local on_attach = function (client, bufnr)
 local function buf_set_keymap(...) vim.api.nvim_buf_set_keymap(bufnr, ...) end
 local function buf_set_option(...) vim.api.nvim_buf_set_option(bufnr, ...) end

local opts = { noremap=true, silent=true }
 buf_set_keymap('n', 'gd', '<Cmd>lua vim.lsp.buf.definition()<CR>', opts)
 buf_set_keymap('n', 'K', '<Cmd>lua vim.lsp.buf.hover()<CR>', opts)
 buf_set_keymap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<CR>', opts)
 buf_set_keymap('n', 'gs', '<cmd>lua vim.lsp.buf.signature_help()<CR>', opts)
 buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
 buf_set_keymap('n', 'gx', '<cmd>lua vim.lsp.diagnostic.show_line_diagnostics()<CR>', opts)
 buf_set_keymap('n', 'g[', '<cmd>lua vim.lsp.diagnostic.goto_prev()<CR>', opts)
 buf_set_keymap('n', 'g]', '<cmd>lua vim.lsp.diagnostic.goto_next()<CR>', opts)
end

 local lsp_installer = require("nvim-lsp-installer")
 lsp_installer.on_server_ready(function(server)
   local opts = {}
   opts.on_attach = on_attach
   server:setup(opts)
   vim.cmd [[ do User LspAttachBuffers ]]
 end)
EOF
'''

[[plugins]]
 repo ='williamboman/nvim-lsp-installer'
 on_source = 'nvim-lspconfig'

[[plugins]]
 repo = 'matsui54/denops-signature_help'
 on_source = 'ddc.vim'
 hook_source = '''
 call signature_help#enable()
'''

[[plugins]]
 repo = 'matsui54/denops-popup-preview.vim'
 on_source = 'ddc.vim'
 hook_source = '''
 call popup_preview#enable()
'''

導入後、対応ファイルを開くとLSPも連動して立ち上がる。プレビュープラグインのddc-nvim-lsp-docがIDEよろしく補完候補の詳細情報を提供してくれるのでかなり心強い。Language Serverごとの細かい設定はまだ定まっていないので本エントリでは割愛させていただく。LSPのインストール情報は:LspInfoで確認できる。

■2022年1月3日追記
前述のddc-nvim-lsp-docは更新が停止され、新規プラグインのdenops-signature_helpdenops-popup-previewに置き換えられた。この変更に倣って上記の設定例も既に書き換えている。実装手法は異なるが機能面にほとんど差はないためさっさと乗り換えた方がよい。

セレクタ

セレクタとはファイルや文字列の絞り込みを行うためのインターフェイスを提供するプラグインだ。中でもfzf.vimは特に高速かつ多機能なことで知られている。Yuki Yano氏が開発したfzf-preview.vimというさらに機能面に秀でたオールインワン版もあるが、あくまで僕はサブ武器的文脈に従ってfzf.vimの調整に留めている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#dein.toml
[[plugins]]
 repo = 'junegunn/fzf'
 merged = 0
 build = '''
 ./install --all
'''

[[plugins]]
 repo = 'junegunn/fzf.vim'
 hook_add = '''
 nnoremap <silent> <Leader>. :<C-u>FZFFileList<CR>
 nnoremap <silent> <Leader>, :<C-u>FZFMru<CR>
 nnoremap <silent> <Leader>l :<C-u>Lines<CR>
 nnoremap <silent> <Leader>b :<C-u>Buffers<CR>
 nnoremap <silent> <Leader>k :<C-u>Rg<CR>
 command! FZFFileList call fzf#run({
            \ 'source': 'rg --files --hidden',
            \ 'sink': 'e',
            \ 'options': '-m --border=none',
            \ 'down': '20%'})
 command! FZFMru call fzf#run({
            \ 'source': v:oldfiles,
            \ 'sink': 'e',
            \ 'options': '-m +s --border=none',
            \ 'down':  '20%'})

 let g:fzf_layout = {'up':'~90%', 'window': { 'width': 0.8, 'height': 0.8,'yoffset':0.5,'xoffset': 0.5, 'border': 'none' } }

 augroup vimrc_fzf
 autocmd!
 autocmd FileType fzf tnoremap <silent> <buffer> <Esc> <C-g>
 autocmd FileType fzf set laststatus=0 noshowmode noruler
      \| autocmd BufLeave <buffer> set laststatus=2 noshowmode ruler
 augroup END

 function! RipgrepFzf(query, fullscreen)
    let command_fmt = 'rg --column --hiddden --line-number --no-heading --color=always --smart-case %s || true'
    let initial_command = printf(command_fmt, shellescape(a:query))
    let reload_command = printf(command_fmt, '{q}')
    let spec = {'options': ['--phony', '--query', a:query, '--bind', 'change:reload:'.reload_command]}
    call fzf#vim#grep(initial_command, 1, fzf#vim#with_preview(spec), a:fullscreen)
 endfunction

 command! -nargs=* -bang RG call RipgrepFzf(<q-args>, <bang>0)
'''

そうすると、こんな感じのセレクタを下から生やせる。たとえ候補が30万件あっても重さを一切知覚させないのは頼もしい。当初はTab補完が欲しいと考えもしたがそこはやはり天下のfzf。期待以上に雑なタイプで目当てのファイルを引っかけられるため特に必要なかった。よって、このケースでのTabキーは複数選択にあてがわれている。なお、LinesとRgコマンドはプレビューの必要性からfloating windowで表示させている。

極めつけはBuffersソースの存在だ。以前の僕はタブで管理をやりくりしようと考えていたが、バッファを明瞭に一覧化できるのならあえて依存せずともよい。表示したい箇所のテキストが予め分かっていればLinesソースを駆使して瞬時にジャンプすることもできる。

トラブルシューティング(順次追記)

Q1. 遅延読み込みさせているプラグインが動かない。
A1. init.vimや.vimrcに遅延読み込みの記述をしていない可能性がある。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 #init.vim
 " .tomlファイルの場所
  let s:rc_dir = expand('~/.config/nvim/')
  if !isdirectory(s:rc_dir)
    call mkdir(s:rc_dir, 'p')
  endif
  let s:toml = s:rc_dir . '/dein.toml'
  let s:lazy_toml = s:rc_dir . '/dein_lazy.toml'

 " .tomlファイルを読み込む
 call dein#load_toml(s:toml, {'lazy': 0})
 call dein#load_toml(s:lazy_toml, {'lazy': 1})

上記の例の通りlazyが1に設定されていないtomlファイルは遅延読み込みを行わない。また、遅延読み込みが無効のtomlファイルでhook_sourceのようなオプションを記述しても、プラグインは起動しない。

Q2. Language Serverの導入・削除方法が分からない。
A2. 基本的には:LspInstall LSの名称でインストールされる。たとえば:LspInstall goplsでGoのLanguage Serverが入る。逆に削除したい時はLspUninstall LSの名称で行える。LspUninstallAllですべてのLSを一括して削除することもできる。

Q3. tomlファイルにLuaの記述を加えたらむっちゃ怒られた。
A3. ほとんどの場合はEOFのインデントをミスっている。余計なスペースを削って行頭に置くと直る。

おわりに

本件に伴ってプラグインの整理や管理方法の改善を実施した結果、時には300ms近くかかっていたNeovimの起動速度が100ms程度まで減少した。人間の単純反応速度に近い値なのでなかなか悪くないと思う。あえてオールインワン系の仕組みから一旦距離をとってみると、自分が真に必要としている機能が判ってくる。この考え方はVimのみならず、VSCodeやその他ツールを設定する上でもなにかと役に立つ。

ひとまずサブ武器としてのVimを錬成したところで、以降はより実戦に適した形状に刃先を尖らせていくことになる。だが、サブ武器だからといって戦闘能力が低いとは限らない。世界観によってはむしろ短剣類の方がハマれば高威力だったりする。僕にとってのVimもそのようなものだと信じている。

参考文献

ddc.vimのlsp機能を強くする with nvim-lsp
Neovim builtin LSP設定入門

©2011 Rikuoh Tsujitani | Twitter | RSS | 小説