ようやく vim-lsp に乗り換えた話 (pylsp-mypy で苦労した話)

RubyKaigi Takeout 2021 に参加して、typeprof などをフルに利用するには LSP を使えるようにしておく必要があるな、と気づきました。
思い立ったが吉日ということで、自分の vim 環境を ALE から LSP に乗り換えることにしました。

その過程で、mypy による lint を有効にするのに苦労したので、備忘録としてメモを残します。

## 前提

* python 使い
* ALE は lint 用に使っていて、formatter としては使っていない
* flake8, isort, mypy を linter として使っている
* vim の plug-in manager には dein を使っている

## LSP を有効にする

本体である [vim-lsp](https://github.com/prabirshrestha/vim-lsp) を使います。
細かい設定をやってくれる [mattn/vim-lsp-settings](https://github.com/mattn/vim-lsp-settings) も合わせて入れます。

```toml
# LSP (Language Server Protocol)
[[plugins]]
repo = 'prabirshrestha/vim-lsp'

# LSP settings
[[plugins]]
repo = 'mattn/vim-lsp-settings'
```

これで基本的な動きはオッケーです。

このタイミングで ALE をリストから削除しています。

## python の linter を pycodestyle から flake8 に変更する

python 向けの設定をしていきます。

これまで flake8 を使って lint をしていたので、vim-lsp-settings のデフォルトである pycodestyle から flake8 を利用するように変更します。

`g:lsp_settings` 経由で設定をします。vim-lsp-settings はデフォルトで pylsp-all という設定(?)を使っているので、 `pylsp-all.workspace_config` 以下に設定を書き加えます。

* ロードするプラグインの切り替え
    * `pylsp-all.workspace_config.plugins` でデフォルトのプラグインを無効化し、かわりに flake8 を有効化します。
        * mccabe, pycodestyle, pyflakes を `enabled: 0` に設定します。
        * flake8 を `enabled: 1` に設定します。
* flake8 の設定ファイルを自動ロードする設定の追加
    * `pylsp-all.workspace_config.pylsp.configurationSources` を `['flake8']` と設定します。
    * これにより flake8 の設定ファイル(setup.cfg, tox.ini など)を探索するようになります

```toml
# LSP settings
[[plugins]]
repo = 'mattn/vim-lsp-settings'
hook_add = '''
  " Enable flake8
  let g:lsp_settings = {
  \  'pylsp-all': {
  \    'workspace_config': {
  \      'pylsp': {
  \        'configurationSources': ['flake8'],
  \        'plugins': {
  \          'flake8': {
  \            'enabled': 1
  \          },
  \          'mccabe': {
  \            'enabled': 0
  \          },
  \          'pycodestyle': {
  \            'enabled': 0
  \          },
  \          'pyflakes': {
  \            'enabled': 0
  \          },
  \        }
  \      }
  \    }
  \  }
  \}
'''
```

※ 僕は設定を一箇所にまとめたいので、プラグインの設定を `hook_add` を使って plug-in のロードの隣に定義を書いていますが、 .vimrc に直接書いてもよいはずです。


flake8 を有効にする(`enabled: 1`)だけでは pycodestyle や pyflakes による lint が行われるため、すべての linter で lint の設定をする必要があります。例えば、自分のプロジェクトでは max-line-length を長めに設定しているのですが、

* 設定により　flake8 はエラーとならない
* 特に設定されていない pycodestyle, pyflakes は行長エラーを出す

という動きになっており、意図した lint 結果が得られませんでした。

重複しているツールが起動しているのは意図したものではないため、flake8 とかぶっている 3ツール(mccabe, pycodestyle, pyflakes8)は無効化することにしました。

### 余談: pyls (python-language-server) と pylsp

vim-lsp-settings の設定を調べていて混乱したのが pyls と pylsp の違いです。
なぜ似たような設定が２つあるのか、何が違うのか、というのがわからなくてググったりコード読んだりして、しばらくぐるぐるしていました。

どうやら、昨年末までは [python-language-server](https://github.com/palantir/python-language-server) がよく利用されていたようなのですが、メンテナンスが停止してしまったためにプロジェクトが　fork されて、現在では [python-lsp-server](https://github.com/python-lsp/python-lsp-server) として再公開されているようです。
vim-lsp-settings では前者を pyls、後者を pylsp と呼んでいるようです。

なお、この他にも Microsoft が管理している [microsoft/python-language-server](https://github.com/microsoft/python-language-server) というものもあるようですが、深追いしていないのでここでは割愛します。

### 余談: flake8 プラグインを書き換える

自前で　flake8 プラグインを作成している場合は、pylsp 経由では意図通りに動かない可能性があります。
通常の実行では保存されているファイルを対象にしていたものが、stdin 経由での呼び出しに対応していない場合はクラッシュすることになります。

pylsp 経由で flake8 が動かない場合、以下のように stdin 経由で flake8 にファイルを与えてクラッシュしないか確認すると良いでしょう。

```bash
$ cat /path/to/file | flake8 -
```

## mypy を有効にする

続いて mypy を有効にします。ここがハマりポイントでした。

ググって出てくる記事にも、python-lsp-server の README にも、mypy を有効にするには [pyls-mypy](https://github.com/tomv564/pyls-mypy) を使えと書いてあるのですが、現時点(2021/9/11)時点ではこれは誤りです。

python-lsp-server で mypy を有効にするには [pylsp-mypy](https://github.com/Richardk2n/pylsp-mypy) をインストールしてください　（一文字違い)。

```bash
$ /Users/tkomiya/.local/share/vim-lsp-settings/servers/pylsp-all/venv/bin/pip install pylsp-mypy
```

その後、 `g:lsp_settings` の plug-in リストに pylsp_mypy を追加します。

```toml
hook_add = '''
  " Enable flake8 and mypy
  let g:lsp_settings = {
  \  'pylsp-all': {
  \    'workspace_config': {
  \      'pylsp': {
  \        'configurationSources': ['flake8'],
  \        'plugins': {
  \          'flake8': {
  \            'enabled': 1
  \          },
  \          'mccabe': {
  \            'enabled': 0
  \          },
  \          'pycodestyle': {
  \            'enabled': 0
  \          },
  \          'pyflakes': {
  \            'enabled': 0
  \          },
  \          'pylsp_mypy': {
  \            'enabled': 1
  \          }
  \        }
  \      }
  \    }
  \  }
  \}
'''
```

### 課題: pylsp-mypy のインストールを自動化する

今回は pylsp-mypy を手動でインストールしていますが、このアプローチでは環境を作り直すたびに手動でインストールする必要があります。
この部分を自動化できるとよいのですが、やり方が分かっていません。

そのため、 `LspInstallServer` を実行するたびに pylsp-mypy が消えてしまうんですよね。

## 課題: isort を有効化する

isort を使ってソースコードを lint しようと思ったのですが、今の所実現できていません。
[pyls-isort](https://github.com/paradoxxxzero/pyls-isort) は linter としては動かない(フォーマッタ専用)ようなので、いまのところ有効になっていません。

[flake8-isort](https://pypi.org/project/flake8-isort/) を入れても意図したように動いていないので、もう少し試行錯誤が必要そうです。実際のところ、もう手癖になってしまったので isort なしでも困らないのですが、つまらないミスで CI でこけないようにするためにもチェックしておきたいところです。

## LSP のエラー、警告を lightline に表示する

LSP が検知した文法エラーや警告などを lightline に表示します。

必要な定義は [lightline-lsp](https://github.com/halkn/lightline-lsp) としてパッケージングされているので、これを入れます。

```toml
# LSP-lightline bridge
[[plugins]]
repo = 'halkn/lightline-lsp'
```

あとは、この定義を lightline から参照するだけです。 `lightline_lsp#warnings`, `lightline_lsp#errors` などを参照すればオッケーです。

```toml
# Customize status line
[[plugins]]
repo = 'itchyny/lightline.vim'
hook_add = '''
  let g:lightline = {
  \  'mode_map': {'c': 'NORMAL'},
  \  'active': {
  \    'right': [
  \      [ 'lsp_errors', 'lsp_warnings' ],
  \    ]
  \  },
  \  'component_expand': {
  \    'lsp_warnings': 'lightline_lsp#warnings',
  \    'lsp_errors':   'lightline_lsp#errors',
  \  },
  \  'component_type': {
  \    'lsp_warnings': 'warning',
  \    'lsp_errors':   'errors',
  \  },
  \}
'''
```

いままで [lightline-ale](https://github.com/maximbaz/lightline-ale) を使っていたので、参照する関数を差し替えるだけだったのでかんたんでした。

## vim-lsp の表示を変更する (ハイライト)

vim-lsp はデフォルトでは signcolumns を使って lint の警告/エラーを表示します。
僕はエラー行をハイライト表示するのに慣れてしまったので、この表示には違和感があります (ALE でも同様の設定をしていた)。
そのため、以下のような設定を加えました。

```toml
# LSP settings
[[plugins]]
repo = 'mattn/vim-lsp-settings'
hook_add = '''
  " Hide signcolumn.
  let g:lsp_diagnostics_signs_enabled = 0
'''
hook_post_source = '''
  " Highlight LSP warnings strongly (like errors)
  highlight link LspWarningHighlight Error
'''
```

`g:gls_diagnostics_sign_enabled` で signcolumns を無効化しています。
そして `LspWarningHighlight` の色を変えることでハイライト表示にしています。
ハイライトの色などはもう少しいい感じのものに調整したほうが良さそうですが、いまのところ `Error` と同じにしています。

他にもいくつかハイライトが定義されているので、highlight コマンドで眺めつついい感じの色を設定すると良さそうです。
いまのところ `LspWarningHighlight` だけアップデートすればよさそうでした。

## エラーの詳細をステータス行に表示する

lint で警告/エラーが発生している行にカーソルを当てたらエラーの詳細を知りたいですよね。
`g:lsp_diagnostics_echo_cursor` を有効にします。

```toml
# LSP settings
[[plugins]]
repo = 'mattn/vim-lsp-settings'
hook_add = '''
  " Show diagnostics message to status line
  let g:lsp_diagnostics_echo_cursor = 1
'''
```

## オートコンプリートを有効にする

ついでにオートコンプリートを LSP 経由のものに置き換えます。

[asyncomplete](https://github.com/prabirshrestha/asyncomplete.vim) と [asyncomplete-lsp](https://github.com/prabirshrestha/asyncomplete-lsp.vim) を入れるだけで有効になります。

```toml
# Auto completion
[[plugins]]
repo = 'prabirshrestha/asyncomplete.vim'

# Auto completion for LSP
[[plugins]]
repo = 'prabirshrestha/asyncomplete-lsp.vim'
```

※ [deoplete.nvim](https://github.com/Shougo/deoplete.nvim) + [deoplete-vim-lsp](https://github.com/lighttiger2505/deoplete-vim-lsp) を使う方法もあるようですが、試していないため割愛します。

## 余談: pylsp にデバッグログを出力させる

flake8 が意図通り動いていないときに、pylsp の状態を知りたくなったため、デバッグログを出力させることにしました。
`g:lsp_settings` 経由で `pylsp-all.args` に pylsp コマンドの引数を指定できます。
デバッグログを得るには `--log-file` と `-v` オプションを適量指定すると良さそうです (調査の際は 3つ指定してました)。

```
# LSP settings
[[plugins]]
repo = 'mattn/vim-lsp-settings'
hook_add = '''
  let g:lsp_settings = {
  \  'pylsp-all': {
  \    'args': ['--log-file=pylsp.log', '-vvv']
  \  }
  \}
```

## まとめ

最終的には以下の設定になりました。

```toml
# LSP (Language Server Protocol)
[[plugins]]
repo = 'prabirshrestha/vim-lsp'

# LSP settings
[[plugins]]
repo = 'mattn/vim-lsp-settings'
hook_add = '''
  " Hide signcolumn.
  let g:lsp_diagnostics_signs_enabled = 0
  " Show diagnostics message to status line
  let g:lsp_diagnostics_echo_cursor = 1
  " Enable flake8 and mypy
  let g:lsp_settings = {
  \  'pylsp-all': {
  \    'workspace_config': {
  \      'pylsp': {
  \        'configurationSources': ['flake8'],
  \        'plugins': {
  \          'pylsp_mypy': {
  \            'enabled': 1
  \          }
  \        }
  \      }
  \    }
  \  }
  \}
'''
hook_post_source = '''
  " Highlight LSP warnings strongly (like errors)
  highlight link LspWarningHighlight Error
'''

# LSP-lightline bridge
[[plugins]]
repo = 'halkn/lightline-lsp'

# Auto completion
[[plugins]]
repo = 'prabirshrestha/asyncomplete.vim'

# Auto completion for LSP
[[plugins]]
repo = 'prabirshrestha/asyncomplete-lsp.vim'

# Customize status line
[[plugins]]
repo = 'itchyny/lightline.vim'
hook_add = '''
  let g:lightline = {
  \  'mode_map': {'c': 'NORMAL'},
  \  'active': {
  \    'right': [
  \      [ 'lsp_errors', 'lsp_warnings' ],
  \    ]
  \  },
  \  'component_expand': {
  \    'lsp_warnings': 'lightline_lsp#warnings',
  \    'lsp_errors':   'lightline_lsp#errors',
  \  },
  \  'component_type': {
  \    'lsp_warnings': 'warning',
  \    'lsp_errors':   'errors',
  \  },
  \}
'''
```