2021/09/15

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

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

・自動補完
・LSP
・MRU

したがって、上記の機能を満たす単機能のプラグインをそれぞれ見繕えば当座の目的は達成できたことになる。僕にとっての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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#dein_lazy.toml これらのプラグインはもっぱら遅延読み込みで運用する。
 [[plugins]]
 repo = 'Shougo/ddc.vim'
 on_event = 'InsertEnter'
 hook_source = '''
 inoremap <silent><expr> <TAB>
      \ pumvisible() ? '<C-n>' :
      \ (col('.') <= 1 <Bar><Bar> getline('.')[col('.') - 2] =~# '\s') ?
      \ '<TAB>' : ddc#manual_complete()
 inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>"

 call ddc#custom#patch_global('sources', ['nvim-lsp', 'around', 'vsnip', 'file'])
 call ddc#custom#patch_global('sourceOptions', {
      \ '_': {
        \   'matchers': ['matcher_head'],
        \   'sorters': ['sorter_rank'],
        \ },
        \ 'around': {'mark': 'A'},
        \ 'file': {
        \   'mark': 'F',
        \   'isVolatile': v:true,
        \   'forceCompletionPattern': '\S/\S*',
        \ },
        \ 'nvim-lsp': {
        \ 'mark': 'LSP',
        \ 'forceCompletionPattern': '\.\w*|:\w*|->\w*',
        \ },
        \ })

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

 call ddc#custom#patch_filetype(
    \ ['ps1', 'dosbatch', 'autohotkey', 'registry'], {
    \ 'sourceOptions': {
    \   'file': {
    \     'forceCompletionPattern': '\S\\\S*',
    \   },
    \ },
    \ 'sourceParams': {
    \   'file': {
    \     'mode': 'win32',
    \   },
    \ }})

 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
#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 = 'LumaKernel/ddc-file'
 on_source = 'ddc.vim'

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

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

[[plugins]]
 repo = 'hrsh7th/vim-vsnip'
 on_source = 'ddc.vim'

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

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に乗り換えたのも将来性に期待して贔屓している部分が大きい。

 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
50
51
52
53
54
55
56
57
#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

 require'lspconfig'.tsserver.setup{}
 require'lspconfig'.solargraph.setup{}

 require'lspinstall'.setup()
 local servers = require'lspinstall'.installed_servers()
 for _, server in pairs(servers) do
  require'lspconfig'[server].setup{}
 end
EOF
'''

[[plugins]]
 repo = 'kabouzeid/nvim-lspinstall'
 on_source = 'nvim-lspconfig'
 hook_source = '''
  lua require'lspinstall'.setup()
'''

[[plugins]]
 repo = 'matsui54/ddc-nvim-lsp-doc'
 on_source = 'ddc.vim'
 hook_source = '''
 let g:ddc_nvim_lsp_doc_config = {
      \ 'documentation': {
      \   'enable': v:true,
      \   'border': 'single',
      \   'maxWidth': 60,
      \   'maxHeight': 30,
      \ },
      \ 'signature': {
      \   'maxHeight': 5,
      \ },
      \ }
  call ddc_nvim_lsp_doc#enable()
'''

導入後、対応ファイルを開くとLSPも連動して立ち上がる。詳細は:Lspinfoで確認できる。Language Serverごとの細かい設定はまだ定まっていないので本エントリでは割愛させていただく。

MRU

MRU(Most Recently Used)とは以前に開いたファイルを呼び出すための機能を指す。僕はfzf本体とfzf-mru.vimというプラグインを組み合わせることで手っ取り早く実現させている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#dein.toml こういったプラグインは遅延読み込みでは困る。
[[plugins]]
 repo = 'junegunn/fzf'
 build = '''
 ./install --all
'''
 hook_add = '''
 nnoremap <silent> <Leader>.   :FZF<CR>
'''

[[plugins]]
 repo = 'pbogut/fzf-mru.vim'
 hook_add = '''
 nnoremap <silent> <Leader>,   :FZFMru<CR>
'''

作業内容の大半が編集なので今のところはこれで事足りている。もう少し欲が出てきたらちゃんとしたFuzzy Finder系のプラグインを導入するかもしれない。

欲が出た場合

実を言うとctrlp.vimの導入を検討している。初出が10年近くも前ということもあり、流行りのfloating windowやpopupを駆使した競合プラグインと比べると質素なUIだが、その代わり幾分シンプルに作られている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#dein.toml
[[plugins]]
 repo = "ctrlpvim/ctrlp.vim"
 hook_add = '''
 if executable('rg')
  set grepprg=rg\ --vimgrep\ --no-heading
  set grepformat=%f:%l:%c:%m,%f:%l:%m
  let g:ctrlp_user_command = 'rg --files %s'
  let g:ctrlp_use_caching = 0
  let g:ctrlp_working_path_mode = 'ra'
  let g:ctrlp_switch_buffer = 'et'
 endif
 nnoremap <Leader>, :<C-u>CtrlPMRUFiles<CR>
 nnoremap <Leader>. :<C-u>CtrlPMixed<CR>
 let g:ctrlp_map = '<Nop>'
 let g:ctrlp_open_new_file = 'r'
 let g:ctrlp_extensions = ['line', 'mixed']
 let g:ctrlp_match_window = 'bottom,order:btt,min:1,max:10'
'''

せめてもの工夫としてripgrepと連携させて絞り込み速度を高速化した。挙動がとても素直で好ましく、通常の利用では懸念していたほど遅くもない。古いプラグインと侮って選択肢から除外したのは誤りだった。しばらく使ってみて十分に機能を扱いきれそうなら移行する。

もっと欲が出た場合

他方、ctrlp.vimの問題点はhiddenフラグ(不可視ファイルを含める)をつけて検索すると途端に重くなることだ。ホームディレクトリからそんな真似をする方が悪いといえばそうだが、かの有名なfzf.vimはこのような極端な状況下でもまったく重くならない。fzfのカスタマイズがサブ武器的文脈から外れているかどうかは議論が待たれる。しかし一応、ミニマルっぽく仕上げた設定を書いたので紹介したい。

 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
#dein.toml
[[plugins]]
 repo = 'junegunn/fzf'
 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>BLines<CR>
 nnoremap <silent> <Leader>k :<C-u>Rg<CR>
 command! FZFFileList call fzf#run({
            \ 'source': 'rg --files --hidden',
            \ 'sink': 'tabedit',
            \ 'options': '-m --border=none',
            \ 'down': '20%'})
 command! FZFMru call fzf#run({
            \ 'source': v:oldfiles,
            \ 'sink': 'tabedit',
            \ '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万件あっても重さを一切知覚させないのは頼もしい。当初はctrlp.vimのTab補完と同様のものが欲しいと考えもしたがそこはやはり天下のfzf。期待以上に雑なタイプで目当てのファイルを引っかけられるため特に必要なかった。よって、このケースでのTabキーは複数選択にあてがわれている。なお、BLinesとRgコマンドはプレビューの必要性からfloating windowで表示させている。

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

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 言語でインストールされる。たとえば:LspInstall goでGoのLanguage Serverが入る。ただしSolargraphやtsserverのようになぜか入手できないサーバもある。

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

おわりに

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

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

参考文献

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

©2011 Rikuoh Tsujitani | Twitter | RSS | 小説