概要
個人的に最近アツい https://github.com/Shopify/ruby-lsp の内部実装を読み解いて追加機能の提案・実装が出来るレベルを目指す。
なおこの回以外のブログは https://d.hatena.ne.jp/keyword/reading-shopify-ruby-lsp#related-blog で見ることができる。
前提
筆者に LSP (Language Server Protocol) の知識はほとんどない。クライアントとして利用しているのでどういったものかは知っているが、その内部実装には明るくない。なのでこの記事を読んでいる人で内容の誤りに気がついたらコメントして頂けるとありがたい。
やること
https://github.com/Shopify/ruby-lsp のコミットを古い方から読んでいく。こういったコードリーディングはやったことないが、単に最新のソースコードを読むよりも Shopify の優秀なエンジニアの手癖を見ることが出来て学びが多そうなので実践してみる。
追記)分量が多くなってきたので第3回からはコミット単位ではなく、気になるPR単位で読む。
今回の内容
前回は https://github.com/mtsmfm/language_server-protocol-ruby のコードリーディングを実施していた。今回は Shopify/ruby-lsp に戻ってコードリーディングを進めていく。
ここまでで v0.0.1 は見終わったので次は v0.0.2。
github.com
本編
Ruby::Lsp::Handler
クラスの実装が追加されている。このクラスは https://github.com/mtsmfm/language_server-protocol-ruby のサンプルコードをリファクタリングしたものになっている。変わらず JSON RPC のIOには標準入出力が利用されていて一瞬「あれ?」となったがどうやらこれが一般的らしい。
通信は基本的に標準入出力を介して行われます。なので、両者のプロセスは完全に独立している訳ではなく、サーバーはクライアントの子プロセスとして起動されるのが普通です[1]。
実際のところ、LSPには通信方式についての規定はありません。標準入出力の代わりにTCPを使う例もあります。従ってサーバーが子プロセスにならないこともあり得ますが、実例に乏しいためこのように記述しています。
Shopify/ruby-lsp の内部実装を読み解く (1) v0.0.1 の変更箇所、LSP の基本動作、LSIF (Language Server Index Format) - igsr5 のブログ で「この時はまだ VSCode Extension (のちの Shopify/vscode-ruby-lsp)も同じリポジトリに同封されているのかー」と言っていたが #6 の PR でリポジトリを分けることが決まっていた。意外と早かった。
この PR では以下の 4 つの method の実装が行われている。
handler.config do
on("initialize") do
store.clear
respond_with_capabilities
end
on("textDocument/didChange") do |request|
uri = request.dig(:params, :textDocument, :uri)
text = request.dig(:params, :contentChanges, 0, :text)
store[uri] = text
nil
end
on("textDocument/didOpen") do |request|
uri = request.dig(:params, :textDocument, :uri)
text = request.dig(:params, :textDocument, :text)
store[uri] = text
nil
end
on("textDocument/didClose") do |request|
uri = request.dig(:params, :textDocument, :uri)
store.delete(uri)
nil
end
on("textDocument/foldingRange") do |request|
respond_with_folding_ranges(request.dig(:params, :textDocument, :uri))
end
on("shutdown") { shutdown }
end
textDocument/didOpen
, textDocument/didChange
, textDocument/didClose
では対象のファイルの uri - contents 対をハッシュとしてインメモリに保存・更新・削除する処理を行なっている。これは大多数の method のリクエストを行う際に uri のみの params で良くするのが狙い。
Add support for document opened, changed and closed events. We need those for every request. We basically keep a hash as storage where the key is the file URI and the value is the content of that file. Most other requests will only send us the URI and so we just access the hash to get the file contents
textDocument/foldingRange
に関してはここで Ruby::Lsp::Requests::FoldingRanges
クラスという Ruby プログラムのパース、FoldingRange[] の抽出などを行う実装が追加されている。なお textDocument/foldingRange
とはその名の通りエディタ内でコード折り畳みをしてくれるあれである。この method にリクエストすることで対象 textDocument 内のどこが折り畳み可能か?それぞれどう折り畳むか?をエディタに知らせることができる。
これはコードを読むのが早いので以下に一部実装を抜粋している。
def initialize(source)
@parser = SyntaxTree.new(source)
@queue = [@parser.parse]
@ranges = []
end
def run
until @queue.empty?
node = @queue.shift
case node
when SyntaxTree::Def
location = node.location
@ranges << LanguageServer::Protocol::Interface::FoldingRange.new(
start_line: location.start_line - 1,
end_line: location.end_line - 1,
kind: "region"
)
else
@queue.unshift(*node.child_nodes.compact)
end
end
@ranges
end
ここではまず syntaxTree (CRubyで利用されている Ripper を wrap したライブラリ) を用いて受け取った Ruby プログラムの文字列をパースしている。その後 SyntaxTree::Def
Node を走査して、見つかった各メソッドを折り畳み対象としている。
10番のPRで追加された Store クラス (uri - contents 対ハッシュをインメモリに保持しているクラス) が contents の代わりに syntax_tree でパース済みの AST を保持するようになった。これで大部分の method でいちいち syntax_tree でパースする必要がなくなった。
AST Node の走査に Visitor パターンが導入された。
このPRではまだ LSP の実装に使われていないが、 https://shopify.engineering/improving-the-developer-experience-with-ruby-lsp を読んでいるなかでも Visitor パターンの言及はあって、例えば以下のような書き方ができるようになっている。
class FoldingRange < SyntaxTree::Visitor
def initialize(ast)
@ast = ast
@ranges = []
end
def run
visit(@ast)
@ranges
end
def visit_class(node)
location = node.location
@ranges << {
startLine: location.start_line - 1,
endLine: location.end_line - 1,
kind: "region",
}
super
end
def visit_def(node)
location = node.location
@ranges << {
startLine: location.start_line - 1,
endLine: location.end_line - 1,
kind: "region",
}
super
end
end
FoldingRange が強化された。今まではシンプルに def のみ折り畳み可能だったが、ここでは class や if、elsif など計 17 ケースが新たにサポートされた。
ここからも少しづつサポート範囲が広がっていくが FoldingRange については個人的にあまり関心がないので積極的に拾わないことにする。
textDocument/documentSymbol
の実装が追加された。
textDocument/documentSymbol とは与えられた textDocument の中にある symbol の種類や階層関係を取得するためのメソッド。
例えば symbol の種類は 3.17 時点で以下の種類が存在する。
export namespace SymbolKind {
export const File = 1;
export const Module = 2;
export const Namespace = 3;
export const Package = 4;
export const Class = 5;
export const Method = 6;
export const Property = 7;
export const Field = 8;
export const Constructor = 9;
export const Enum = 10;
export const Interface = 11;
export const Function = 12;
export const Variable = 13;
export const Constant = 14;
export const String = 15;
export const Number = 16;
export const Boolean = 17;
export const Array = 18;
export const Object = 19;
export const Key = 20;
export const Null = 21;
export const EnumMember = 22;
export const Struct = 23;
export const Event = 24;
export const Operator = 25;
export const TypeParameter = 26;
}
export type SymbolKind = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26;
基本的に syntax_tree がパースした AST Node のクラスと Kind が一致するので前述の Visitor パターンがとても刺さっていた。
長くなってしまったので今回はここまで。次回は https://github.com/Shopify/ruby-lsp/pull/32 から見ていく。
参考資料