Atom の PHP 補完を vim-lsp で使う

Vim で PHP を書くときは [vim-lsp](https://github.com/prabirshrestha/vim-lsp) で Intelephense を使っているのですが、プロプライエタリなソフトウェアなのでサーバサイドのコードが公開されていません。[Intelephense のウェブサイト](https://intelephense.com/)はあまり親切ではなく、初めてインストールしたときは正しく動かせているのかよく分からなかった事を覚えています。
そのため OSS の PHP LSP を探していたところ [Serenata](https://serenata.gitlab.io/) というソフトウェアを見つけました。

Serenata は Atom エディタのプラグインとして公開されていたようです。サーバ型の Serenata 本体と、Atom プラグインの [php-ide-serenata](https://atom.io/packages/php-ide-serenata) の2つで構成されています。バージョン4までは Atom エディタ用ですがバージョン5で Language Server Protocol をサポートしたようです。
対応している LSP の機能は [Language Server Protocol Support Table のページ](https://gitlab.com/Serenata/Serenata/-/wikis/Language%20Server%20Protocol%20Support%20Table) で確認できます。

*php-ide-serenataの動作画面*

![php-ide-serenata.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/c03b8378-5883-c9e6-d5f3-082d012e59bb.png)

今回はこの Serenata をvim-lspから使えるように設定してみました。

*vim-lsp の動作画面*
<img width="968" alt="serenata_completion.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/1e32b882-b55e-c0b2-a4c8-d3efcbf879fe.png">

Serenata はTCPソケットしかサポートされていなかったので、vim-lsp の標準入出力とつなげるために `socat` コマンドを使いました。
ひとまず vim 側での補完やホバー表示ができるところまでは確認したのですが、速度面で難があったり、vim-lsp の起動`initialize`／終了`exit`とうまく連動できないので実用レベルには至りませんでした。 もう少し調査して使えるようにしたいのですが一旦ここまでの記録を公開します。

## 事前準備
vim-lsp が動く状態になっていることが前提です。その他に必要なソフトウェアは以下の通りです。

- PHP 7.1+
  - mbstring
  - xml
  - libxml
  - dom
  - openssl
  - pdo_sqlite
- `socat` コマンド
- vim
  - vim-lsp

`socat` コマンドがインストールされているか確認してください。macOS は brew でインストール可能です。

```bash
$ which socat
/usr/local/bin/socat
$ socat -V
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
socat version 1.7.3.3 on May 11 2019 02:21:21
...
$ brew install socat # インストールされていなければ
```

## Serenata の導入

### 1. 実行可能な PHAR 形式のファイルをダウンロード
Serenata サーバをダウンロードしてください。インストール方法はいくつかありますが実行可能な PHAR 形式のファイルが簡単です。[PHAR compatible with PHP 7.x](https://gitlab.com/Serenata/Serenata/-/tags) を選択してください。

### 2. ポート `11111` で起動
Serenata サーバをポート `11111` で起動してみましょう。Serenata はソースコードの解析結果をデータベースに保存します。デフォルト設定では SQLite をインメモリで使用します。メモリ使用量が多いので `php` の起動オプションでメモリを多めに確保します。

```bash
$ php -d memory_limit=2048M ~/bin/serenata.phar --uri tcp://127.0.0.1:11111
Starting server bound to socket on URI tcp://127.0.0.1:11111...
```

正常に起動できれば `Starting server bound ....` と表示されて LISTEN 状態になります。

### 3. 動作確認
動作確認のために `initialize` メソッドを実行してみます。`socat` を使って Serenata サーバのポート `11111` に送信します。`Content-Length` には JSON のバイト数が書かれていますので、`rootPath` `rootUri` を変更したら `Content-Length` も忘れずに変更してください。1バイトでも合わないと正しく動作しません。
また、保存するファイルのフォーマットにも注意してください。LSP の仕様で改行コードは `CRLF` 、ファイル最終行の行末には改行を含めないようにしてください。

```json:serenata.jsonrpc
Content-Length: 1429

{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":19002,"rootPath":"/Users/foobar/src/foobar/foobarbazfo/","rootUri":"file:///Users/foobar/src/foobar/foobarbazfo/","workspaceFolders":[],"capabilities":{"workspace":{"applyEdit":true,"configuration":false,"workspaceEdit":{"documentChanges":true},"workspaceFolders":false,"didChangeConfiguration":{"dynamicRegistration":false},"didChangeWatchedFiles":{"dynamicRegistration":false},"symbol":{"dynamicRegistration":false},"executeCommand":{"dynamicRegistration":false}},"textDocument":{"synchronization":{"dynamicRegistration":false,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":false,"completionItem":{"snippetSupport":true,"commitCharactersSupport":false},"contextSupport":true},"hover":{"dynamicRegistration":false},"signatureHelp":{"dynamicRegistration":false},"references":{"dynamicRegistration":false},"documentHighlight":{"dynamicRegistration":false},"documentSymbol":{"dynamicRegistration":false,"hierarchicalDocumentSymbolSupport":true},"formatting":{"dynamicRegistration":false},"rangeFormatting":{"dynamicRegistration":false},"onTypeFormatting":{"dynamicRegistration":false},"definition":{"dynamicRegistration":false},"codeAction":{"dynamicRegistration":false},"codeLens":{"dynamicRegistration":false},"documentLink":{"dynamicRegistration":false},"rename":{"dynamicRegistration":false}},"experimental":{}}}}
```
*動作確認するだけならパスの変更は不要です*

実行して `Content-Length` で始まるレスポンスが返ってくれば成功です。

```bash
$ socat stdio tcp4:127.0.0.1:11111,shut-none < serenata.jsonrpc
Content-Length: 951

{"jsonrpc":"2.0","id":0,"result":{"capabilities":{"textDocumentSync":{"openClose":false,"change":1,"willSave":false,"willSaveWaitUntil":false,"save":{"includeText":true}},"hoverProvider":true,"completionProvider":{"resolveProvider":false,"triggerCharacters":null},"signatureHelpProvider":{"triggerCharacters":["(",","]},"definitionProvider":true,"typeDefinitionProvider":false,"implementationProvider":false,"referencesProvider":false,"documentHighlightProvider":true,"documentSymbolProvider":true,"workspaceSymbolProvider":false,"codeActionProvider":false,"codeLensProvider":{"resolveProvider":true},"documentFormattingProvider":false,"documentRangeFormattingProvider":false,"documentOnTypeFormattingProvider":null,"renameProvider":false,"documentLinkProvider":null,"colorProvider":false,"foldingRangeProvider":false,"executeCommandProvider":null,"workspace":{"workspaceFolders":{"supported":false,"changeNotifications":false}},"experimental":null}}}
```

## vim-lsp 設定

### 1. `lsp#register_server` の設定
Serenata が正しく動いていることが確認できたので vim-lsp の設定を追加してください。

```vim
au User lsp_setup call lsp#register_server({
    \ 'name': 'serenata',
    \ 'cmd': {server_info->['socat', 'stdio', 'tcp4:127.0.0.1:11111,shut-none']},
    \ 'initialization_options': {"rootPath":"/Users/foobar/src/foobar/foobarbazfo/","rootUri":"file:///Users/foobar/src/foobar/foobarbazfo/"},
    \ 'whitelist': ['php'],
    \ 'workspace_config': { 'serenate': {
    \   'files.associations': ['*.php'],
    \ }},
    \ })
```

### 2. 動作確認
Serenataサーバを起動した状態で vim で PHP のファイルを開きます。vim-lsp が Serenata サーバに正しく接続できれば `:LspStatus` の実行結果が `serenata: running` になります。vim-lsp と Serenata の起動・終了の仕様に齟齬があるので、vim を終了した場合は Serenata サーバも再起動しないと `LspStatus` の結果が `failed` になります。

## まとめ
Serenata はサーバ型なので LSP の `exit` メソッド実行されても終了せず、vim-lsp が再度 `initialize` メソッドを実行するとエラーになります。速度も Atom エディタで使うより遅いのでもう少し改善の余地がありそうです。
また成果があれば公開します。

## その他のスクリーンショット
*クラス名の補完*
<img width="650" alt="serenata_completion_type.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/e3d7eeb2-1052-2ea4-98ba-98d86d7bd678.png">
*メソッド補完*
<img width="498" alt="selenata_completion_method.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/45ab3991-7fe5-d8bb-81ce-ecc0b0d5015e.png">
*クラスのホバー表示*
<img width="450" alt="serenata_hover_type.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/051c5597-a1dd-06cb-78b1-e66f1f01d1de.png">
*メソッドのホバー表示*
<img width="418" alt="serenata_hover_method.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/ca465506-0c43-69c5-1c5d-1981d6496430.png">