NeovimでモダンなPython環境を構築するv2(LSPを添えて)

# tl;dr

私は以前、[NeovimでモダンなPython環境を構築する](https://qiita.com/lighttiger2505/items/e0ada17634516c081ee7)という記事を投稿しました。

上記記事の投稿から1年8ヶ月が経過し、LSPや新たなVimの機能などによりVimを取り巻く環境には次々と大きな変化が訪れていることを日々感じており、[VimConf 2019](https://vimconf.org/2019/)に参加したことでその感覚はより強い確信へと変わりました。

以前から上記記事の内容は最新の状態に則しておらず、現状を踏まえた新しい記事を書く必要性は感じていました。
本記事を書くにあたり前記事に対して上書きすることも考えたのですが、あえて別記事にすることで、
この数年でVimの開発環境にどれほどの変化が起こったのか。
以前との対比を残すと面白いのではないか。
と思いv2として新しく本記事を書くことを決めました。

## Language ServerによりPythonのインテリセンスを提供する

Vimに訪れた最も大きな環境の変化として`Language Server Protocol(LSP)`の普及とそれに伴って発達した`Language Server(LS)クライアント`が挙げられることでしょう。
またLSから提供された情報を表示するための手段がより豊富になり、より直感的なUIが作れるようになったことも忘れてはなりません。
VimにおけるpopupやNeovimに追加されたFloatingWindowやVirtualTextなどですね。

LSPによりコードの静的解析とによって得られる言語のインテリセンスは、劇的な変化を遂げました。
より高速により正確に提供されるインテリセンスによって、私達はさらに快適な開発環境を得られたのです。

Vimにおいては各機能ごと(自動補完や定義ジャンプ、静的解析ツール結果表示など)に分割されていたプラグイン郡は徐々にLSクライアントに集約されたり、インテリセンス提供部分をLSクライントに任せてインタフェースの表示のみを担当する別プラグインが生まれるなど、エコシステムに変化が見受けられます。

また後述する各プラグインのセットアップをすることでPythonの開発環境を整えれば、他の言語に対して同等のインテリセンスを持とうとしたとき、比較的楽にその設定を追加することができるようになりました。

### vim-lsp + python-language-server(pyls)

- [vim-lsp](https://github.com/prabirshrestha/vim-lsp)
- [python-language-server](https://github.com/palantir/python-language-server)

Pythonのインテリセンスを得るためには`pyls`が必要となります。pipでインストール可能です。

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

以前まではPythonの静的解析サーバであるjediがあり、jedi解析結果を受け取ってVim上でインテリセンスを得るための[jedi-vim](https://github.com/davidhalter/jedi-vim)というプラグインを使用することがPythonのインテリセンスを得る手段として一般的であったと思います。
これらの役割は現在vim-lsp + python-language-server(pyls)で担うことができます。
READMEを見る限り、pylsは実態としてjediやその他ツールを統合し、LSP経由でアクセスできるようにしたもののようです。

以下にvim-lspを利用した場合の設定例を示します。

```vim
" vim-lspの各種オプション設定
let g:lsp_signs_enabled = 1
let g:lsp_diagnostics_enabled = 1
let g:lsp_diagnostics_echo_cursor = 1
let g:lsp_virtual_text_enabled = 1
let g:lsp_signs_error = {'text': '✗'}
let g:lsp_signs_warning = {'text': '‼'}
let g:lsp_signs_information = {'text': 'i'}
let g:lsp_signs_hint = {'text': '?'}

if (executable('pyls'))
    " pylsの起動定義
    augroup LspPython
        autocmd!
        autocmd User lsp_setup call lsp#register_server({
            \ 'name': 'pyls',
            \ 'cmd': { server_info -> ['pyls'] },
            \ 'whitelist': ['python'],
            \})
    augroup END
endif

" 定義ジャンプ(デフォルトのctagsによるジャンプを上書きしているのでこのあたりは好みが別れます)
nnoremap <C-]> :<C-u>LspDefinition<CR>
" 定義情報のホバー表示
nnoremap K :<C-u>LspHover<CR>
" 名前変更
nnoremap <LocalLeader>R :<C-u>LspRename<CR>
" 参照検索
nnoremap <LocalLeader>n :<C-u>LspReferences<CR>
" Lint結果をQuickFixで表示
nnoremap <LocalLeader>f :<C-u>LspDocumentDiagnostics<CR>
" テキスト整形
nnoremap <LocalLeader>s :<C-u>LspDocumentFormat<CR>
" オムニ補完を利用する場合、定義の追加
set omnifunc=lsp#complete
```

上記に加えて、goplsなど他のLSを導入することで容易に対応言語を増やすことが出来ます。
嗚呼素晴らしきかなLSP。

#### フォーマッター

pylsは単体ではLSPのフォーマット(textDocument/formatting Method)には対応しておらず、
[別途プラグイン](https://github.com/palantir/python-language-server#3rd-party-plugins)が必要となります。

- [pyls-isort](https://github.com/paradoxxxzero/pyls-isort)
- [pyls-black](https://github.com/rupert/pyls-black)

こちらについてもpipでインストール可能です。

```sh
pip3 install pyls-isort
pip3 install pyls-black
```

blackなどはPyCon 2019でもいくつかのセッションで利用を推奨されるようなフォーマッタになっており、特別理由がないのであれば入れておいたほうが良いでしょう。
フォーマッタに準拠したコードを維持するのであれば以下のような`autocmd`を追加することで、保存時にフックしてフォーマットをかけることができます。

```vim
augroup LspAutoFormatting
    autocmd!
    autocmd BufWritePre *.py LspDocumentFormatSync
augroup END
```

マニュアル実行をかけたいのであればキーバインドに`LspDocumentFormat`を登録してあげると良いでしょう。

#### Linterおよびその設定

pylsで注意が必要なのは、デフォルトであると少々過剰なチェックが実施されるという点です。
型チェックを実施しないのであればPythonのLintはflake8だけでだいたいは足ります。
しかしpylsでは以下のLinter郡がデフォルトで動作するようになっています。

- pep8
- pycodestyle
- pylint
- flake8

pylintはかなり厳し目のスタイルチェックが実行されるため、未設定で実行するとflake8だけでは出ないかなり多くのErrorやWarnが出ることが予想されます。
そのためpyls初期化時に不要な設定を無効化する。ないし以下の[lint設定ファイル](https://github.com/palantir/python-language-server#configuration)にて個別設定することをおすすめします。

| Lint        | Config File               |
|-------------|---------------------------|
| pep8        | $HOME/.config/pep8        |
| pycodestyle | $HOME/.config/pycodestyle |
| pylint      | $HOME/.config/pilintrc    |
| flake8      | $HOME/.config/flake8      |


参考までに私の設定ファイルを以下に示しておきます。

- [flake8](https://github.com/lighttiger2505/.dotfiles/blob/master/.config/flake8)
- [pep8](https://github.com/lighttiger2505/.dotfiles/blob/master/.config/pep8)
- [pycodestyle](https://github.com/lighttiger2505/.dotfiles/blob/master/.config/pycodestyle)
- [pylintrc](https://github.com/lighttiger2505/.dotfiles/blob/master/.config/pylintrc)

これらの各LinterはLSクライアントの設定で以下のようにON/OFFが可能です。

```vim
" pylsの設定。LinterのON/OFFなどが可能
let s:pyls_config = {'pyls': {'plugins': {
    \   'pycodestyle': {'enabled': v:true},
    \   'pydocstyle': {'enabled': v:false},
    \   'pylint': {'enabled': v:false},
    \   'flake8': {'enabled': v:true},
    \   'jedi_definition': {
    \     'follow_imports': v:true,
    \     'follow_builtin_imports': v:true,
    \   },
    \ }}}
" pylsの起動定義
augroup LspPython
    autocmd!
    autocmd User lsp_setup call lsp#register_server({
        \ 'name': 'pyls',
        \ 'cmd': { server_info -> ['pyls'] },
        \ 'whitelist': ['python'],
        \ 'workspace_config': s:pyls_config
        \})
augroup END
```

またTypeHintを利用するためのmypyについては外部提供のプラグイン([pyls-mypy](https://github.com/tomv564/pyls-mypy))を追加することでLSに機能追加が可能です。

#### vim-lsp以外のLSクライアント

VimのLSクライアントにはいくつかの選択肢があります。自分の環境や好みに合わせてチョイスすると良いでしょう。
と言われてもどう選べばよいのだとおっしゃるかもしれませんね。そんな方のためにいくつか簡単な説明を添えます。

LSクライアントを選択する上で重要なのは今ご自分が利用されているVimがなにかに大きく左右されます。

素のVimであれば私はvim-lspをおすすめします。vim-lspはVim/Neovimの両対応を明言しており、両方のユーザがいるため、バグ報告や修正も活発に行われるからです。
他のcoc.nvimやLanguageClient-neovimは元々Neovimのリモートプラグインとして開発されているため、Vimのユーザが少なくVimに対するメンテが滞る可能性があると考えています。

Neovimであれば選択肢はいろいろとありますが、大別して

- IDEライクなインテリセンスを欲するならcoc.nvim
- Vimのデフォルト機能を愛する人ならvim-lsp
- 上記2つの中間ならLanguageClient-neovim

と認識しています。最近masterにマージされたNeovim標準搭載のLSクライアントを使うのも面白いかもしれません。
ただこれらは状況次第で変わる可能性があるため、継続的にプロジェクトの動向を追うことをおすすめします。

## LSを用いて自動補完をする(deoplete.nvim with vim-lsp)

- [deoplete.nvim](https://github.com/Shougo/deoplete.nvim)
- [deoplete-vim-lsp](https://github.com/lighttiger2505/deoplete-vim-lsp)

Vimの自動補完フレームワークとしてよく知られるdeoplete.nvimですが、LSに対してvim-lspを経由して補完候補のリクエストを行うためには、deoplete.nvimとは別途補完ソースが必要となります。手前味噌ですがdeoplete-vim-lspがそのためのソースとなります。

以下に私の設定を載せておきます。

```vim
let g:deoplete#enable_at_startup = 1

inoremap <expr><C-h> deoplete#smart_close_popup()."<C-h>"
inoremap <expr><BS> deoplete#smart_close_popup()."<C-h>"

call deoplete#custom#option({
    \ 'auto_complete': v:true,
    \ 'min_pattern_length': 2,
    \ 'auto_complete_delay': 0,
    \ 'auto_refresh_delay': 20,
    \ 'refresh_always': v:true,
    \ 'smart_case': v:true,
    \ 'camel_case': v:true,
    \ })
let s:use_lsp_sources = ['lsp', 'dictionary', 'file']
call deoplete#custom#option('sources', {
    \ 'go': s:use_lsp_sources,
    \ 'python': s:use_lsp_sources,
    \ 'vim': ['vim', 'buffer', 'dictionary', 'file'],
    \})
```

deoplete-vim-lspは、このLS過渡期におけるつなぎのような存在だと私は認識しています。
今後の動向次第では他のより良い選択肢がいつ出てきてもおかしくなく、そうなったときに儚く消えるかもしれません。

### deoplete-vim-lsp以外のLS用補完ソース

deoplete.nvimの補完ソースの話が出たので、deoplete-vim-lsp以外の補完ソースについても言及しておきましょう。

現在主要なLSクライアントにはdeoplete用の補完ソースが存在しています。もしvim-lsp以外のLSクライアントにてdeopleteを用いた補完が行いたいのであれば、それぞれの補完ソースを導入する必要性があります。

- [LanguageClient-neovimのdeoplete補完ソース](https://github.com/autozimu/LanguageClient-neovim/blob/next/rplugin/python3/deoplete/sources/LanguageClientSource.py)
- [deoplete-lsp(NeovimビルトインLSクライアントの補完ソース)](https://github.com/Shougo/deoplete-lsp)

### deoplete.nvim以外の自動補完フレームワーク

#### asyncomplete.vim

deoplete.nvim以外にもvim-lsp経由の自動補完を行うための手段は存在します。
というかvim-lsp経由の自動補完を用いるのであればasyncomplete.vimのほうが一般的かもしれません。こちらはvim-lspを作成されているprabirshrestha氏が同じくメンテナをされています。

- [asyncomplete.vim](https://github.com/prabirshrestha/asyncomplete.vim)
- [asyncomplete-lsp.vim](https://github.com/prabirshrestha/asyncomplete-lsp.vim)

#### coc.nvim

coc.nvimは現存するVimのLSクライアントの中でもひときわ大きなプロジェクトの一つになっています。
元々はcoc(Conquer Of Completion)の名の通り、自動補完用のフレームワークであったと思いますが、現在ではLSを通したインテリセンスをVim上で全て実現するための統合環境。VimにおけるIDE機能の実装とも呼ぶべきものになっており、自動補完はcoc.nvimを導入すれば、手に入れることが可能です。

- [coc.nvim](https://github.com/neoclide/coc.nvim)

### LSクライアントと補完フレームワークの組み合わせまとめ

さて、これまででLSクライアントと補完フレームワークがいろいろと出てきましたね。これらの組み合わせには種類があり、各LSクライアントと補完フレームワークは個別のポリシーを持って独自進化を遂げているような状況です。つまりディファクトスタンダードがあり、「コレを使えば間違いないさ！」というような状況ではないのです。
これまで紹介したLSクライアントと補完フレームワークの組み合わせを参考までに以下にまとめておきます。

| LSクライアント                 | 補完フレームワーク                      |
|--------------------------------|-----------------------------------------|
| vim-lsp                        | asyncomplete.vim + asyncomplete-lsp.vim |
| vim-lsp                        | deoplete.nvim + deoplete-vim-lsp        |
| coc.nvim                       | coc.nvim                                |
| LanguageClient-neovim          | deoplete.nvim + LanguageClient Source   |
| NeovimビルトインLSクライアント | deoplete.nvim + deoplete-lsp            |

というわけでLSPがもたらしたVimのエコシステムへの変化は現状も続いています。
あるいはあなたがこれから作成するプラグインによってこのような状況に一石を投じるといったことも可能かもしれません。

## アウトライン表示

- [vista.vim](https://github.com/liuchengxu/vista.vim)

エディタに欲しいIDE的機能の一つにアウトライン(関数やクラスなどシンボルの一覧)表示があります。
元々Vimには[ctags](https://github.com/universal-ctags/ctags)を用いてのシンボルジャンプや生成されたctagファイルを用いてアウトラインを表示するプラグイン([tagbar](https://github.com/majutsushi/tagbar)など)があり、これまではアウトラインをVimで表示するならctagsを使うことが一般的でした。
一方LSPにはシンボル取得のためのメソッド(textDocument/documentSymbol Method)が定義されており、
LSクライアントを経由して取得したシンボルからアウトラインを表示するvista.vimがあります。

以下のように設定すれば、デフォルトでctagsで生成したアウトラインを、LSを起動する言語に関してはvim-lspを経由して取得したアウトラインを表示します。

```vim
let g:vista_sidebar_width = 40
let g:vista_echo_cursor = 0

" デフォルトの情報ソースをctagsにする
let g:vista_default_executive = 'ctags'
" 特定の言語の場合vim-lspを利用した情報ソースを利用するようにする
let g:vista_executive_for = {
    \ 'go': 'vim_lsp',
    \ 'python': 'vim_lsp',
    \ }

" トグル(アウトラインを非表示の場合は表示、表示済みの場合は非表示に)
nnoremap <silent> <Leader>o :<C-u>Vista!!<CR>
```

vista.vimはctagsとLSクライアントから取得したシンボルの表示どちらにも対応しています。
`g:vista_executive_for`にて言語と対応するLSクライアントを指定することで、言語別でctagsとLSの切り替えが可能になります。
ところが現状だとctagsを利用したほうが見やすかったりする部分があります。なので適宜状況を見ながら切り替えをしたほうが無難と考えています。
これはLSPやLSの拡張次第でどんどん状況が変わるると予測しています。

ctagsを用いるときはuniversal-ctagsのインストールをお忘れなく。

またlightline.vimで定義を追加することで、現在カーソルにあるシンボル名を[vista.vimから取得してステータスラインにシンボル名を表示する](https://github.com/liuchengxu/vista.vim#show-the-nearest-methodfunction-in-the-statusline)ことが出来ます。

以下に私のlightline設定をおいておきます。

```vim
let g:lightline = {
    \ 'colorscheme': 'iceberg',
    \ 'active': {
    \   'left':  [ ['mode', 'paste'], ['readonly', 'myfilename', 'method', 'modified'], ],
    \   'right': [ [ 'lineinfo' ], [ 'percent' ], ['char_code', 'fileformat', 'fileencoding', 'filetype' ], ],
    \ },
    \ 'component_function': {
    \   'myfilename': 'LightlineFilename',
    \   'method': 'NearestMethodOrFunction',
    \ },
    \ 'separator': { 'left': "\ue0b0", 'right': "\ue0b2" },
    \ 'subseparator': { 'left': "\ue0b1", 'right': "\ue0b3" },
    \ }

function! LightlineFilename()
    let l:p = expand('%:t')
    if '' !=# l:p
        return l:p
    endif
    return '[No Name]'
endfunction

function! NearestMethodOrFunction() abort
    let l:func_name = get(b:, 'vista_nearest_method_or_function', '')
    if l:func_name != ''
        return ' ' . l:func_name
    endif
    return ''
endfunction

augroup LightLineOnVista
    autocmd!
    autocmd VimEnter * call vista#RunForNearestMethodOrFunction()
augroup END
```

## マイクロコードの実行

- [vim-quickrun](https://github.com/thinca/vim-quickrun)

使用したことのない関数やライブラリなどの動作をチェックするために、マイクロコードを作成して実行することはよくあると思います。
その際、Vimから離れてCLIから`python main.py`などして実行する方法がありますが、この方法であると以下のようにいくつかの手間がかかります。

- Vimを離れる必要がある
- エラー発生時に表示された行数とVimで表示しているコードの行数を脳内で一致させる必要がある

すばやくコードを書く上でほぼ必須の機能と言えるのですが、現状LSにはコード実行用の機能がありません。それっぽいメソッドがLSPにあるのですが、少なくともそれを実装しているLSを私は知らないので、誰か知っていたら教えてください。
なので別途プログラムランナーが必要になるのですが、私はvim-quickrunをおすすめします。

vim-quickrunがあればVimで記述したコードをVim上からすばやく実行することができます。これが最速です。
コード全てではなく、ヴィジュアルモードで選択した一部のコードのみでも実行することも可能です。便利ですね。

さてプログラムをVim上から実行するときに、実行中にVimの編集がブロックされるのは避けたいものです。つまりjobが必要になります。
vim-quickrunの作者であるthinca氏はVimのみを利用するユーザであるため、vim-quickrun本体にNeovimのjobを実行する機能は含まれていません。
しかしご安心くださいlambdalisue氏が作成した[vim-quickrun-neovim-job](https://github.com/lambdalisue/vim-quickrun-neovim-job)を追加することでNeovimのjobをvim-quickrunのrunnerとして追加することができるのです

```vim
let g:quickrun_config = {
    \ '_' : {
        \ 'outputter' : 'error',
        \ 'outputter/error/success' : 'buffer',
        \ 'outputter/error/error'   : 'quickfix',
        \ 'outputter/buffer/split' : ':botright 8sp',
    \ }
\}

" VimとNeovimで利用するrunerを変更
if has('nvim')
  let g:quickrun_config._.runner = 'neovim_job'
elseif exists('*ch_close_in')
  let g:quickrun_config._.runner = 'job'
endif
```

## 最小.vimrc(init.vim)

これまで紹介したVimプラグインが使える最小(と言う割には多い)vimrcをご用意しました。
参考にしていただければ幸いです。
なお普段のパッケージマネージャはdein.vimですが、deinのキャッシュを壊したくないので今回はvim-plugを使ったものをご用意しました。

- [LSP利用のための最小vimrc](https://gist.github.com/lighttiger2505/13886b8a2cb03e1191cb50fc15999a3e)

このvimrcを`~/.config/init.vim`を貼るなり、適当なとこに配置して`nvim -u {file}`したあとに、以下のように実行すれば使えると思います。多分。

- nvim起動後に`:PlugInstall`
- 上記の後nvim再起動して`:UpdateRemotePlugins`
- nvim再起動

なお、自分の趣味で`g:python3_host_prog`にインストールされているpylsを利用するようにしています。ご注意を。
パス解決周りの話がわからなかったら以前書いた以下の記事を参照してみると良いでしょう。

- [Pythonのパス解決問題を解消し、pyls(Python Language Server)を導入した on Neovim](https://qiita.com/lighttiger2505/items/29fecc9df0fddc80927a)

## まとめ

長文になりましたが、いかがだったでしょうか？
Vimは言語のインテリセンスが弱いのでちょっと。という方が本記事をみて、おぉVimもこんなことができるのかと知っていただければ幸いです。

それでは皆さんLanguage Serverを使ってより快適なプログラミングをお楽しみください！！