LSP 実装メモ(gopls cache `View` 生成編)
前回
引き続き、gopls の cache 実装を読む。
前回のまとめ
gopls で採用されている cache は 3 層で前回読んだのは Cache/Session の 2 層だ。
- Cache
- 素朴な OS ファイルシステム上のファイルの cache
- cache 自体の実体は
source.FileHandle
型(の実装)
- Session
- LSP の Text Document Synchronization でやりとりされる TextDocument の cache
- cache 自体の実体は
source.Overlay
型(の実装)
Session に cache が登録されるときに、その TextDocument の URI から OS ファイルシステム上のファイルを Cache に cache することを注意しておく。
Session の定義は以下だ。
source.Session
interface
type Session interface { NewView(ctx context.Context, name string, folder span.URI, options Options) (View, Snapshot, func(), error) View(name string) View ViewOf(uri span.URI) (View, error) Views() []View Shutdown(ctx context.Context) GetFile(ctx context.Context, uri span.URI) (FileHandle, error) DidModifyFiles(ctx context.Context, changes []FileModification) ([]Snapshot, []func(), []span.URI, error) Overlays() []Overlay Options() Options SetOptions(Options) }
cache.Session
型
type Session struct { cache *Cache id string options source.Options viewMu sync.Mutex views []*View viewMap map[span.URI]*View overlayMu sync.Mutex overlays map[span.URI]*overlay gocmdRunner *gocommand.Runner }
今回はこの Session からさらに生成される View について読む。
View
View は設定と設定された package を持つものだ。
cf. https://github.com/golang/tools/blob/master/gopls/doc/implementation.md#viewsessioncache
いよいよよく分からない、のでさっさとコードを読む。
type View interface { Session() Session Name() string Folder() span.URI ModFile() span.URI BackgroundContext() context.Context Shutdown(ctx context.Context) AwaitInitialized(ctx context.Context) WriteEnv(ctx context.Context, w io.Writer) error RunProcessEnvFunc(ctx context.Context, fn func(*imports.Options) error) error Options() Options SetOptions(context.Context, Options) (View, error) Snapshot(ctx context.Context) (Snapshot, func()) Rebuild(ctx context.Context) (Snapshot, func(), error) ValidBuildConfiguration() bool IsGoPrivatePath(path string) bool IgnoredFile(uri span.URI) bool }
type View struct { session *Session id string optionsMu sync.Mutex options source.Options mu sync.Mutex baseCtx context.Context backgroundCtx context.Context cancel context.CancelFunc name string folder span.URI root span.URI importsMu sync.Mutex processEnv *imports.ProcessEnv cleanupProcessEnv func() cacheRefreshDuration time.Duration cacheRefreshTimer *time.Timer cachedModFileIdentifier string cachedBuildFlags []string filesByURI map[span.URI]*fileBase filesByBase map[string][]*fileBase snapshotMu sync.Mutex snapshot *snapshot initialized chan struct{} initCancelFirstAttempt context.CancelFunc initializationSema chan struct{} initializeOnce *sync.Once initializedErr error hasValidBuildConfiguration bool modURI, sumURI span.URI tmpMod bool hasGopackagesDriver bool gocache, gomodcache, gopath, goprivate string goEnv map[string]string }
コメントは省略したが、session.View
interface には以下のようなコメントがある。
// 原文はリンク先を参照してほしい、ここでは訳を書いておく。
// View は単一のワークスペースを表現する。 // これは、ワーキングディレクトリやビルドタグのような設定値を保つためのレベルだ。
View の生成
これを確かめるために View の生成がどこで行われているかを追っていく。
View の生成は (Session).NewView()
メソッドにより行われる。
このメソッドは (*Server).addView()
からのみ呼ばれており、さらに addView()
は (*Server).addFolders()
からのみ呼ばれる。
この addFolders()
メソッドは以下の3ヶ所で呼ばれている。
それぞれ以下の LSP メソッドと対応している。
workspace/didChangeWorkspaceFolders
initialized
- クライアントからサーバへ、対応機能の合意を取るための
initialize
というリクエストがあるのだが、このレスポンスをクライアントが受け取ったことをサーバへ通知する
- クライアントからサーバへ、対応機能の合意を取るための
textDocument/didOpen
とにかく、クライアントが何か TextDocument を開いたときに叩かれるものだと思えばよい。
gopls が initialize
ハンドラでなく initialized
ハンドラで addFolders()
を呼んでいるのは初期化フェイズが完全に完了したことを gopls が確認するためである。
initialize()
ハンドラでは pendingFolders
というフィールドに一時的に溜めておき、initialized()
ハンドラで実際に View を作成する。
didOpen()
から始めて View の生成を追っていく。
didOpen()
では通知された URI を含むディレクトリごと addFolders()
に渡している。
cf. https://github.com/golang/tools/blob/c1903db4dbfe5ab8e2ec704e203535dae53c2adc/internal/lsp/text_synchronization.go#L101-L106
addFolders()
ではディレクトリ毎に addView()
を呼んでいる。
cf. https://github.com/golang/tools/blob/d1954cc86c824f62dd7845fadb8b09b089425026/internal/lsp/general.go#L202
で、addFolders()
は最終的に NewView()
を呼び、そのディレクトリで View を生成する。
cf. https://github.com/golang/tools/blob/c1903db4dbfe5ab8e2ec704e203535dae53c2adc/internal/lsp/workspace.go#L41
結果として View がディレクトリ単位で生成されることが分かる。
ワークスペース、とは
ここまで濁してきたのだが、ワークスペースとはなんだろうか。
例えば、initialize
リクエストで送信される、InitializeParams
に書かれた以下のコメントを読む。
/** * ワークスペースの rootUri。開いているフォルダが無い場合 * は null。`rootPath` と `rootUri` が両方設定されている場合 * は `rootUri` が勝つ。 */
ワークスペースとは特定のディレクトリのことで、その root が初期化時に渡されるのだろうと分かる。
一方、LSP 仕様にはワークスペースフォルダという言葉も出現しており、例えば以下のようなところで見られる。
https://microsoft.github.io/language-server-protocol/specification#workspace_workspaceFolders
全体の訳は書かないが、適当にかいつまむと、
- さまざまなツールがワークスペース毎に複数のルートフォルダをサポートする
- このような機能を持つエディタが複数のワークスペースフォルダを開くときには
InitializeParam
でworkspaceFolders
に入れてくれ
細かいニュアンスは原文を参照してもらいたいが、ワークスペースのルートフォルダ == ワークスペースフォルダと読める。
先の workspace/didChangeWorkspaceFolders
はこれを追加、削除するための通知だ。
LSP 仕様的にはワークスペースフォルダとはただのディレクトリではなくプロジェクトルートのような意味合いで書いているように読める。
では、Go での対応する概念はなんだろうか。
先に述べたとおり session.View
は単一のワークスペースを表わす。
cf. https://github.com/golang/tools/blob/master/internal/lsp/source/view.go#L122
その実装である cache.View
には root
というフィールドがあり、ここには GOPATH mode ならただのディレクトリで、module-aware mode なら module root が入る。
cf. https://github.com/golang/tools/blob/39188db5885871ea0132a6314eab0c1b5d179a8b/internal/lsp/cache/view.go#L63-L65
つまり module(module-aware mode なら) または package(GOPATH mode なら) 単位であると思えばよさそうだ。
ただし、View 自体はディレクトリ毎に生成される点はここまで読んだ通りだ。
ここまでで、View が単一ディレクトリの何かしらの意味での cache であることまでが分かった。
長くなってしまったので今回はここまで。
次回は View がどのような意味での cache であるのかを明らかにしたい。