LSP 実装メモ (Text Document Synchronization `textDocument/didOpen` 編)
Language Server Protocol に従ったサーバを実装するためのメモ
まずは Text Document Synchronization の実装から調査する。
Text Document Synchronization とは
Text Document Synchronization は LSP サーバ/クライアント間で Text Document、つまり補完などの処理をする対象のテキストファイル内のデータを同期するための仕様群である。
LSP で定義されたこれに関連するメソッドは以下である。
// 今となってはどうでもいいが、ちょっと前まで仕様書には textDocument/open
textDocument/change
textDocument/close
という未定義なメソッドについての記述があったが、さっき読みなおしたところ textDocument/didOpen
textDocument/didChange
textDocument/didClose
に修正されていた。
textDocument/didOpen
通知textDocument/didChange
通知textDocument/willSave
通知textDocument/willSaveWaitUntil
リクエストtextDocument/didSave
通知textDocument/didClose
通知
これらがクライアントから送信されてくる。
では各々のリクエストパラメータなどの詳細を見ていく。
短かく分けることで記事を継続して書くモチベーションになるかもしれないので、今回は textDocument/didOpen
通知のみを見ていく。
textDocument/didOpen
通知
textDocument/didOpen
はドキュメントをクライアントが開いた際、そのことをサーバに教えるための通知である。
パラメータは以下である。
なお、仕様書が TypeScript で記述されているため、ここでもそのまま記述する。
interface DidOpenTextDocumentParams { textDocument: TextDocumentItem; } interface TextDocumentItem { uri: DocumentUri; languageId: string; version: number; text: string; }
uri
LSP ではドキュメントの位置は URI で指定される。
クライアント側の実装例として prabirshrestha/vim-lsp を見ると、以下のように基本的には files://
がスキーマとして指定されてくるようである。
URI 生成処理部分
languageId
複数の言語に対応したサーバなど(例えば Go 言語の Language Server 実装である gopls は Go で書かれたコードだけでなく、go.mod
の解釈もする)ではどの言語で書かれた TextDocument であるかは重要である。
このとき、URI をパースして拡張子を解釈したりしないですむように languageId
を指定できる。
推奨される Language ID 一覧は TextDocumentItem の定義 に記述されている。
vim-lsp では Vim の filetype
値が送信される。
version
version
はそのテキストのバージョンを示し、編集操作が行なわれる度増加(undo/redo などでも増加)する。
vim-lsp では 1
が送られる。
text
text
にはファイルの中身のテキストが格納される。
uri
が送信されるので、サーバ側で読めばいいのでは、と思われるかもしれないが、LSP ではテキストはクライアント側 (エディタ側) で管理されるものと定められており、プロトコルとしてサーバ側で開いてはならない (MUST NOT) と規定されている。
DidOpenTextDocument Notification
その他動作に関する仕様として、
textDocument/didOpen
通知は同一の TextDocument について連続で送信してはならない。- 必ず
textDocument/didClose
通知を送信してから送信しなければならない。 - 実際サーバ側が
didOpen
通知を連続して受け取った場合にどのように処理すべきかについては触れられていない。Note that a server’s ability to fulfill requests is independent of whether a text document is open or closed.
という記述から、クライアントが送信してはならないがサーバは処理してもいい、と解釈できるかもしれない- このあたりはサーバ実装を調査する
- 必ず
languageId
を変更する場合は TextDocument を開きなおさなければならない- つまり
didClose
通知で閉じてから再度didOpen
通知で開く - vim-lsp ではぱっと読んだところ
FileType
autocmd を処理してないので、途中でlanguageId
が変更されても何も起きない、はず - 例えば JavaScript から TypeScript に Language ID を変更するときに、TypeScript に対応したサーバを起動すべきかどうかについては言及がない
- 同様に JavaScript の TextDocument がなくなったときにサーバを終了すべきかどうかについても言及はない
- つまり
サーバ側実装例
サーバ側でどのように実装しているかをいくつか観察してみる。
私は Go が一番手に馴染んでいるので、以下の Go で書かれたサーバ実装を見る。
efm-langserver/sqls
この二つの実装はだいたい同じ実装である。
シンプルにサーバ/ハンドラの構造体に Map でファイルの中身をそのまま持つ。
openFile()
で map に登録して、updateFile()
でパラメータのテキスト内容に更新する、という流れである。
またすでに開いている TextDocument のチェックは記述されていないため、didOpen
が連続した場合は上書き登録される。
gopls
gopls は implementation.md にあるとおり、多重 cache 機構があるので、その兼ね合いで一見複雑そうに見えるが、基本的には同じでパラメータの URI にデータを登録して、テキスト内容に更新する。
まず Session.ViewOf()
で登録された cache を探して、存在しなければ cache を初期化する。
その後、中身をパラメータで指定された値に更新する。
こちらもぱっと読んだ限り、すでに存在しているかのチェックは無いのでそのまま cache の内容が新しいパラメータで更新されるだけと思われる。
cache 周りの詳細な実装は追えておらず、また後日。
次は textDocument/didChange
編予定。
-- 追記(2020/08/01) --
書いた
tennashi.hatenablog.com