リクエストの振り分け
RubyLsp::Server#process_message でリクエストの振り分けが書かれている。
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/server.rb#L13
今回見るのは textDocument/hover なので以下箇所から。
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/server.rb#L52-L53
引数で渡ってくる message のデータ構造が気になった。Sorbet の型だと Hash 以上の情報がない。
sig { override.params(message: T::Hash[Symbol, T.untyped]).void }
気になるので print debug しておいた。 textDocument/hover で実際に飛んでくるものはこんな感じ。
{jsonrpc: "2.0",
id: 13,
method: "textDocument/hover",
params:
{textDocument:
{uri:
#<URI::File file:///path/to/file.rb>},
position: {line: 5, character: 18}}}
text_document_hover メソッド
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/server.rb#L708
レスポンスの生成は Requests::Hover を new して、そちらに処理を委譲している。
@store.get() で取得されるオブジェクトの型が気になるところだけど、Requests::Hover の方で型定義がありそうなのでそちらを確認する。
document = @store.get(params.dig(:textDocument, :uri))
Requests::Hover.new
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/requests/hover.rb#L25-L34
document は RubyDocument もしくは ERBDocument だった。
document: T.any(RubyDocument, ERBDocument),
delegate_request_if_needed! は ERB ファイルにおいて埋め込み Ruby コードではない場合は Host Language (= HTML) 側の Language Server にリクエストを委譲するかどうかの判定。
delegate_request_if_needed!(global_state, document, char_position)
おそらくこのメソッドの主たる処理は Listeners::Hover の new メソッドを呼び出すこと。
node_context = RubyDocument.locate( document.parse_result.value, char_position, node_types: Listeners::Hover::ALLOWED_TARGETS, code_units_cache: document.code_units_cache, )
uri = document.uri @response_builder = T.let(ResponseBuilders::Hover.new, ResponseBuilders::Hover) Listeners::Hover.new(@response_builder, global_state, uri, node_context, dispatcher, sorbet_level)
node_context が何かはそのクラスのコメントに書いてあった。通常 AST の node 単体からだとその親 node へのアクセスが困難(できない認識)だけど、そういったアクセスを可能にするためのものと解釈した。
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/node_context.rb#L5C1-L7
# This class allows listeners to access contextual information about a node in the AST, such as its parent, # its namespace nesting, and the surrounding CallNode (e.g. a method call).
Listeners::Hover.new
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/listeners/hover.rb#L52-L62
dispather.register しているので特定の node 時にそれぞれのメソッドが呼ばれる。
dispatcher.register( self, :on_constant_read_node_enter, :on_constant_write_node_enter, :on_constant_path_node_enter,
クラス参照(定数)に hover した時
イメージはこう。

dispatcher を動く箇所は以下なので、ここの @target を print debug する。
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/requests/hover.rb#L77
出力された結果は以下。
@ ConstantReadNode (location: (6,16)-(6,20)
なので on_constant_read_node_enter メソッドを見る。
on_constant_read_node_enter メソッド
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/listeners/hover.rb#L114
やっていることは単純だった。
name = RubyIndexer::Index.constant_name(node) return if name.nil? generate_hover(name, node.location)
実際の name は hover したクラス名そのものが入っている(のでキャプチャの例だと "Base" となる)
generate_hover メソッド
https://github.com/Shopify/ruby-lsp/blob/v0.23.11/lib/ruby_lsp/listeners/hover.rb#L378
entries を取り出して、 @response_builder に値を詰め込んでいる。
entries = @index.resolve(name, @node_context.nesting)
categorized_markdown_from_index_entries(name, entries).each do |category, content| @response_builder.push(content, category: category) end
この entries がどういったデータなのかを確認した。
[
#<RubyIndexer::Entry::Class:0x0000000157a18350
@comments=nil,
@location=
#<RubyIndexer::Location:0x00000001499f52f0
@end_column=5,
@end_line=109,
@start_column=2,
@start_line=5>,
@name="Logicuit::Base",
@name_location=
#<RubyIndexer::Location:0x00000001499f52a0
@end_column=12,
@end_line=5,
@start_column=8,
@start_line=5>,
@nesting=["Logicuit", "Base"],
@parent_class="::Object"
@uri=
#<URI::Generic file:///path/to/file.rb>,
@visibility=#<RubyIndexer::Entry::Visibility::PUBLIC>>]
RubyIndexer クラスによってコードベースを解析した情報がインデックスとして保持されていると解釈。つまりは LSP Client からのリクエストに対してこのインデックスを使って適切な情報をレスポンスしてあげると言うのが LSP Server の役目と表現できそう。
ちなみに categorized_markdown_from_index_entries の each ブロックでの引数 content を出力すると以下のような結果が得られた。
"```ruby\n" + "Base\n" + "```"
"**Definitions**: [base.rb](file:///path/to/file.rb#L5,3-109,6)"
"\n" + "\n" + "base class for all gates and circuits"
hover で表示されていた情報と完全に一致。
次回
RubyIndexer クラスがどうやってインデックスを作成しているのかを確認する。