RubyKaigiの復習 LSP
先日久しぶりに RubyKaigi に参加してきました。やっぱりオフラインはいいですね。noteはブースを出させてもらったので社外の方ともたくさん交流でき楽しかったです。
イベント参加レポートを書くのは苦手なので..理解できなかったものや、詳しく知らなかったものを復習して記事にしていこうと思います。
とりあえず第一弾は LSP について。
Matz のキーノート で Solargraph が、Shopify さんの syntax tree の発表で ruby-lsp が触れられていました。
(LSP については Microsoft のドキュメントをご参照ください)
私は Rubymine を使っているので あまり意識してこなかったのですが、今後ますます便利になりそうなので仕組みを理解しておきたいと思い Solargraph の中身を軽く見てました。
以下は Microsoft のドキュメントから拝借したLSP図です。
IDE と LSP サーバーが JSON-RPC で通信しています。
(VSCodeへのインストール方法は https://solargraph.org/guides/getting-started の通りです)
まずは Language サーバーの起動から見ていきます。
VSCode の拡張機能として入れた castwide/vscode-solargraph から microsoft/vscode-languageserver-node の LanguageClient クラスを経由して起動しているようです。
let client = new LanguageClient('Ruby Language Server', serverOptions, clientOptions);
上記の serverOptions を辿っていくと Solargraph を起動し、コネクションを作成していることがわかります。
} else if (transport == 'socket') {
return () => {
return new Promise((resolve, reject) => {
let socketProvider: solargraph.SocketProvider = new solargraph.SocketProvider(configuration);
socketProvider.start().then(() => {
let socket: net.Socket = net.createConnection(socketProvider.port);
resolve({
reader: socket,
writer: socket
});
}).catch((err) => {
reject(err);
});
});
};
さて VSCode 側からの起動、接続がわかったので Solargraph gem を見ていきます。
さきほど VSCode から叩いた起動コマンドはこちらです。
def socket
require 'backport'
port = options[:port]
port = available_port if port.zero?
Backport.run do
Signal.trap("INT") do
Backport.stop
end
Signal.trap("TERM") do
Backport.stop
end
Backport.prepare_tcp_server host: options[:host], port: port, adapter: Solargraph::LanguageServer::Transport::Adapter
STDERR.puts "Solargraph is listening PORT=#{port} PID=#{Process.pid}"
end
end
Solargraph は Backport という gem を使って TCPServer を立ち上げています。
Backport からは Adapter を介してデータを受け取ります。
Adapter には、起動時に呼ばれるメソッド(opening)、停止時に呼ばれるメソッド(closing)、データ受信時に呼ばれるメソッド(receiving) などを定義します。
ここではデータ受信時に呼ばれるメソッド(receiving)を見ていきます
def receiving data
@data_reader.receive data
end
receiving で受けとったデータは MessageWorker というワーカーで処理されます。
def tick
message = @mutex.synchronize do
@resource.wait(@mutex) if messages.empty?
messages.shift
end
handler = @host.receive(message)
handler && handler.send_response
end
上記の @host .receive の中でLSPのメッセージに対応したハンドラーが生成されメッセージを処理します。
以下がハンドラーの一部です。
register 'textDocument/didOpen', TextDocument::DidOpen
register 'textDocument/didChange', TextDocument::DidChange
register 'textDocument/didSave', TextDocument::DidSave
register 'textDocument/didClose', TextDocument::DidClose
register 'textDocument/hover', TextDocument::Hover
例えばホバーの場合は、 TextDocument::Hover の process メソッドで処理されます
def process
line = params['position']['line']
col = params['position']['character']
contents = []
suggestions = host.definitions_at(params['textDocument']['uri'], line, col)
last_link = nil
suggestions.each do |pin|
parts = []
this_link = host.options['enablePages'] ? pin.link_documentation : pin.text_documentation
if !this_link.nil? && this_link != last_link
parts.push this_link
end
parts.push "`#{pin.detail}`" unless pin.is_a?(Pin::Namespace) || pin.detail.nil?
parts.push pin.documentation unless pin.documentation.nil? || pin.documentation.empty?
unless parts.empty?
data = parts.join("\n\n")
next if contents.last && contents.last.end_with?(data)
contents.push data
end
last_link = this_link unless this_link.nil?
end
set_result(
contents_or_nil(contents)
)
何やらサジェストを用意してそうです。
最後に処理した結果を JSON-RPC で VSCode へ返します。
(ここではキューに積んで、最終的に Adapter の write メソッドでクライアントに送ります)
def send_response
return if id.nil?
if host.cancel?(id)
# https://microsoft.github.io/language-server-protocol/specifications/specification-current/#cancelRequest
# cancel should send response RequestCancelled
Solargraph::Logging.logger.info "Cancelled response to #{method}"
set_result nil
set_error ErrorCodes::REQUEST_CANCELLED, "cancelled by client"
else
Solargraph::Logging.logger.info "Sending response to #{method}"
end
response = {
jsonrpc: "2.0",
id: id,
}
response[:result] = result unless result.nil?
response[:error] = error unless error.nil?
response[:result] = nil if result.nil? and error.nil?
json = response.to_json
envelope = "Content-Length: #{json.bytesize}\r\n\r\n#{json}"
Solargraph.logger.debug envelope
host.queue envelope
host.clear id
end
以上で一通りメッセージ処理が確認できました。
オブザーバーパターンを使っているところで少し迷いましたが、そんなに複雑ではなく読みやすかったです。
VSCodeを使用しているチームメンバーに Solargraph について聞いたところ現状はそこまで活用できてないとのことでしたが、今後ますます便利になることを期待しています。
Solargraph のプラグインも書けるようなので何か独自ルールも定義できるといいかもしれないなと思いました。
(サンプルコードのままでは動かなかったのでその旨を伝えてみました https://github.com/castwide/solargraph/issues/596)
ということで第1弾の復習は以上です。
この記事が気に入ったらサポートをしてみませんか?