LSP 実装メモ (Text Document Synchronization `textDocument/didClose` 編)

前回

tennashi.hatenablog.com

引き続き、Text Document Synchronization 周りの仕様について書いていく。

  • textDocument/didOpen 通知 <- done
  • textDocument/didChange 通知 <- done
  • textDocument/willSave 通知 <- done
  • textDocument/willSaveWaitUntil リクエスト <- done
  • textDocument/didSave 通知 <- done
  • textDocument/didClose 通知 <- 今日ここ

Text Document Synchronization 編、感動の最終回。

textDocument/didClose 通知

サーバに TextDocument を閉じたことを伝えるための通知。

通知パラメータもシンプルに TextDocumentIdentifier が送信されてくるのみである。

interface DidCloseTextDocumentParams {
    textDocument: TextDocumentIdentifier;
}

textDocument/didOpen 通知で触れていたように、制約として、必ず textDocument/didClose 通知の前に textDocument/didOpen 通知が送信されてなければならない。

tennashi.hatenablog.com

なお textDocument/didOpen 回でよく分かっていなかった以下の文章だが、今回の textDocument/didClose 通知の仕様にも記述されている。

microsoft.github.io

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 したりはしない。
つまり事前に mapURI が登録されていなくても(つまり事前に textDocument/didOpen 通知が送信されていなくても)単純に無視する。

LSP の仕様は、クライアントが必ず textDocument/didOpentextDocument/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 を実装していないと主張しない(後で出てくる TextDocumentSyncOptionssave フィールド参照)限りは 送信するようになっている
// 一応 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/didChangeTextDocumentSyncKind を指定する都合でフィールドが分かれているが、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 willSaveWaitUntilvim-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) -- かいた

tennashi.hatenablog.com