neovimでjedi-language-serverをnvim-lspのデフォルト以外の設定で使う

# まえがき

最近vimからneovimに移行するにあたってPythonの開発環境を整えているのですが，jedi-language-serverの設定に関する情報が少なかったので，備忘録がてらまとめます．

LSPを使うのが初めてということもあり，「とりあえずは補完が出てくれればいいや，リンターやフォーマッタなどはあとからにしよう」という感じで今回は補完オンリーをターゲットにして環境を開発構築していきます．

# 環境

今回セットアップした環境は以下の通りです．

- OS: macOS 10.14 (Mojave)
- neovim: 0.5.0 (HomebrewのHEAD 1ca67a7)
- Python: 3.8.5 (Homebrewでインストールしたやつ）
- LSPクライアント: neovim0.5.0から標準のクライアント + [nvim-lsp](https://github.com/neovim/nvim-lsp)
- LSPサーバ: [jedi-language-server](https://github.com/pappasam/jedi-language-server)
- 補完: deoplete.nvim

巷では[python-language-server](https://github.com/palantir/python-language-server)を使用している例が多いのですが，私の環境では補完候補が出てくるのがものすごく遅かったのでjedi-language-serverを採用しています．

# Luaの設定

jedi-language-serverをnvim-lspのデフォルト設定で使うのは簡単です．以下の設定を `init.vim` に入れておけば使えるようにはなります．

```lua
lua <<END
local nvim_lsp = require 'nvim_lsp'

nvim_lsp.jedi_language_server.setup{}
END
```

が，デフォルトだとリンターの反応が早い & 張り切りすぎてうるさい．タイプ途中に構文エラーで怒られても．．．という感じなので，リンターをオフにします．

```lua
lua << END
local nvim_lsp = require 'nvim_lsp'

nvim_lsp.jedi_language_server.setup{
  init_options = {
    diagnostics = {
      enable = false,
    },
    completion = {
      disableSnippets = true,
    },
    jediSettings = {
      autoImportModules = {'numpy', 'pandas'},
    },
  },
}
END
```

設定可能な項目は，GitHubの[README](https://github.com/pappasam/jedi-language-server#configuration)を参照ください．

`pyls`の場合は`settings.pyls.plugins`なんかに記載するのですが，jedi-language-serverは`init_options`のようです．このへんの設定のお作法？がよく分かっていないのがつらいところ．

# 補完をした際にtextEditのエラーが発生する

上記の設定を行い，構文エラーで怒られないことを確認した後，意気揚々と`np.`と打ったところ，以下のエラーが発生しました．

```
Error executing vim.schedule lua callback: ...vim/HEAD-1ca67a7/share/nvim/runtime/lua/vim/lsp/util.lua:277: attempt to index field 'textEdit' (a userdata value)
```

どうやらLuaの`nil`と`vim.NIL`の違いが原因のエラーのようです．既に[バグは報告](https://github.com/neovim/nvim-lsp/issues/304)されており，近いうちに修正されるとは思うのですが，今のところは以下の変更を加えることで使えるようになります．

```diff
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
index 52a6fe89f..fc619c8a8 100644
--- a/runtime/lua/vim/lsp/util.lua
+++ b/runtime/lua/vim/lsp/util.lua
@@ -274,13 +274,13 @@ end
 -- textEdit.newText > insertText > label
 -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
 local function get_completion_word(item)
-  if item.textEdit ~= nil and item.textEdit.newText ~= nil then
+  if item.textEdit ~= vim.NIL and item.textEdit.newText ~= vim.NIL then
     if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
       return item.textEdit.newText
     else
       return M.parse_snippet(item.textEdit.newText)
     end
-  elseif item.insertText ~= nil then
+  elseif item.insertText ~= vim.NIL then
     if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
       return item.insertText
     else
```

この修正を行うことで，ようやく補完が出てくるようになりました．それなりに早くて快適．

![Screen Shot 2020-07-24 at 17.02.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240609/efe7e1ee-642b-0741-596a-e2481cd70fa6.png)

ちなみに，ROS2のrclpyもさくさく補完できるようになりました．これは便利．

![Screen Shot 2020-07-24 at 17.04.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240609/e5bb62fb-c48d-fc67-e64f-90458f95a96d.png)

これは本当にPyCharmがいらなくなる日がくるかもしれない．

# おまけ1

最終的には使いませんでしたが，pylsを使ってリンターの出力を消したときの設定も載せておきます．

```lua
nvim_lsp.pyls.setup{
  settings = {
    pyls = {
      plugins = {
        pyflakes = {enabled = false},
        pycodestyle = {enabled = false},
        pydocstyle = {enabled = false},
      }
    }
  }
}
```

# おまけ2

jedi-language-serverでどんな設定が最終的にされているのかを確認するために使ったコードです．

参考: https://stackoverflow.com/a/27028488

```lua
function dump_impl(o, level)
  if type(o) == 'table' then
    local s = '{\n'
    for k, v in pairs(o) do
      if type(k) ~= 'number' then
        k = '"' .. k .. '"'
      end
      local indent = string.rep('  ', level + 1)
      s = s .. indent .. '['..k..'] = ' .. dump_impl(v, level + 1) .. ',\n'
    end
    local indent = string.rep('  ', level)
    return s .. indent .. '}'
  else
    return tostring(o)
  end
end
function dump(o)
  return dump_impl(o, 0)
end

local c = nvim_lsp.jedi_language_server.make_config()
print(dump(c))
```