Help us understand the problem. What is going on with this article?

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

tl;dr

私は以前、NeovimでモダンなPython環境を構築するという記事を投稿しました。

上記記事の投稿から1年8ヶ月が経過し、LSPや新たなVimの機能などによりVimを取り巻く環境には次々と大きな変化が訪れていることを日々感じており、VimConf 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)

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

pip install python-language-server

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

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

" 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)には対応しておらず、
別途プラグインが必要となります。

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

pip3 install pyls-isort
pip3 install pyls-black

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

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設定ファイルにて個別設定することをおすすめします。

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

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

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

" 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)を追加することで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)

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

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

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を用いた補完が行いたいのであれば、それぞれの補完ソースを導入する必要性があります。

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

asyncomplete.vim

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

con.nvim

coc.nvimは現存するVimのLSクライアントの中でもひときわ大きなプロジェクトの一つになっています。
元々はcoc(Conquer Of Completion)の名の通り、自動補完用のフレームワークであったと思いますが、現在ではLSを通したインテリセンスをVim上で全て実現するための統合環境。VimにおけるIDE機能の実装とも呼ぶべきものになっており、自動補完は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のエコシステムへの変化は現状も続いています。
あるいはあなたがこれから作成するプラグインによってこのような状況に一石を投じるといったことも可能かもしれません。

アウトライン表示

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

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

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から取得してステータスラインにシンボル名を表示することが出来ます。

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

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から離れて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を追加することでNeovimのjobをvim-quickrunのrunnerとして追加することができるのです

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を使ったものをご用意しました。

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

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

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

まとめ

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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away