昨日に引き続き、vscode-rubyのlanguage-server-rubyを読んでいく。

昨日は6つのProviderから構成されているという話をし、DocumentHighlightProviderの実装を読んだ。

  • FoldingRangeProvider
  • DocumentHighlightProvider
  • DocumentSymbolProvider
  • DocumentFormattingProvider
  • ConfigurationProvider
  • WorkspaceProvider

DocumentSymbolProviderを見てみたが、DocumentHighlightProviderと比べて幾らか難しい実装だった。 tree-sitterのwrapper?であるForestのstreamっぽいAPIを使っていて複雑。

DocumentHighlightAnalyzerはリクエストのたびにインスタンスをつくって解析する仕組みだったが、DocumentSymbolAnalyzerとFoldingRangeAnalyzerは1個だけつくって使い回す実装らしく、Analyzer.tsがシングルトンをつくってそのプロパティにもたせている。

Analyzer.tsには深さ優先探索のtree-walkingを行う仕組みが実装されている。treeが送られ続けてくるforestStreamというやつをanalyzerのシングルトンが購読しており、これをwalkで歩き続けつつ、symbolとfoldingの解析を続けている。FoldingRangeリクエストを受け取った際には、このanalyzerのシングルトンの現在の状態を読んで返すだけ。

DocumentFormattingProviderを見てみよう。

次の二種類のリクエストに対応するProviderであるらしい。

  • textDocument.Formatting
  • textDocument.rangeFormatting

前者は与えられたドキュメント全体を、後者は与えられたドキュメントのうち指定された範囲のフォーマットを整えるためのものらしい。

The document range formatting request is sent from the client to the server to format a given range in a document

language-server-rubyの実装では、ここでRuboCopやらRufoやらを利用することになる。

rxjsのObservableが利用されている。外部のプロセスとのやりとりが発生するところをこれでやりとりしているのだろうか。

「ドキュメント」という単位が何を表しているのかという話があった。これは多分「ファイル」と読み替えてもさほど影響はないように思う。language-server-rubyでは、DocumentManager.tsがdocumentsというシングルトンを提供している。ドキュメントはIDを持っているらしく、このIDでdocumentsに問い合わせると対象のドキュメントが取得できるという仕組みらしい。

vscode-languageserverというパッケージがdocumentsを管理する仕組みを提供していて、documents.listen(connection) とやると良い感じにクライアントのドキュメントの情報を管理してくれるらしい。つまり、document formattingのリクエストでわざわざソースコードをやり取りしなくても、そこではドキュメントのIDだけ含めておけば良くなるということ。

Formatting Providerは、リクエストに応じて Promise<TextEdit[]> を返せば良いらしい。TextEditはvscode-languageserverが提供している。TextEditというのは、テキストを変形させる命令のことだと思う (e.g. replaceとか)。

result: TextEdit[] | null describing the modification to the document to be formatted.

BaseFormatterとRuboCopFormatterを見てみると、spawnを使って子プロセスをつくり、そこで bundle exec rubocop -s ... -a を実行していることがわかる。-sは標準入力経由で使うやつらしい。

    -s, --stdin FILE                 Pipe source from STDIN, using FILE in offense
                                     reports. This is useful for editor integration.
$ echo "1 + 2" | rubocop -s foo.rb -A
Inspecting 1 file
C

Offenses:

foo.rb:1:1: C: [Corrected] Style/FrozenStringLiteralComment: Missing frozen string literal comment.
1 + 2
^
foo.rb:2:1: C: [Corrected] Layout/EmptyLineAfterMagicComment: Add an empty line after magic comments.
1 + 2
^

1 file inspected, 2 offenses detected, 2 offenses corrected
====================
# frozen_string_literal: true

1 + 2

これで ====== の後ろを見れば変更後のコードが取得できる。この標準出力をもとに、``TextEdit[]` を用意する。これにはdiff-match-patchというNPMパッケージが利用されている。

選択範囲だけformatしてくれというリクエストだった場合でも、ファイル全体に対してRuboCopを利用するが、diffを見て選択範囲からの書き換えだけ適用することになっている。

LSPの説明でmethod, paramsと呼ばれていたのはこういうやつのことか。

LSP クライアントからサーバーへのメッセージとして、以下のような文字列が送られます。(改行は \r\n)

Content-Length: 88

{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "shutdown",
    "params": null
}

この文字列は、今回はサーバーのプロセスの標準入力に送られてきます。標準入力を受け取るには process.stdin の data イベントを監視して、送られてくるデータをバッファーにためていけばいいです。

実際にはどう送られてくるのかと思ったら、この場合は標準入力を利用しているんだな。素朴で嬉しい。

クライアント側の実装も学び始めた。これを読んでみている。

クライアント (vscodeの拡張) 起動時に、サーバ起動用のコマンド名も記述するもんなんだな。まあそれはそうか。 例えばsolargraph gemはlanguage serverを提供していたはずで、あれは別途自前で起動させておくものだった気がするから、多分そういう場合にはサーバーを起動しないオプションもあるのだと思う。

手元でvscode-rubyのビルドを試してみたが大変だった。これを見ながら進めた。

yarn install

これを実行する時点でエラー。./packages/vscode-ruby-client/package.jsonからdependenciesに書いているlanguage-server-rubyが存在しないとのこと。lernaの使い方が間違ってるとか? とりあえずこのdependenciesの行を手動で消すと進めるように。

yarn install中にtree-sitter-rubyの何かでエラー。npm install tre-sitter-rubyを試すも問題無し。再度yarn installを実行するとなぜか成功。なぜ?

yarn watchも失敗。

Error: error:0308010C:digital envelope routines::unsupported at new Hash (node:internal/crypto/hash:67:19)

webpackのhash関数がどうのこうのということで、ググってこの環境変数を追加して再試行すると上手くいった。

export NODE_OPTIONS=--openssl-legacy-provider