Chanomic Blog

neovimのプラグインがうまく動かなかったので原因を探した話

(last modified:
)

categories:

(2021/12/25追記) この記事で話題にした問題は最新のddc-nvim-lspで修正されている。こちらのissue及びこちらのcommitを参照。もっとも、この記事を書いてから大分経ったため、ddc_nvim_lsp.luaのソースコードも今では大分変わっている。

以下の文章のまとめ

バージョン違いには注意する

ddc-nvim-lspは2021/10/1時点では、neovim 0.5.0を想定して作られているプラグインである。しかし自分はneovim 0.5.1を使ってしまっていた。neovim 0.5.1からlsp handlerの引数に破壊的変更があったため、LSPの補完が効かなかった。

究明に当たってDockerを触ったり、Luaを触ったり、ドキュメントを漁ったりして色々糧にはなったので、記録しておく。

何が起きたのか

まず、プラグインの管理にはShougo/dein.vimを使った。

neovimのbuildin LSPを使ってLSPが使える環境を構築した。設定に当たって以下のプラグインを導入した。

入力補完はShougo/ddc.vimを使った。それにあたって以下のプラグインを導入した。

最後のddc-nvim-lspがうまく動かなかった.

Language Serverとしてpyrightを導入したのだが、実際にPythonのファイルで入力補完を試したところ,ddc-aroundの補完は反応するが,ddc-nvim-lspの補完候補が現れなかった。

Dockerを使って再現性を検証する

まず、

という仮説を立てた。そのためには、何も無い素のneovimの環境を作る必要があると考えた。そこで、環境をDockerで構築しようと考えた。

Docker環境の構築

適当なディレクトリを作って、そこにDockerfiledocker-compose.ymlを作成する。

Dockerfileを以下のようにする。ベースイメージはanatolelucet/neovimにした。この時点でdeinを導入する。コマンドはdeinのQuick startを参照した。deinのインストールにあたってcurlgitコマンドが必要なので、ここで導入する。

FROM anatolelucet/neovim:stable-ubuntu
RUN apt-get update && apt-get install -y curl git
RUN curl https://raw.githubusercontent.com/Shougo/dein.vim/master/bin/installer.sh > installer.sh && sh ./installer.sh ~/.cache/dein

neovimの設定ファイルはコンテナ外で編集できるようにしておく。同ディレクトリにディレクトリ.config/nvim/を作成し、その上で、docker-compose.ymlを以下のようにする。

version: '3'
services:
  nvim:
    build: .
    volumes:
      - .config:/root/.config
    entrypoint: 'bash'
    working_dir: /root

.config/nvim/の中にinit.vimdein.toml'、'dein_lazy.tomlを作成。init.vimは以下の通り。

"dein Scripts-----------------------------
if &compatible
  set nocompatible               " Be iMproved
endif

" Required:
set runtimepath+=/root/.cache/dein/repos/github.com/Shougo/dein.vim

" Required:
call dein#begin('/root/.cache/dein')

" Let dein manage dein
" Required:
call dein#add('/root/.cache/dein/repos/github.com/Shougo/dein.vim')

let s:toml = '~/.config/nvim/dein.toml'
let s:lazy_toml = '~/.config/nvim/dein_lazy.toml'
call dein#load_toml(s:toml, {'lazy': 0})
call dein#load_toml(s:lazy_toml, {'lazy': 1})

" Required:
call dein#end()

" Required:
filetype plugin indent on
syntax enable

" If you want to install not installed plugins on startup.
if dein#check_install()
  call dein#install()
endif

"End dein Scripts-------------------------

dein.tomldein_lazy.tomlについては長くなるため省略するが、必要なプラグインを導入する。

この時点でdocker-compose buildをすればコンテナがビルドされ、その後docker-compose run nvimでコンテナを実行する。

コンテナ内での作業

Language Serverであるpyrightを導入する。pyrightの導入に当たってpython3npmnodeが必要。aptコマンドだと古いバージョンしか入らないようだったので、 NodeSourceのInstallation instructionsに従って最新のバージョンを入れる。

# curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
# apt-get install -y nodejs

pyrightのCommand-lineに従ってpyrightを導入する。

# npm install -g pyright

続いてDenoのInstallationに従ってDenoを導入する。

# curl -fsSL https://deno.land/x/install/install.sh | sh

denoのパスを設定するため、.bashrcに以下の内容を追記する。その後、変更を反映させるためにsource .bashrcを実行。

export DENO_INSTALL="/root/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"

動いた

これでnvim main.pyを実行する。なんとこれでLanguage Serverの補完が効いた。

コンテナの設定ファイルをホストに移すが、うまくいかない

うまくいったinit.vimdein.tomldein_lazy.tomlをホスト側に移して、うまくいくか試してみる。勿論バックアップは取っておく。 ところが、ホスト側では補完が効かなかった。

同じ設定ファイルなのにもかかわらず、コンテナとホストで違いが現れている。「それでは、Mac固有の問題なのだろうか」と考えた(実はそうでないことは後々分かることになる)。

「Mac固有の問題だとしても、どこでうまくいっていないのかを知りたい」と考えた。うまくいかない状況を以下の2つに分割して、まず前者について調査した。

以降、Dockerからは離れ、ホスト側で調査をする。

Luaを触る

自前でLSPサーバーとの通信を行ってみて、正しい結果が帰ってくるかどうかをみることにした。neovimではこれをLuaのAPIとして提供している。Luaなんてほとんど書いたことは無いが、ddc-nvim-lspのソースコードから何をやっているのかを類推して、補完を取得する処理を読み出した。

Luaのスクリプトを実行する方法は:h lua-commandsに書かれている。ここではLuaファイルに書いたものを実行したいので、luafileコマンドを使う。

Luaファイルの内容

適当なLuaファイルを作成し、内容を以下のようにする。

function dump(o)
  if type(o) == 'table' then
    local tbl = {}
    for k,v in pairs(o) do
      table.insert(tbl, '"' .. tostring(k) .. '":' .. toJson(v))
    end
    return '{' .. table.concat(tbl, ',') .. '}'
  elseif type(o) == 'number' or type(o) == 'boolean' then
    return tostring(o)
  else
    return '"' .. tostring(o) .. '"'
  end
end

local f = function(_, result)
  print(dump(result['items']))
end


local params = vim.lsp.util.make_position_params()
vim.lsp.buf_request(0, 'textDocument/completion', params, f)

buf_request

LSPにリクエストを送るにはvim.lsp.buf_requestという関数を使えば良いらしい。ドキュメントから抜粋すると、引数の説明は以下の通り。

buf_request({bufnr}, {method}, {params}, {handler})
                Sends an async request for all active clients attached to the
                buffer.

                Parameters: ~
                    {bufnr}    (number) Buffer handle, or 0 for current.
                    {method}   (string) LSP method name
                    {params}   (optional, table) Parameters to send to the
                               server
                    {handler}  (optional, function) See |lsp-handler|

現在のバッファに対して補完情報のリクエストを送りたいので、{bufnr}には0を入れる。補完のリクエストなので、LSPの仕様より{method}にはtextDocument/completionを入れる。{params}には現在のカーソル位置の情報を入れたいが、これはvim.lsp.util.make_position_params関数でできる。

lsp-handler

handlerにはいわゆるコールバック関数を指定する。関数の引数は:h lsp-handlerで確認できる。以下、neovim 0.5.1のドキュメントから一部抜粋する。

lsp-handlers are functions with special signatures that are designed to handle
responses and notifications from LSP servers.

For |lsp-request|, each |lsp-handler| has this signature:

  function(err, result, ctx, config)

        Parameters: ~
            {err}       (table|nil)
                            When the language server is unable to complete a
                            request, a table with information about the error
                            is sent. Otherwise, it is `nil`. See |lsp-response|.
            {result}    (Result | Params | nil)
                            When the language server is able to succesfully
                            complete a request, this contains the `result` key
                            of the response. See |lsp-response|.

dump

handlerでの処理は、単にLSPから受け取った結果を出力することにした。table関数はそのままでは出力できないため、dump関数を書いた。これはtableをJSONっぽい形式で出力する関数。

Luaファイルの実行

適当なPythonのファイルを作り、以下のように書く。

"Hello".

補完の処理が正常に動作しているなら、ピリオドの後に文字列系の関数(joinなど)が補完候補として出てくるはずである。 そこで、ピリオドの部分にカーソルを持っていって、luafile ファイル名でLuaファイルを実行する。

すると、neovimのウインドウ下に以下の文が表示された。

{"1":{"data":{"filePath":"/Users/bombrary/tmp/main.py","workspacePath":"/Users/bombrary/tmp","...init_subclass__"},"label":"__init_subclass__","sortText":"10.9999.__init_subclass__","kind":2}}

これはLSPサーバーから受けとった情報である。ということは、LSPとの通信はうまくいっているようだ。LSPとの通信で失敗しているわけではない。

原因の場所を見つける

「LSPサーバーから情報が受け取れているとしたら、その後の処理に問題があるかもしれない」と考え、ddc-nvim-lspのコードを読み始めた。コードが置いてある場所は設定によるが、自分の環境では~/.cache/dein/repos/github.com/Shougo/ddc-vim-lsp/ある。

とりあえずvim.lsp.buf_requestに近いところから読み始めた。そこで、{handler}として指定したget_candidates関数に目をつけた。

local get_candidates = function(_, arg1, arg2)
  -- For neovim 0.6 breaking changes
  -- https://github.com/neovim/neovim/pull/15504
  local result = (vim.fn.has('nvim-0.6') == 1
                  and type(arg1) == 'table' and arg1 or arg2)
  -- ... 略

ソースコードのコメントによると、neovimのバージョンによって、lsp-handlerの引数が変わるらしい。代入文にて短絡評価を使っている。そこではneovimのバージョンが0.6でなかった時点で、2番目の引数arg2resultであると確定している。しかし、よく考えるとそれはおかしい。neovim 0.5.1のドキュメントを参照した時は、引数の順番はfunction(err, result, ctx, config)であり、1番目の引数がresultのはず。

もしや、と思い、のソースコードを以下のように変更した。

local get_candidates = function(_, result)
  -- ... 略

なんとこれでLSPの補完が効くようになった。その場しのぎの変更ではあるが、使えるのでそのままにしておく。

lsp-handler変更についてのPull requestをみると、どうやらneovim 0.5.1のバージョンにこれが取り込まれたようである。実際、0.5.1 Changelogにも記載されている。この変更のせいで、補完の処理がうまく動かなくなっていたようである。

Dockerコンテナのneovimでは動いたのは、Dockerのベースイメージとして入っていたneovimのバージョンが0.5.0だったからである。

プラグインのリポジトリをよく見る

2021/10/1時点でddc-nvim-lspのRequiredを読むと、そこにneovim 0.5.0+ with LSP configurationと書いてあった。なので、neovim 0.5.0じゃないと動作が保証されない。0.5.0と0.5.1のバージョン違いなんて対して無いだろうと思っていたが、そういうところまでちゃんと疑うべきだった。

学んだこと