LSP 実装メモ(gopls cache `Snapshot` 編)
前回
先週はやる気が消失したためお休みした。
前回のまとめ
ひたすらに (*Session).NewView()
からの呼び出しを追っていった。
今回は残った (*snapshot).load()
メソッドの処理を読む。
が、全て詳細に読むと長くなりすぎてしまうので、さっくりと読んだ後、Snapshot
は何をするためのものなのかを追う。
(*snapshot).load()
このメソッドは以下の部分に分割できる。
packages.Load()
に渡す引数の準備packages.Load()
の呼び出し- 返り値から必要な情報を抽出して、cache データを構築する
packages.Load()
は第一引数にどう package を parse するかなどのコンフィグを取り、第二引数に parse する package をパターン文字列で指定する。
これらの準備を以下の部分で行っている。
そして packages.Load()
を呼び、以下の部分で parse した package から情報を cache に溜めていく。
このとき cache としての肝となるのは例えば以下のような x/tools/internal/memoize
package を使った部分である。
例えば上記では、builtin package のファイルを parse した結果を s.generation.Bind()
メソッドで登録しておき、s.builtin
フィールドに保持している。
必要になれば s.builtin.handle.Get()
メソッド呼び出しをすれば登録されたハンドラの処理結果を取得できるという訳だ。
x/tools/internal/memoize
package については以下を参照。
snapshot
snapshot 型のフィールドを見てみる。
type snapshot struct { memoize.Arg // allow as a memoize.Function arg id uint64 view *View generation *memoize.Generation builtin *builtinPackageHandle mu sync.Mutex ids map[span.URI][]packageID metadata map[packageID]*metadata importedBy map[packageID][]packageID files map[span.URI]source.VersionedFileHandle goFiles map[parseKey]*parseGoHandle packages map[packageKey]*packageHandle actions map[actionKey]*actionHandle workspacePackages map[packageID]packagePath workspaceDirectories map[span.URI]struct{} unloadableFiles map[span.URI]struct{} parseModHandles map[span.URI]*parseModHandle modTidyHandles map[span.URI]*modTidyHandle modUpgradeHandles map[span.URI]*modUpgradeHandle modWhyHandles map[span.URI]*modWhyHandle modules map[span.URI]*moduleRoot workspaceModuleHandle *workspaceModuleHandle }
source.VersionedFileHandle
以外の .*Handle
型は(全部は見てないので恐らく)先に説明した memoize package による cache を保持するための型だ。
つまり Go のコードとしての情報はほとんどこの型に保持されているので、LSP メソッドの処理はこの snapshot を取得するところから始まると推測される。
まとめ
少し詳細に踏み込みすぎて目的を見失ってしまった感があったが、あくまで LSP サーバ実装の一例としての cache 機構がどのようになっているのかを gopls から学びたかったのだ。
LSP サーバを書く際 TextDocument の扱いで私が気になっていたのは以下の点である。
- いつその言語の文字列として parse するか
textDocument/didOpen
通知が飛んできたらその URI のファイルを開き、parse までして cache を構築する- ただし goroutine で非同期に処理される
- いつ関連するファイル(import された package など)を開くか
- cache を構築するときに必要なので、同じタイミングで行なわれる
- parse 結果をどのように cache するか
- ディスク上のファイルは Cache(fileHandle) に保持される
- LSP により送信された TextDocument は Session(overlays) に保持される
- Go のコードとして parse された結果は View(snapshot) に保持される
gopls cache 編は一旦ここまでとする。
次回からは LSP 仕様とその実装を追うことに戻るが、何から読んでいこうか。