Open13

coc.nvim -> nvim-lsp移行プロジェクト

kawarimidollkawarimidoll

追記:以下の動機で移行を考え始めたけどそもそも競合せず使える

skkeletonを使うためにddc.vimを入れたらcoc.nvimと競合したっぽい
https://twitter.com/KawarimiDoll/status/1469255067731644424

skkeletonの入力体験が良いことがわかったので、これを機にcoc.nvimからddc.vim x nvim-lspに全面移行したい

こんな感じで設定はしていたんだけどな…cocがddcを邪魔しなくなるのは良かったんだけどcocが出なくなってしまったのよね
https://twitter.com/KawarimiDoll/status/1469471826665689089

kawarimidollkawarimidoll

https://github.com/neovim/nvim-lspconfig
https://github.com/williamboman/nvim-lsp-installer

READMEを参考にして最低限の設定はこんな感じ

init.vim
" 読み込み(vim-plug)
call plug#begin(stdpath('config') . '/plugged')
Plug 'neovim/nvim-lspconfig'
Plug 'williamboman/nvim-lsp-installer'
call plug#end()

lua << EOF
-- READMEを参考
local nvim_lsp = require('lspconfig')

-- language serverがバッファにアタッチされたときに実行する関数
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

  -- <c-x><c-o>による補完
  buf_set_option('omnifunc', 'v:lua.vim.lsp.omnifunc')

  -- map用オプション
  local opts = { noremap=true, silent=true }

  -- `vim.lsp.*`関数のマッピング(多いので省略)
  buf_set_keymap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<CR>', opts)
  buf_set_keymap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<CR>', opts)
end

-- lsp_installerの設定
local lsp_installer = require("nvim-lsp-installer")

-- デフォルトアイコンが見分けつかないので設定
-- その他のデフォルト設定は[READMEを参照](https://github.com/williamboman/nvim-lsp-installer#default-configuration)
lsp_installer.settings({
  ui = {
    icons = {
      server_installed = "✓",
      server_pending = "➜",
      server_uninstalled = "✗"
    }
  }
})

-- サーバー起動時に自動でon_attachをアタッチする
lsp_installer.on_server_ready(function(server)
  local opts = {}
  opts.on_attach = on_attach

  server:setup(opts)
  vim.cmd [[ do User LspAttachBuffers ]]
end)
EOF

これで:LspInstall {server-name}でlanguage serverをインストールできるようになる

kawarimidollkawarimidoll

個人的に重要なポイント Denoプロジェクトではdenolsを有効にし、tsserverとeslintを停止する
lsp_installer.on_server_readyに設定を追加

 lsp_installer.on_server_ready(function(server)
   local opts = {}
   opts.on_attach = on_attach

+  if server.name == 'tsserver' or server.name == 'eslint' then
+    opts.root_dir = nvim_lsp.util.root_pattern("package.json")
+  elseif server.name == 'denols' then
+    opts.root_dir = nvim_lsp.util.root_pattern("deno.json", "deno.jsonc", "deps.ts")
+    opts.init_options = { lint = true, unstable = true, }
+  end

   server:setup(opts)
   vim.cmd [[ do User LspAttachBuffers ]]
 end)
kawarimidollkawarimidoll

ddcの追加

https://github.com/Shougo/ddc.vim
https://github.com/Shougo/ddc-nvim-lsp
https://github.com/matsui54/denops-popup-preview.vim
https://github.com/ray-x/lsp_signature.nvim

init.vim
Plug 'vim-denops/denops.vim'

Plug 'Shougo/ddc.vim'
Plug 'Shougo/ddc-around'
Plug 'Shougo/ddc-matcher_head'
Plug 'Shougo/ddc-matcher_length'
Plug 'Shougo/ddc-sorter_rank'
Plug 'Shougo/ddc-nvim-lsp'

Plug 'matsui54/denops-popup-preview.vim'
Plug 'ray-x/lsp_signature.nvim'

" 略

" lspを表示だけするならaroundは必須ではないけど基礎的なsourceなので記載
call ddc#custom#patch_global('sources', ['nvim-lsp', 'around'])
call ddc#custom#patch_global('sourceOptions', #{
  \   _: #{
  \     ignoreCase: v:true,
  \     matchers: ['matcher_head'],
  \     sorters: ['sorter_rank'],
  \   },
  \   around: #{
  \     mark: 'A',
  \     matchers: ['matcher_head', 'matcher_length'],
  \   },
  \   nvim-lsp: #{
  \     mark: 'lsp',
  \     forceCompletionPattern: '\.\w*|:\w*|->\w*',
  \   },
  \ })

" ちょっとここの設定の意味はよくわからない
call ddc#custom#patch_global('sourceParams', #{
  \ nvim-lsp: #{ maxSize: 500, kindLabels: #{ Class: 'c' } },
  \ })

" 明示的に起動が必要(忘れがち)
call ddc#enable()
call popup_preview#enable()

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

lua require("lsp_signature").setup()

これで補完が出るはず
https://twitter.com/KawarimiDoll/status/1470012738600861697

kawarimidollkawarimidoll

設定して早々ではあるけどddc-fuzzyへ移行しよう
https://github.com/tani/ddc-fuzzy

プラグインを読み込んで、patch_global('sourceOptions'...)をddc-fuzzyを使うように修正、キーマップをpumを使うように修正

init.vim
Plug 'Shougo/pum.vim'
Plug 'tani/ddc-fuzzy'

" 略

call ddc#custom#patch_global('completionMenu', 'pum.vim')
call ddc#custom#patch_global('sourceOptions', #{
  \   _: #{
  \     ignoreCase: v:true,
  \     matchers: ['matcher_fuzzy'],
  \     sorters: ['sorter_fuzzy'],
  \     converters: ['converter_fuzzy']
  \   },
  \   around: #{ mark: 'A' },
  \   nvim-lsp: #{
  \     mark: 'lsp',
  \     forceCompletionPattern: '\.\w*|:\w*|->\w*',
  \   },
  \ })

" <TAB>/<S-TAB> completion. ここはddcのtab補完設定なのでpum.vimを使うなら不要
" inoremap <silent><expr> <TAB>
"   \ pumvisible() ? '<C-n>' :
"   \ (col('.') <= 1 <Bar><Bar> getline('.')[col('.') - 2] =~# '\s') ?
"   \ '<TAB>' : ddc#map#manual_complete()
" inoremap <expr><S-TAB>  pumvisible() ? '<C-p>' : '<C-h>'

inoremap <Tab>   <Cmd>call pum#map#insert_relative(+1)<CR>
inoremap <S-Tab> <Cmd>call pum#map#insert_relative(-1)<CR>
inoremap <C-n>   <Cmd>call pum#map#insert_relative(+1)<CR>
inoremap <C-p>   <Cmd>call pum#map#insert_relative(-1)<CR>
inoremap <C-y>   <Cmd>call pum#map#confirm()<CR>
inoremap <C-e>   <Cmd>call pum#map#cancel()<CR>
inoremap <PageDown> <Cmd>call pum#map#insert_relative_page(+1)<CR>
inoremap <PageUp>   <Cmd>call pum#map#insert_relative_page(-1)<CR>
kawarimidollkawarimidoll

snippetの設定
https://github.com/hrsh7th/vim-vsnip
https://github.com/hrsh7th/vim-vsnip-integ

各サーバーにcapabilitiesを設定する
https://github.com/hrsh7th/vim-vsnip-integ/issues/36

init.vim
Plug 'hrsh7th/vim-vsnip'
Plug 'hrsh7th/vim-vsnip-integ'

" 略

" ddcのsourceにvsnip追加
call ddc#custom#patch_global('sources', ['nvim-lsp', 'vsnip', 'around'])
call ddc#custom#patch_global('sourceOptions', #{
  \  " ...
  \   vsnip: #{
  \     mark: 'VS',
  \     dup: v:true,
  \   },
  \  " ...
  \ })

" Tab / S-Tab / C-n / C-pのマッピングを変更、autocmd追加
imap <silent><expr> <TAB>   pum#visible() ? '<Cmd>call pum#map#insert_relative(+1)<CR>' : vsnip#jumpable(+1) ? '<Plug>(vsnip-jump-next)' : '<TAB>'
imap <silent><expr> <S-TAB> pum#visible() ? '<Cmd>call pum#map#insert_relative(-1)<CR>' : vsnip#jumpable(-1) ? '<Plug>(vsnip-jump-prev)' : '<S-TAB>'
imap <silent><expr> <C-n>   (pum#visible() ? '' : '<Cmd>call ddc#map#manual_complete()<CR>') . '<Cmd>call pum#map#select_relative(+1)<CR>'
imap <silent><expr> <C-p>   (pum#visible() ? '' : '<Cmd>call ddc#map#manual_complete()<CR>') . '<Cmd>call pum#map#select_relative(-1)<CR>'
autocmd User PumCompleteDone call vsnip_integ#on_complete_done(g:pum#completed_item)

lua << EOF
local nvim_lsp = require('lspconfig')
local on_attach = function(client, bufnr)
  -- 略
end

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

local lsp_installer = require("nvim-lsp-installer")
-- iconとかの設定は省略
lsp_installer.on_server_ready(function(server)
  local opts = {}
  opts.on_attach = on_attach
  opts.capabilities = capabilities

  -- tsserverとかdenolsとかの部分は省略

  server:setup(opts)
  vim.cmd [[ do User LspAttachBuffers ]]
end)
EOF

gamoutatsumiさんのdofilesを参考にした
https://github.com/gamoutatsumi/dotfiles/blob/master/.config/nvim/ddc.toml

これでこの記事のAuto snippet的なのができるようになると思ったんだけど…動かないな
https://zenn.dev/matsui54/articles/2021-09-03-ddc-lsp#スニペット

kawarimidollkawarimidoll

先にcmdlineのコンプリートを設定
ddcのドキュメントにpum.vimを使ってコマンドラインの補完をやるサンプルが載っている
あと例によって前出のdotfilesも参考にする

init.vim
Plug 'Shougo/neco-vim'
Plug 'Shougo/ddc-cmdline-history'

" 略

cnoremap <expr> <TAB>   pum#visible() ? '<Cmd>call pum#map#insert_relative(+1)<CR>' : ddc#map#manual_complete()
cnoremap <expr> <S-TAB> pum#visible() ? '<Cmd>call pum#map#insert_relative(-1)<CR>' : ddc#map#manual_complete()
cnoremap <expr> <C-n>   pum#visible() ? '<Cmd>call pum#map#insert_relative(+1)<CR>' : '<C-n>'
cnoremap <expr> <C-p>   pum#visible() ? '<Cmd>call pum#map#insert_relative(-1)<CR>' : '<C-p>'
cnoremap <expr> <CR>    pum#visible() ? '<Cmd>call pum#map#confirm()<CR>' : '<CR>'
" cnoremap <C-y>   <Cmd>call pum#map#confirm()<CR>
" cnoremap <C-e>   <Cmd>call pum#map#cancel()<CR>
nnoremap :       <Cmd>call CommandlinePre()<CR>:

function! CommandlinePre() abort
  " Overwrite sources
  let s:prev_buffer_config = ddc#custom#get_buffer()
  call ddc#custom#patch_buffer('sources', ['necovim', 'cmdline-history'])
  call ddc#custom#patch_buffer('autoCompleteEvents', ['CmdlineChanged'])
  call ddc#custom#patch_buffer('sourceOptions', #{
    \   _:  #{
    \    ignoreCase: v:true,
    \    matchers:   ['matcher_fuzzy'],
    \    sorters:    ['sorter_fuzzy'],
    \    converters: ['converter_fuzzy']
    \   },
    \   necovim: #{ mark: 'neco' },
    \   cmdline-history: #{ mark: 'hist' },
    \ })

  autocmd CmdlineLeave ++once call CommandlinePost()

  " Enable command line completion
  call ddc#enable_cmdline_completion()
endfunction
function! CommandlinePost() abort
  " Restore sources
  call ddc#custom#set_buffer(s:prev_buffer_config)
endfunction

https://twitter.com/KawarimiDoll/status/1470360073486876674

kawarimidollkawarimidoll

Deno/Nodeの出し分けの箇所、修正
前述の設定だとtsserverは起動しないものの、起動失敗メッセージが出てしまっていた
root_dirはあくまでプロジェクトルートを定義するものであり、起動可否はautostartに真偽値で指定する必要がある
したがって、root_dirで指定されたファイルが見つかった場合のみサーバーを起動させる設定を書く
root_dirがどのように実行されているかはnvim-lspのコードを参考にした

lua part in init.vim
local nvim_lsp = require("lspconfig")
local lsp_installer = require("nvim-lsp-installer")
function detected_root_dir(root_dir)
  return not(not(root_dir(vim.api.nvim_buf_get_name(0), vim.api.nvim_get_current_buf())))
end
lsp_installer.on_server_ready(function(server)
  local opts = {}
  opts.on_attach = on_attach
  opts.capabilities = capabilities

  if server.name == 'tsserver' or server.name == 'eslint' then
    local root_dir = nvim_lsp.util.root_pattern("package.json", "node_modules")
    opts.root_dir = root_dir
    opts.autostart = detected_root_dir(root_dir)
  elseif server.name == 'denols' then
    local root_dir = nvim_lsp.util.root_pattern("deno.json", "deno.jsonc", "deps.ts")
    opts.root_dir = root_dir
    opts.autostart = detected_root_dir(root_dir)
    opts.init_options = { lint = true, unstable = true, }
  end

  server:setup(opts)
  vim.cmd [[ do User LspAttachBuffers ]]
end)

nvim_lsp.util.root_pattern()は指定のファイルが存在すればそのパスを、なければnilを返すが、nvim-lsp内部ではautostart==trueで判断しているので、not(not())で囲んで明示的にtrue/falseに変換する必要がある

これで.ts .tsxなどを開いたとき、以下のように選択的にサーバーを立ち上げることができる

  • "package.json", "node_modules"のあるディレクトリ以下ではtsservereslintが立ち上がる
  • "deno.json", "deno.jsonc", "deps.ts"のあるディレクトリ以下ではdenolsが立ち上がる

条件に使っているファイルが両方存在してしまうとどちらのサーバーも起動してしまうが…一旦考えないこととする

kawarimidollkawarimidoll

on-attachするとdenolsでマッピングがつかない?気がしたので外に出した

init.vim
nnoremap <expr> K '<Cmd>' . (['vim','help']->index(&filetype) >= 0 ? 'help ' . expand('<cword>') : 'lua vim.lsp.buf.hover()') . '<CR>'
nnoremap gD        <cmd>lua vim.lsp.buf.declaration()<CR>
nnoremap gd        <cmd>lua vim.lsp.buf.definition()<CR>
nnoremap gi        <cmd>lua vim.lsp.buf.implementation()<CR>
nnoremap gr        <cmd>lua vim.lsp.buf.references()<CR>
nnoremap <C-k>     <cmd>lua vim.lsp.buf.signature_help()<CR>
nnoremap <space>wa <cmd>lua vim.lsp.buf.add_workspace_folder()<CR>
nnoremap <space>wr <cmd>lua vim.lsp.buf.remove_workspace_folder()<CR>
nnoremap <space>wl <cmd>lua print(vim.inspect(vim.lsp.buf.list_workspace_folders()))<CR>
nnoremap <space>D  <cmd>lua vim.lsp.buf.type_definition()<CR>
nnoremap <space>rn <cmd>lua vim.lsp.buf.rename()<CR>
nnoremap <space>ca <cmd>lua vim.lsp.buf.code_action()<CR>
nnoremap <space>e  <cmd>lua vim.diagnostic.open_float()<CR>
nnoremap [g        <cmd>lua vim.diagnostic.goto_prev()<CR>
nnoremap ]g        <cmd>lua vim.diagnostic.goto_next()<CR>
nnoremap sl        <cmd>lua vim.diagnostic.setloclist()<CR>
nnoremap <space>p  <cmd>lua vim.lsp.buf.formatting()<CR>

あとvim/helpファイルではKが強制的にヘルプを見るよう修正(vimlではhelp参照にならないっぽい)

kawarimidollkawarimidoll

vim-jpで質問したところvsnipの設定の問題とかではなさそう
言語サーバーによって対応している機能は異なるため一律にスニペットが使えるというわけではない

kawarimidollkawarimidoll

なんだかんだ言ってcoc.nvimとddc x skkeletonの共存できた

参考:
https://gist.github.com/yuki-yano/d2197be559841d0aeaf91344fde60b54

設定:

.vimrc or init.vim
imap <C-j> <Plug>(skkeleton-enable)
cmap <C-j> <Plug>(skkeleton-enable)

call ddc#custom#patch_global('sources', ['skkeleton'])
call ddc#custom#patch_global('sourceOptions', #{
  \   skkeleton: #{
  \     matchers: ['skkeleton'],
  \     minAutoCompleteLength: 1,
  \   },
  \ })
call skkeleton#config(#{
  \   eggLikeNewline: v:true,
  \   globalJisyo: "path/to/jisyo",
  \ })

function s:enable_ddc() abort
  let b:coc_suggest_disable = v:true
  call ddc#custom#patch_global('autoCompleteEvents',
    \ ['TextChangedI', 'TextChangedP', 'CmdlineChanged'])
endfunction

function s:disable_ddc() abort
  let b:coc_suggest_disable = v:false
  call ddc#custom#patch_global('autoCompleteEvents', [])
endfunction

" initialize
call <sid>disable_ddc()

augroup skkeleton
  autocmd!
  autocmd User skkeleton-enable-pre  call <sid>enable_ddc()
  autocmd User skkeleton-disable-pre call <sid>disable_ddc()
augroup END

単に2つのオートコンプリーターをトグルすれば良い

  • skkeletonが有効になったとき→cocを無効化、ddcを有効化
  • skkeletonが無効になったとき→cocを有効化、ddcを無効化

cocは専用の変数があるのでそれにv:true / v:falseを入れればOK
ddcはそういった設定がないっぽいが、autoCompleteEventsの対象を空配列にすることで「どのイベントが起きても補完しない」状態になり、実質disableにできる
汎用的にやるならddc#custom#get_global()とかを使って設定を取り出したほうが良いが今回はskkeletonオンリーなので決め打ちでやってみた
あと設定上insert開始時には必ずddcは無効状態なのでInsertEnterイベントの設定は不要