LSP 実装メモ (Text Document Synchronization `textDocument/didClose` 編)
前回
引き続き、Text Document Synchronization 周りの仕様について書いていく。
textDocument/didOpen
通知 <- donetextDocument/didChange
通知 <- donetextDocument/willSave
通知 <- donetextDocument/willSaveWaitUntil
リクエスト <- donetextDocument/didSave
通知 <- donetextDocument/didClose
通知 <- 今日ここ
Text Document Synchronization 編、感動の最終回。
textDocument/didClose
通知
サーバに TextDocument を閉じたことを伝えるための通知。
通知パラメータもシンプルに TextDocumentIdentifier
が送信されてくるのみである。
interface DidCloseTextDocumentParams { textDocument: TextDocumentIdentifier; }
textDocument/didOpen
通知で触れていたように、制約として、必ず textDocument/didClose
通知の前に textDocument/didOpen
通知が送信されてなければならない。
なお textDocument/didOpen
回でよく分かっていなかった以下の文章だが、今回の textDocument/didClose
通知の仕様にも記述されている。
Note that a server’s ability to fulfill requests is independent of whether a text document is open or closed.
requests
と書かれていることに今さら気付き、この文章は恐らく、他のリクエスト(例えば補完リクエストなど)には、事前に textDocument/didOpen
していなければならない、とか textDocument/didClose
された TextDocument に対してリクエストしてはならない、といった制約は無いぞ、と言っているだけのようである。
// とはいえ、意味のある情報を返せるかどうかは別だとは思う。
サーバ実装例
を読んでいく。
efm-langserver/sqls
ハンドラはここ。
中で closeFile()
を呼んでおり、
中身は Go 組み込みの delete()
関数である。
これは削除対象のキーが map
に存在しない場合でもエラーや panic したりはしない。
つまり事前に map
に URI が登録されていなくても(つまり事前に textDocument/didOpen
通知が送信されていなくても)単純に無視する。
LSP の仕様は、クライアントが必ず textDocument/didOpen
と textDocument/didClose
を対にして送信しろ、と言っているだけで、クライアントがそれに反した通知をサーバに送信してきたときには何も規定されていないためこれで十分なはずである。
sqls もほぼ同一の実装である。
gopls
gopls では TextDocument の中身を nil
として変更する、という方針のようである。
...と、(分かってはいたし、何度も言及したが) didModifyFiles()
の実装をちゃんと読まん限りあんまり gopls を読んでも面白くないので、次回は didModifyFiles()
から gopls のキャッシュ構造を追う回にします。
Capability
ここまで部分部分でも触れていたが、Text Document Synchronization 周りをひとしきり終えたので、ここでクライアントとサーバが事前に互いの機能を通知し合う initialize
リクエストで送信されるパラメータを一覧しておく。
クライアント側
クライアントが対応している機能を通知するための型は以下である。
export interface TextDocumentSyncClientCapabilities { dynamicRegistration?: boolean; willSave?: boolean; willSaveWaitUntil?: boolean; didSave?: boolean; }
dynamicRegistration
については今回割愛するが、他の項目は各通知を送信するかどうかについての boolean
である。
textDocument/didOpen
textDocument/didClose
textDocument/didChange
(TextDocumentSyncKind == Full
TextDocumentSyncKind == Incremental
双方含む) についてはクライアントは必ず実装しなければならないため、ここには含まれない。
textDocument/willSave
textDocument/willSaveWaitUntil
textDocument/didSave
についてはクライアントは実装してもしなくてもよいため、自身が実装しているかどうかをサーバに通知する。
vim-lsp では textDocument/willSave
textDocument/willSaveWaitUntil` は実装されていない。
textDocument/didSave
については デフォルトではサーバ側に通知していないが、サーバが willSave
を実装していないと主張しない(後で出てくる TextDocumentSyncOptions
の save
フィールド参照)限りは 送信するようになっている。
// 一応 PR チャンス?
サーバ側
サーバが対応している機能を通知するための型は以下。
export namespace TextDocumentSyncKind { export const None = 0; export const Full = 1; export const Incremental = 2; } export interface SaveOptions { includeText?: boolean; } export interface TextDocumentSyncOptions { openClose?: boolean; change?: number; willSave?: boolean; willSaveWaitUntil?: boolean; save?: boolean | SaveOptions; }
textDocument/didOpen
textDocument/didClose
textDocument/didChange
はサーバ側は実装しなくてもよい。
ただし、仕様上、全て実装するか、全て実装しないかのどちらかしか指定してはならないと決められている。
textDocument/didChange
は TextDocumentSyncKind
を指定する都合でフィールドが分かれているが、openClose == false && change == None
のような指定は禁止されている。
textDocument/willSave
textDocument/willSaveWaitUntil
textDocument/didSave
は実装が任意なのでそれぞれフィールドが用意されており、textDocument/didSave
はさらに通知パラメータの text
フィールドを参照するかどうかを指定する includeText
が指定できる。
ややこしいのが、実際リクエストパラメータに指定できるのは TextDocumentSyncOptions | number
であることだ。
number
とは TextDocumentSyncKind
を直接指定することを示しており、この指定方法の場合、openClose
は仕様から判断できる(None
なら openClose == false
それ以外なら openClose == true
)、が willSave
willSaveWaitUntil
save
についてはクライアントに判断が任される(仕様としてどう判断するか定められていない)。
実際 vim-lsp では、"save": { "includeText" : false }
と指定されたものとして処理される(willSave
willSaveWaitUntil
は vim-lsp が対応していない)。
通知/リクエスト(textDocument/ は省略) |
gopls | efm-langserver | sqls |
---|---|---|---|
didOpen didClose |
o | o | o |
didChange |
Incremental |
Full |
Full |
willSave |
x | -(クライアント実装に依存) | -(クライアント実装に依存) |
willSaveWaitUntil |
x | -(クライアント実装に依存) | -(クライアント実装に依存) |
didSave |
o(includeText == false ) |
-(クライアント実装に依存) | -(クライアント実装に依存) |
まとめ
これで Text Document Synchronization の仕様に関連する通知/リクエストの仕様は終わりである。
didOpen
->didChange
-> (willSave
) (willSaveWaitUntil
) -> (didSave
) ->didClose
という流れで TextDocument のライフサイクルが管理されている- サーバは TextDocument の URI を直接開くことは禁止されているので、効率的に TextDocument の更新を受けたければ
TextDocumentSyncKind.Incremental
を実装する
ここまででは TextDocument の中身をどう利用するのか(いつパースする? どう TextDocument の中身を持ち続ける? どういうクエリで TextDocument から情報を取り出す?) については触れてこなかったので、これからは一旦そのあたりを中心にして読んでいきたい。
次回、手始めに gopls のキャッシュ周りを調査する。
-- 追記(2020/08/30) -- かいた