Pythonのパス解決問題を解消し、pyls(Python Language Server)を導入した on NeoVim

この記事は[iRidge Advent Calendar 2018](https://qiita.com/advent-calendar/2018/iridge) の10日目です。Vimのほうのアドベントカレンダーではない。

# tl;dr

みなさん、VimでPythonツールを使っていますか？

私は現在勤めているiRidgeにおいて、
主要開発言語であるPythonで構築されたシステムを開発しています。
その中で開発環境の整備に付随するパスの問題に悩まされてきました。

今回はそのパス問題を解決して開発環境の構築がようやく満足できるレベルとなり、
自分の中で一応の決着を迎えたため、この機会のその内容を共有したいと思います。

同じような悩みを抱えている方の一助となれば幸いです。

# 前提条件

- 使用Vim
    - nvim v0.3.1
- 使用Pythonツール
    - [pyenv](https://github.com/pyenv/pyenv)
    - [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)
    - [pyenv-virtualenvwrapper](https://github.com/pyenv/pyenv-virtualenvwrapper)

# Python開発環境を整えるツール群

Pythonの開発環境を整える上で私が用いたツールは以下の通りです。

- [pyenv](https://github.com/pyenv/pyenv)
    - Pythonのバージョン管理
- [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)
    - Python開発ツールのために隔離した仮想環境
- [pyenv-virtualenvwrapper](https://github.com/pyenv/pyenv-virtualenvwrapper)
    - プロジェクトのために使用ライブラリを隔離した仮想環境

Pythonのコレ系のツールは非常に多くあり、Pythonを使う立場によって
必要なツールや不必要なツールは異なります。初学者からすれば混乱の種になってしまっていますね。
このような事情を憂いている方は他にもいらっしゃるようで、参考記事として挙げさせていただきます。

[Pythonの仮想環境構築(2017年版) pyenvとpyenv-virtualenvとvirtualenvとvirtualenvwrapperとpyvenvとvenv](https://qiita.com/maskedw/items/aaa2fd7abfd493cf2820)

私の立場はPythonを用いた開発者ですので、その前提で見ていただければと思います。

それでは順番に解説していきます。

なお、各ツールのインストール手順は、本記事に書くよりも
個別にREADMEや別記事などを参照頂いたほうが確実かと思いますので、割愛させていただきます。

## Pyenv(Pythonのバージョン管理)

- [pyenv](https://github.com/pyenv/pyenv)
    - Pythonバージョンの切り替えを行う
    - 後に語る仮想環境ツールの親玉的存在

Pythonの開発を行うのであれば、複数バージョンのPythonに対応することは必須です。
現に私はPython2系のプロダクトを触ることもあればPython3系のプロダクトを触ることもあります。
Python2系のサポートが2020年を境に打ち切りになるものの、現実としてはまだ全然両方を触る機会はあります。

たとえPython2がなくなったとしても、マイナーバージョンにより機能が異なるため、
今後も複数バージョンを切り替えることはあるでしょう。

## pyenv-virtualenv(Python開発ツールのために隔離した仮想環境)

- [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)
    - 以下の用にNeoVimからコールするPythonツールを隔離するために使用
        - NeoVimのリモートプラグインに使用されるPython
        - Vimプラグインに使用されるPython

Vimに限らず、開発ツールなどから利用されるツール群。
具体例をいえば以下のような(静的解析やフォーマットを行う)ツールをインストールしたPythonを
隔離するための仮想環境を構築するため、pyenv-virtualenvを使用しています。

- flake8
- autopep8
- isort
- yapf
- pyls(Python Language Server)

これはなぜかというと、これらのツールは各プロジェクト毎で使用するものではなく、
どのプロジェクトを参照するにしても共通して使うものだからです。

## pyenv-virtualenvwrapper(プロジェクトのために使用ライブラリを隔離した仮想環境)

- [pyenv-virtualenvwrapper](https://github.com/pyenv/pyenv-virtualenvwrapper)
    - プロジェクト毎に使用するPythonライブラリを隔離するために使用

こちらはプロジェクトが使用しているPythonライブラリ、DjangoやRequestsなどを仮想環境に隔離するために使用しています。
こちら素のvirtualenvで良いのではと言われるかもしれませんね。
確かにvirtualenvwrapperを使用している理由は、
私が利用している[vim-virtualenv](https://github.com/plytophogy/vim-virtualenv)がvirtualenvwrapper使用を前提としているからです。
vim-virtualenvについては後に書きます。

## 開発環境構築時のワークフロー

上記ツールは以下の流れで使用します。

### 導入時

この手順は各ツールインストール後に最初に一回のみ実施する手順です。

1. pyenvを用いて使用するPythonバージョンをインストール

    ```sh
    $ pyenv install ${PYTHON_VERSION}
    ```
1. pyenv-virtualenvを用いてNeoVim用の仮想環境を構築

    ```sh
    $ pyenv virtualenv ${VIRTUALENV_NAME}
    ```
    - Python2とPython3それぞれ用意
1. 上記仮想環境にNeoVimからコールするツールをインストール

    ```sh
    $ pyenv shell ${VIRTUALENV_NAME}
    $ pip install ${DEVTOOL_NAME}
    $ pyenv shell --unset
    ```
1. 各仮想環境のパスをNeoVimに登録

### プロジェクト開始時

この手順は新しいシステムを触る際に一回のみ実施する手順です。

1. pyenv-virtualenvwrapperを用いてプロジェクト用の仮想環境を構築

    ```sh
    $ mkproject ${PROJECT_NAME}
    ```
1. プロジェクトの必要ライブラリをインストール

    ```sh
    $ mkproject ${PROJECT_NAME}
    $ workon ${PROJECT_NAME}
    $ pip install -r requirements.txt
    $ deactivate
    ```

### NeoVim起動時

この手順は、開発開始時に都度実施する手順です。

1. プロジェクトの仮想環境をロード

    ```sh
    $ workon ${PROJECT_NAME}
    ```
1. NeoVim起動

    ```sh
    $ nvim
    ```

### 構築後の状況

開発環境構築後のPythonのパスは以下のようになります。
Python本体のパスは以下の各ディレクトリの`./bin/python`となっています。

- `$HOME`
    - .pyenv(pyenvのroot)
        - versions(pyenvが管理するバージョン毎のPython, pyenv-virtualenvが管理しているもの含む)
            - 2.x.x(特定バージョンのPythonのroot)
            - 3.x.x(同上)
            - neovim2(エディタ(NeoVim)からコールするツールを隔離したPython2の仮想環境)
            - neovim3(エディタ(NeoVim)からコールするツールを隔離したPython3の仮想環境)
    - .virtualenv(pyenv-virtualenvwrapperが管理するプロジェクト毎のPython仮想環境)
        - group1_project1(プロジェクトが必要とするライブラリを隔離したPythonの仮想環境)
        - group1_project2(同上)

# LSP導入 on Neovim

NeoVimでLSPクライアントをインストールし、Python Language Serverを用いて、
静的解析及び定義ジャンプをできるようにします。Pythonのパス周りの話を除けばVimでもできるかと思います。

余談ですが私は過去に3度ほどLanguage Serverの導入に失敗しています。
これはPythonパスの解決がうまくいっていなかったのが理由ですが、
VimのLSPクライアントが戦国時代状態で導入するたびに違うツールを使っていたりしたのも原因の一つです。

というわけでちゃんと導入できて感動しています。

## Python Language Server(pyls)のインストール

まずはPython Language Serverの導入です。
この作業はNeoVimから参照するPythonに対して実施します。

```sh
$ pip install python-language-server
```

なおこの操作により導入されるのは[python-language-server](https://github.com/palantir/python-language-server)です。

READMEに書いてありますがpython-language-serverの実態は、
これまでもPythonツールとして使われていた[Jedi](https://github.com/davidhalter/jedi)などをLSPのプロトコルで参照できるようにしたもののようですね。

現在Microsoft名義で開発されている[Microsoft/python-language-server](https://github.com/Microsoft/python-language-server)もあり、こちらは開発がかなり活発な様子で今後に期待です。


## LSPプラグイン

VimのLSPプラグインはいろいろあるのですが、今回は割と古株である[vim-lsp](https://github.com/prabirshrestha/vim-lsp)をチョイスしました。
vim-lspは非同期処理をVimとNeoVim共通のインタフェースとして扱うために[async.vim](https://github.com/prabirshrestha/async.vim)に依存しているようです。

### インストール

私はパッケージマネージャとして[dein](https://github.com/Shougo/dein.vim)を使用しているため、こちらでインストールします。

```vim
call dein#add('prabirshrestha/vim-lsp')
call dein#add('prabirshrestha/async.vim')
```

### vim-lspにpyls登録

[vim-lsp wiki python](https://github.com/prabirshrestha/vim-lsp/wiki/Servers-Python)に従ってvimrcの設定をします。

```vim
let s:pyls_path = fnamemodify(g:python_host_prog, ':h') . '/'. 'pyls'
if (executable('pyls'))
    au User lsp_setup call lsp#register_server({
  \ 'name': 'pyls',
  \ 'cmd': {server_info->[expand(s:pyls_path)]},
  \ 'whitelist': ['python']
  \ })
endif
```

上記は私の環境用にちょっとした書き換えを行っています。
以下がその部分になっていて

```vim
let s:pyls_path = fnamemodify(g:python_host_prog, ':h') . '/'. 'pyls'
```

pylsをNeoVim参照用のパスに固定しています。こうすることで
vim-lspが参照するpylsをNeoVimが参照する仮想環境のPythonに固定しています。

### キーマップ

vim-lspの持つコマンドを使いやすいようにマッピングしています。
vim-lspの持つコマンドは[vim-lsp supported commands](https://github.com/prabirshrestha/vim-lsp#supported-commands)または`:h vim-lsp-commands`を参照。

ちなみに私はLocalLeader(特定のファイルタイプでのみ使用するプラグイン用キー)として`\(バックスラッシュ)`を割り当てており、
今回Python限定なのでそちらを使っています。

なお今回設定しているコマンドは参照系のみでLintの設定などしていません。
Linterとしては[ALE](https://github.com/w0rp/ale)、自動補完は[deoplete-jedi](https://github.com/zchee/deoplete-jedi)を使用しているからです。

ALEの設定の話は[別記事(ALE(on NeoVim)でPythonコードを楽に整形する)](https://qiita.com/lighttiger2505/items/9a36c5b4035dd469134c)をご参照ください。

使用感として私の好みに合うLSPクライアントがないため、現状はこうしていますが、
このあたりをカバーできるようなLSPクライアントが出てくれば、その時考えます。

```vim
augroup PylsCommands
    autocmd!
    autocmd FileType python nnoremap <C-]> :<C-u>LspDefinition<CR>
    autocmd FileType python nnoremap K :<C-u>LspHover<CR>
    autocmd FileType python nnoremap <LocalLeader>R :<C-u>LspRename<CR>
    autocmd FileType python nnoremap <LocalLeader>n :<C-u>LspReferences<CR>
augroup END
```

### 自分の環境でうまく動かすハック的ななにか

vim-lsp導入に際して、困ったことが一つありました。
私の環境要因の可能性があるので明確にバグなのかはわかりませんが、事象として以下のようになりました。

1. Vim起動時にPythonファイルを開くとpylsが起動する
1. Vim起動時にファイル無指定で起動のみ行い、後にPythonファイルを開くとpylsが起動しない

vim-lspソースを読んでみると、Vim起動時`lsp#enable()`をコールしており、
登録ずみのLanguageServerに紐付いたファイルタイプがないとこの関数のトリガーとして引っかからないようでした。

なので一旦Pythonファイルを開いたときにこの関数をコールするようにAutoCmdを設定しています。

```vim
augroup LaunchPyls
    autocmd!
    autocmd BufWinEnter *.py :call lsp#enable()
augroup END
```

落ち着いたらvim-lspにIssueを投げてみる予定です。

### vim-lspのデバッグ

上記のように動かなかったときは、[vim-lsp debugging](https://github.com/prabirshrestha/vim-lsp#debugging)を参考にするとvim-lspのデバッグが可能です。
躓いたらやってみたらいかがでしょうか。

```vim
let g:lsp_log_verbose = 1
let g:lsp_log_file = expand('~/vim-lsp.log')
```

## プロジェクト毎の仮想環境を見てくれない

上記で一通り完了しましたが、まだ終わりではありませんでした。

なぜならばDjangoなど各プロジェクト毎にインストールしているライブラリを`LspDefinition`コマンドで参照することが出来なかったからです。
おそらく原因はpylsの中で起動しているJediが仮想環境を静的解析の対象として認識していないのが原因です。

この問題を解決するためには[vim-virtualenv](https://github.com/plytophogy/vim-virtualenv)が必要となります。

このプラグインはvirtualenvwrapperでactivateした仮想環境を認識し、
Jedi側の静的解析の対象に加えるような動きとなるようです。(このあたりはjediのソースを読む必要がありそうでちゃんと追えてない)

## 最終的なvimrc

私はdeinのtomlファイル定義を用いているので、最終的には以下のような定義となりました。

```toml
[[plugins]]
repo = 'jmcantrell/vim-virtualenv'
on_ft = ['python']

[[plugins]]
repo = 'prabirshrestha/async.vim'

[[plugins]]
repo = 'prabirshrestha/vim-lsp'
on_ft = ['python']
depends = ['async.vim', 'vim-virtualenv']
hook_add = '''
" registar pyls to vim-lsp
let s:pyls_path = fnamemodify(g:python_host_prog, ':h') . '/'. 'pyls'
if (executable('pyls'))
    au User lsp_setup call lsp#register_server({
    \ 'name': 'pyls',
    \ 'cmd': {server_info->[expand(s:pyls_path)]},
    \ 'whitelist': ['python']
    \ })
endif

augroup LaunchPyls
    autocmd!
    autocmd BufWinEnter *.py :call lsp#enable()
augroup END

augroup PylsCommands
    autocmd!
    autocmd BufWinEnter *.py :call lsp#enable()
    " local key mapping
    autocmd FileType python nnoremap <C-]> :<C-u>LspDefinition<CR>
    autocmd FileType python nnoremap K :<C-u>LspHover<CR>
    autocmd FileType python nnoremap <LocalLeader>R :<C-u>LspRename<CR>
    autocmd FileType python nnoremap <LocalLeader>n :<C-u>LspReferences<CR>
augroup END
'''
```

# 最後に

なお今回の記事の作成および検証は[meguro.vim](https://megurovim.connpass.com/)というもくもく会にて行いました。まだ来ていないVimmerの方はぜひ一回来てみてはいかがでしょうか？

それでは皆さん。良いVim&Pythonライフを。