2022-07-24

今回はvscode-rubyの読書メモを書き連ねていきます。

vscode-rubyは、複数のパッケージを扱うmonorepoとして管理されている。

  • vscode-ruby
  • vscode-ruby-client
  • vscode-ruby-debugger
  • language-server-ruby

monorepoの管理にはLernaを使っているとのこと。

language-server-rubyについて詳しく見ていく。

package.jsonを見ると、次のパッケージがdependenciesに含まれていた。

  • web-tree-sitter
  • web-tree-sitter-ruby

web- prefix付きなのが気になるが、確かにtree-sitterが使われている。

package.jsonのmainフィールドにdist/index.jsとあり、npm run buildではesbuild.jsが利用されている。

esbuild.jsの中では、web-tree-sitter-ruby/tree-sitter-ruby.wasmが参照されている。 なるほど、WASM形式でビルドされてNode.js等で使えるように配布されているのがweb-tree-sitterなのかな。 ちなみにtree-sitterはRustで実装されている。

language-server-rubyのesbuild.jsを見ると、次の二つのことをやっているのが分かる。

  • tree-sitter{,-ruby}.wasmをコピーして、distディレクトリに入れる
  • src/inedx.tsをビルドして、distディレクトリに入れる

language-server-rubyのエントリポイントがsrc/index.jsであることが分かったので、ここを読み進めていく。

手元に環境にnodenvが入っているので、vscode-rubyのリポジトリのルートディレクトリで次のコマンドを実行するだけで環境構築を完了させられた。

nodenv install
npm install

これをやらないと、VSCodeが怒ってくる。

image

esbuildを使うのは初めてだけど、こういう感じで使うんだなあ。

esbuild というexecutableを実行するわけではなく、

node esbuild.js

と実行するというのが面白い。普通だったらオプションとかを渡したくなってCLIオプションを付けるためにCLIを用意するものだけど、esbuildの場合はどうせ設定ファイルを用意することになるので、そういうものを用意する必要は無いわという判断なのだろうか。まあ無くて良いなら無いに越したことはない。

// Don't die when attempting to pipe stdin to a bad spawn
// https://github.com/electron/electron/issues/13254
process.on('SIGPIPE', () => {
	log.error('SIGPIPE received');
});

とあり、そういえばVS CodeはElectronベースだから、Electronの不具合の影響を受けるんだったと思い出した。

基本のlanguage serverの実装はこういう感じのようだ。

import { createConnection, ProposedFeatures } from "vscode-languageserver";
cosnt connection = createConnection(ProposedFeatures.all);
// Customize connection as you like...
connection.listen();
let server;
connection.onInitialize(async (params) => {
  server = new Server(connection, params);
  server.initialize();
  return server.capabilities;
});

こんな感じのコードが書かれているから、実際には Server という内部実装の中でいろいろな処理をやっている。

なので、読むべきは次の実装:

  • Serverのconstructor
  • Server#initialize

capabilitiesというのは、「このlanguage serevrはDocumentHighlightに対応ています」のような情報を明示するためのオブジェクトらしい。

language-server-rubyでは、VS Codeの拡張として提供する各機能の単位を "Provider" と呼称しているようだ。VS Codeの用語なのか独自用語なのかはわからない。

language-server-rubyでは、次の6つのProviderを用意しているようだ。

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

前者4つは初期化フェーズ中に用意され、後者2つは初期化フェーズ完了後に用意される。connection.onInitializeとconnection.onInitializedでそれぞれ用意されている。

  • Server#initialize
  • Server#setup

でそれぞれ実装されているが、このネーミングは少し分かりづらい…

  • Server#onConnectionInitialize
  • Server#onConnectionInitialized

とかで良かっただろうと思う。

その話はさておき、Server.tsの主な仕事はこのようにProviderを用意してあげることらしく、主な実装はつまりそれぞれのProviderに書かれているに違いない。

Providerは、初期化時にconnectionを受け取って、何か良い感じに動くもののようだということが分かっている。

各種Providerは基底Providerクラスを継承しているらしい。 各種Providerは .registerというstatic methodを持っていて、外部向けのインターフェースがこれ

Connection#onDocumentHighlight という、コールバック登録用のメソッドが生えているらしいDocumentHighlightProviderはこれをそのconstructorで呼び出している。ググってもonDocumentHighlightに関する情報は乏しい……

onDocumentHighlightに渡す引数はServerRequestHandler型で、実際のコード例を見ると、TextDocumentPositionParams型の引数を取るらしい。位置情報が与えられるので、それに応じて何か適切に動作しろということだろうか。Promise<DocumentHighlight[]> 型の値を返す関数として実装されている。

paramsはpositionとuriを持つObjectらしい。多分ファイルパスとその中での位置情報が入っている。

DocumentHighlightAnalyzer.analyze にこれらの値を渡してその返り値をそのまま返している。役割分担を整理するとこうだ:

  • Provider
    • Connection#onDocumentHighlight のことを知っている
    • 適切なハンドラー関数を用意してあげる責務を持つ
  • Analyzer
    • Connectionについては詳しいことは知らない
    • コードの位置情報とファイルパスをもらって解析を行うだけ

DocumentHighlightAnalyzerでは、tree-sitterを利用した構文の解析と、DocumentHighlight[] の生成をやっている。どういう機能を持っているかというと、多分こんな感じ:

  • endにカーソルを載せているときは、対応するbeginやdoからendまでをハイライトする
  • beginやdoにカーソルを載せているときは、そこから対応するendまでをハイライトする

tree-sitter-rubyの生成する構文木の実例を見ながら進めていった方が分かりやすいかもしれない。

vscodeの "document highlight" という機能・概念を正確にするため、ぐぐってみた方が良さげ。

A document highlight is a range inside a text document which deserves special attention. Usually a document highlight is visualized by changing the background color of its range.

image

いろいろ調べていると、次のような言及があった。

textDocument/documentHighlightは、同じシンボルの使用箇所をエディタ上でハイライトする場合にも使用できます。

image

image

この機能だな。endにカーソルが載っている状態だと、そのendと対応するdoの背景色が変わっている。

if elsif else end や case when end, begin rescue else ensure end のときは全部光ってほしいな。少し改良してみる。