Vim で PHP を書くときは vim-lspで Intelephense を使っているのですが、プロプライエタリなソフトウェアなのでサーバサイドのコードが公開されていません。Intelephense のウェブサイトはあまり親切ではなく、初めてインストールしたときは正しく動かせているのかよく分からなかった事を覚えています。
そのため OSS の PHP LSP を探していたところ Serenataというソフトウェアを見つけました。
Serenata は Atom エディタのプラグインとして公開されていたようです。サーバ型の Serenata 本体と、Atom プラグインの php-ide-serenataの2つで構成されています。バージョン4までは Atom エディタ用ですがバージョン5で Language Server Protocol をサポートしたようです。
対応している LSP の機能は Language Server Protocol Support Table のページで確認できます。
php-ide-serenataの動作画面
今回はこの Serenata をvim-lspから使えるように設定してみました。
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 でインストール可能です。
$ 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を選択してください。
2. ポート 11111
で起動
Serenata サーバをポート 11111
で起動してみましょう。Serenata はソースコードの解析結果をデータベースに保存します。デフォルト設定では SQLite をインメモリで使用します。メモリ使用量が多いので php
の起動オプションでメモリを多めに確保します。
$ php -dmemory_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
、ファイル最終行の行末には改行を含めないようにしてください。
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
で始まるレスポンスが返ってくれば成功です。
$ 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 の設定を追加してください。
auUser 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 エディタで使うより遅いのでもう少し改善の余地がありそうです。
また成果があれば公開します。