LSP 実装メモ(gopls cache `Cache` `Session` 編)
前回
今回から少し gopls の cache 機構を集中して読んでいく。
というのも結局 LSP サーバの実装の肝は
- クライアントから受け取った TextDocument の中身をいつパースするか
- どのようにパースするか
- どのように保持するか
- どうやって TextDocument の中身にアクセスするか
なはずで、gopls の場合この cache 機構を読み解くことがこの大部分の理解に繋がると思われるからである。
cache 機構に関連して参考になるドキュメントは以下である。
gopls の cache は 3層になっており、それぞれ
- Cache
- Session
- View
と名付けられており、各々が互いに参照及び構築できる。
各々について、もう少し詳細を見ていく。
Cache
cache の最下層にある構造。
ファイルシステムやそのコンテンツの情報などのグローバルな情報を保持する、らしいがよく分からないので、gopls 内の実装も読んでみる。
型定義
Cache
型は以下のように定義されている。
type Cache struct { id string fset *token.FileSet options func(*source.Options) store memoize.Store fileMu sync.Mutex fileContent map[span.URI]*fileHandle }
メソッド
また、この型の公開されたメソッドは以下である。
func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) func (c *Cache) NewSession(ctx context.Context) *Session func (c *Cache) FileSet() *token.FileSet
他にも MemStats()
/PackageStats()
/ID()
というメソッドが存在するが debug ページの表示にしか使われていないため除外する。
// cf.
https://github.com/golang/tools/tree/master/internal/lsp/debug
source.FileHandle
型
GetFile()
に返される source.FileHandle
とは次で定義される interface 型である。
type FileHandle interface { URI() span.URI Kind() FileKind FileIdentity() FileIdentity Read() ([]byte, error) }
FileKind
は対象のファイルが .go
ファイルなのか go.mod
なのか go.sum
なのかを区別するための型で、FileIdentity
は URI とその中身の Hash を保持し、テキストを特定するための型である。
また Read()
でファイルの中身を []byte
で取り出せる。
つまりファイルそのものであると思える。
token.FileSet
型
cache のメソッドの解説に戻り、FileSet()
が返す token.FileSet
型はこの手のソースコードを解析するコードを書く人にはおなじみの型で、複数ファイルのファイル上での位置情報を保持してくれる型である。
位置情報の参照がしたいときに使う、はず。
// なお私はそういうコードまだあまり書かないので別におなじみではない。
Cache
は FileSet()
メソッドで token.FileSet
を返すが、これは保持しているだけで、FileSet
に File
を追加するなどの操作をするのは、呼び出し側の仕事のようである。
GetFile()
Cache
の GetFile()
の実装を読んでみる。
func (c *Cache) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { fi, statErr := os.Stat(uri.Filename()) if statErr != nil { return &fileHandle{err: statErr}, nil } fh, ok := c.fileContent[uri] if ok && fh.modTime.Equal(fi.ModTime()) { return fh, nil } fh = readFile(ctx, uri, fi.ModTime()) c.fileContent[uri] = fh return fh, nil } func readFile(ctx context.Context, uri span.URI, modTime time.Time) *fileHandle { data, err := ioutil.ReadFile(uri.Filename()) if err != nil { return &fileHandle{ modTime: modTime, err: err, } } return &fileHandle{ modTime: modTime, uri: uri, bytes: data, hash: hashContents(data), } }
これは並行処理のための Lock などを省略したコードだが、やっていることは同じ、はず。
- 指定されたファイルが存在しなければ即時エラーを格納して返却
- cache に存在すれば、ディスク上のファイルと最終更新日時を比較して一致していればそれを返す
- 存在しなければ、ディスクからファイルを読み込み、メモリに cache する
ここまでの内容から Cache
は GetFile()
されたファイルの内容と token.FileSet
を保持する素朴な意味での cache であることが分かる。
Session
Session はエディタへのコネクション情報を保持する。
例えば、編集したファイルを(overlays という名前で) 保持している。
らしいがこれまた読んでもよく分からんので定義をあさる。
これは internal/lsp/source
パッケージに interface が定義され、実装は internal/lsp/cache
パッケージに存在する。
型定義
まずは internal/lsp/source
パッケージにある 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) }
Session
は Cache
の NewSession()
から生成され、自身を生成した Cache
を保持することが期待されており、その Cache
を生で操作するための Cache()
というメソッドもあるが、これは debug 用とのことなので省略した。
NewView()
や DidModifyFiles()
で取得される Snapshot
型は View
を生成した瞬間、やファイルを変更した瞬間のスナップショットなのだが、これは恐らく利便性のためにここで返しているだけで、本来は View
との関連が強い型なので詳細は View
の解説の中で紹介する。
この実装である 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 }
Server
構造体が保持するのはこの Session
型(正確には source.Session
interface)であり、Cache
は間接的に操作される。
Overlay
型
Overlays()
で返される Overlay
型は以下で定義される interface 型である。
type Overlay interface { VersionedFileHandle Saved() bool }
VersionedFileHandle
は前述の FileHandle
にバージョン情報を付加したものである。
ここで、internal/lsp/cache
パッケージ側の実装を読むと、Overlay
が何であるかが少し分かる。
source.Session
interface の実装である。cache.Session
型は overlays
という map[span.URI]*overlay
型のフィールドを持つ。
この overlay
型が source.Overlay
interface を満たす、internal/lsp/cache
パッケージ内の実装である。
このフィールドに overlay
が追加するためのメソッドは updateOverlays()
であり、これは DidModifyFiles()
からのみ呼ばれる。
updateOverlays()
ではファイルへの変更をスライスで受け取り、最新のファイルの中身を overlays に更新している。
さらに、処理をよく読むと、変更種別が削除/保存でないときは s.cache.getFile()
が呼ばれている。
つまり、
- overlays には LSP クライアントから送信された TextDocument の中身
Cache
には指定された URI のディスク上の中身
が保持されている。
もうすこし言葉を加えると、overlays は LSP でやりとりされる TextDocument の cache であり、Cache
はそれに限らないディスク上のファイルの cache である。
なお、ここでもまだ Go ファイルとしてのパースは行われていない。
疲れたので今日はここまで。
次回は View
を読む。
-- 追記(2020/09/06) -- かいた