LSP 実装メモ (Text Document Synchronization `textDocument/didSave` 編)
前回
引き続き、Text Document Synchronization 周りの仕様について書いていく。
textDocument/didOpen
通知 <- donetextDocument/didChange
通知 <- donetextDocument/willSave
通知 <- donetextDocument/willSaveWaitUntil
リクエスト <- donetextDocument/didSave
通知 <- 今日ここtextDocument/didClose
通知
textDocument/didSave
通知
前回の willSave
willSaveWaitUntil
は保存前だったが、textDocument/didSave
通知は保存後にクライアントから送信される。
interface DidSaveTextDocumentParams { textDocument: TextDocumentIdentifier; text?: string; }
textDocument
はおなじみ TextDocumentIdentifier
つまり TextDocument の URI である。
text
は保存された TextDocument の中身であるが、これを送信するかどうかは事前にクライアントと合意が取られる。
具体的にはクライアントから送信される intialize
リクエストへのレスポンスの一部として以下が送信される。
export interface SaveOptions { includeText?: boolean; }
includeText
が true
な場合はサーバはクライアントが DidSaveTextDocumentParams
の text
フィールドを埋めることを期待することを示す。
クライアントは自身がこのフィールドを埋める機能を実装しているかどうかはサーバに通知できないので、サーバは includeText
を true
にしたところで、クライアントが対応していない場合のハンドリングはしておかなければならないと考えられる。
サーバ実装例
を読んでいく
efm-langserver
ハンドラの実装は以下
ちゃんと params.Text
の中身を取り扱っていることが分かる。
ただし、initialize
レスポンスに SaveOptions
を渡していないため、SaveOptions
を無視して保存後のテキストを送り付けてくるクライアントがあった場合にのみ発火する処理である。
// プロトコル上 includeText
が false
ならクライアントは text
を埋めてはならない、とは書いていないためそのようなクライアントを書いてもよい、はず。
あらためて handleTextDocumentDidSave()
を追っていく。
params.Text
が埋まっている場合は updateFile()
へ、そうでない場合は saveFile()
へ処理が移る。
updateFile()
の第三引数は TextDocument のバージョンを示すが、この textDocument/didSave
で送信された text
ではバージョンが変化しない(例え直前の textDocument/didChange
の適用後から変更されていたとしても)ので nil
を渡し、バージョンを変更しないことを伝えている。
saveFile()
は linter を発火するための h.request
チャネルに URI を伝えている。
efm-langserver は TextDocument の更新および保存の度に linter を動作させている、言い換えると textDocument/didChange
textDocument/didSave
通知が送信されるたびにクライアントに textDocument/publishDiagnostics
通知を投げていることが分かる。
sqls
ハンドラの実装は以下
efm-langserver と同様に params.Text
をハンドルしていることが分かる。
params.Text
の中身に応じて updateFile()
と saveFile()
に振り分けているところまでは同様だが、sqls は textDocument/publishDiagnostics
に対応していないため、saveFile()
では何もせず。updateFile()
も params.Text
の内容を反映するのみである。
gopls
今回 gopls はハンドラ部分を軽く追うだけにしておく。
// これまでも別に深く追っていたわけではないが...
gopls では didModifyFiles()
というメソッドが TextDocument 関連の通知の肝になる処理だが、これは gopls のキャッシュの持ち方と強く結び付いた処理なので、このあたり含め textDocument/didClose
まで紹介した後に集中して読み解いておきたい。
gopls は source.FileModification
という型のフィールドの内容で didModifyFiles()
の処理を制御しており、params.Text
の有無も同様に対応される。
ここまでを注意深く読んでいると、以下の部分がおかしいことに気付く。
LSP 3.15 で定められた仕様では TextDocument のバージョン情報は入っていないはずである。
実際の定義も VersionedTextDocumentIdentifier
型に変更されている。
gopls は LSP で定義された各種リクエスト/構造体を microsoft/vscode-languageserver-node から自動生成している(internal/lsp/protocol/typescript)。
で、実際 vscode-languageserver-node の定義を見れば
VersionedTextDocumentIdentifier
になっている。
どうやら LSP 次期バージョンの 3.16 でも TextDocumentIdentifier
のまま で、vscode-languageserver-node
のこの実装は 4年前からある ようなので、VSCode の独自実装かなと考えている。
クライアント実装
サーバ側を読んでいるとクライアント実装で気になるところが出てきたのでちょっとだけ追う。
SaveOptions
のincludeText
によってパラメータのtext
フィールドを操作しているかどうか- パラメータの TextDocument を VSCode に追従して
VersionedTextDocumentIdentifier
で扱っているかどうか
読むのは vim-lsp である。
includeText
のハンドリング
こちらはちゃんと includeText
に応じて text
を埋めるか決めている。
textDocument
を VersionedTextDocumentIdentifier
として扱っているか
uri
しか埋めていないことから通常の(仕様通りなのでもちろんこれが正しい) TextDocumentIdentifier
として扱っている。
というわけで次回は Text Document Synchronization 最終回 textDocument/didClose
編予定。
-- 追記(2020/08/23) -- かいた