はじめに
Language Server Protocol の理解として、Pyright を LSP サーバとした自作クライアントの作成を行いました(Pyright を LSP サーバとした自作 LSP クライアント(実装編))。その際、Pyright に解析を行わせるための初期化方法がドキュメントには書かれていなかったので、VSCode 拡張用のクライアントをトレースして調査することにしました。
調査方法
Pyright のリポジトリには言語サーバ(packages/pyright)だけでなく、VSCode 拡張用のクライアント(packages/vscode-pyright)が存在します。今回はこの2つをデバッガで実行して調査します。
https://github.com/microsoft/pyright
1. インストール
https://github.com/microsoft/pyright/blob/main/docs/build-debug.md
ます、上の記事にしたがって Pyright をローカルでビルドします。
- Node.js のインストール
git clone https://github.com/microsoft/pyright.git && cd pyrightnpm install
また、拡張機能として Pyright および Pylance を導入している場合には無効にします。
2. デバッグ実行
Pyright を VSCode 拡張としてデバッグ実行します。VSCode のサイドバーから「実行とデバッグ」を選択し、プルダウンメニューから “Pyright extension” を選択、実行します。なお、実行時のオプションについてはプルダウンメニュー横の歯車、あるいは .vscode/launch.json から確認できます。
実行すると、VSCode がもう1つ別のウィンドウで立ち上がります。上部に [拡張機能開発ホスト] と書かれていることを確認します。このウィンドウは現在実行している拡張機能が反映された VSCode になっています。
ブレークポイントが動作することを確認します。packages/vscode-pyright/src/extension.ts:206 にはクライアントからサーバへ再起動を要求するメッセージ送信が実装されているので、ここにブレークポイントを置いてみます。[拡張機能開発ホスト] のウィンドウでコマンドパレットを開き、”Pyright: Restart Server” を実行すると、プログラムが一時停止しておりブレークポイントが機能していることを確認できます。
3. デバッガのアタッチ
2 までの手順では、クライアントのみがデバッガで実行されます。しかし、メッセージを受信した後の処理はサーバ側で行われるため、調査のためにはこちらもデバッガで実行したくなります。extension.ts:66 では、サーバがポート 6600 で建てられているので、ここにデバッガをアタッチします。.png)
「実行とデバッグ」のプルダウンメニューに “Pyright attach server” があるのでこれを “Pyright extension” 実行後に実行すればよいです。.vscode/launch.json の "port": 6600 が先ほど確認したポートと一致することに注意します。.png)
pyright-internal/src/commands/restartServer.ts がサーバ側で再起動コマンドを扱う部分です。ブレークポイントを打って同様にメッセージを送信すると、一時停止することが確認できます。.png)
アタッチできていない場合には、下の画像のように Unbound breakpoint となり一時停止しません。.png)
調査内容
1. Initialize Request
初期化関連の仕様を見ると、メソッド initialize は送信する必要がありそうです。そこでまず次の2つを順に送信してみます。
initializeメソッド:サーバの初期化を要求- 適当な解析メソッド
すると、pyright-internal/src/commands/languageServerBase.ts:417 で停止してしまいました。workspace.isInitialized が true とならないことが原因です。
したがって、initialize メソッドの後に何か他のメソッドを送信する必要がありそうです。
initializeメソッド:サーバの初期化を要求- ???:ワークスペースを初期化
- 適当な解析メソッド
2. DidChangeWorkspaceFolders Notification
調べると、workspace.isInitialized はメソッド updateSettingsForWorkspace が実行されて true となります。
このメソッドは onDidChangeWorkspaceFolders で管理されているので、workspace/didChangeWorkspaceFolders を送信することで呼ばれます。
つまり、手順としては次のようになります。
initializeメソッド:サーバの初期化を要求workspace/didChangeWorkspaceFoldersメソッド:ワークスペースフォルダを変更- 適当な解析メソッド
ただし、onDidChangeWorkspaceFolders は特定の条件で有効化されることに注意します。
3. Initialized Notification
onDidChangeWorkspaceFoldersの前後 を確認すると、有効化には以下の2つの条件を満たす必要があります。
initializedメソッドの送信this.client.hasWorkspaceFoldersCapability = true
1 は明らかに送信するだけです。initialized メソッド は サーバからの InitializeResult に対応するものなので、タイミングは InitializeResult を受け取った後、workspace/didChangeWorkspaceFolders メソッドを送信する前になります。2 はわかりにくいですが、initializeメソッド のオプションに capabilities があるのでここで登録します。つまり、初期化方法は全体で次のようになることがわかりました。
initializeメソッド:サーバの初期化を要求
a.capabilities.workspace.workspaceFolders = true:ワークスペースフォルダ機能を有効化initializedメソッド:クライアント側の初期化が完了したことを通知workspace/didChangeWorkspaceFoldersメソッド:ワークスペースフォルダの変更を通知- 適当な解析メソッド
まとめ
以上から、Pyright の初期化は下図のようにして行われることがわかりました。実装はPyright を LSP サーバとした自作 LSP クライアント(実装編)で扱っていますので、合わせて読んでいただければと思います。