LSP 実装メモ(gopls cache `Snapshot` 編)

前回

tennashi.hatenablog.com

先週はやる気が消失したためお休みした。

前回のまとめ

ひたすらに (*Session).NewView() からの呼び出しを追っていった。

今回は残った (*snapshot).load() メソッドの処理を読む。
が、全て詳細に読むと長くなりすぎてしまうので、さっくりと読んだ後、Snapshot は何をするためのものなのかを追う。

(*snapshot).load()

https://github.com/golang/tools/blob/5d1fdd8fa3469142b9369713b23d8413d6d83189/internal/lsp/cache/load.go#L43-L199

このメソッドは以下の部分に分割できる。

  • packages.Load() に渡す引数の準備
  • packages.Load() の呼び出し
  • 返り値から必要な情報を抽出して、cache データを構築する

packages.Load() は第一引数にどう package を parse するかなどのコンフィグを取り、第二引数に parse する package をパターン文字列で指定する。

これらの準備を以下の部分で行っている。

https://github.com/golang/tools/blob/5d1fdd8fa3469142b9369713b23d8413d6d83189/internal/lsp/cache/load.go#L44-L139

そして packages.Load() を呼び、以下の部分で parse した package から情報を cache に溜めていく。

https://github.com/golang/tools/blob/5d1fdd8fa3469142b9369713b23d8413d6d83189/internal/lsp/cache/load.go#L166-L194

このとき cache としての肝となるのは例えば以下のような x/tools/internal/memoize package を使った部分である。

https://github.com/golang/tools/blob/5d1fdd8fa3469142b9369713b23d8413d6d83189/internal/lsp/cache/snapshot.go#L1369-L1389

例えば上記では、builtin package のファイルを parse した結果を s.generation.Bind() メソッドで登録しておき、s.builtin フィールドに保持している。
必要になれば s.builtin.handle.Get() メソッド呼び出しをすれば登録されたハンドラの処理結果を取得できるという訳だ。

x/tools/internal/memoize package については以下を参照。

godoc.org

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 仕様とその実装を追うことに戻るが、何から読んでいこうか。