概要
個人的に最近アツい https://github.com/Shopify/ruby-lsp の内部実装を読み解いて追加機能の提案・実装が出来るレベルを目指す。
なおこの回以外のブログは https://d.hatena.ne.jp/keyword/reading-shopify-ruby-lsp#related-blog で見ることができる。
前提
筆者に LSP (Language Server Protocol) の知識はほとんどない。クライアントとして利用しているのでどういったものかは知っているが、その内部実装には明るくない。なのでこの記事を読んでいる人で内容の誤りに気がついたらコメントして頂けるとありがたい。
また Ruby および TypeScript はそれなりに歴があるのでコードを読むことに不自由はない。
やること
https://github.com/Shopify/ruby-lsp のコミットを古い方から読んでいく。こういったコードリーディングはやったことないが、単に最新のソースコードを読むよりも Shopify の優秀なエンジニアの手癖を見ることが出来て学びが多そうなので実践してみる。
なお、あまり上手くいかなかったら違うアプローチに変えるつもり。
本編
前回に引き続き、今回は https://github.com/mtsmfm/language_server-protocol-ruby のコードリーディングを行う。
launguage_server-protocol-ruby は Ruby 向けの LSP SDK である。本筋はあくまで Shopify/ruby-lsp なので今回はサクッとコードを読んで実装・役割を把握しておく。
全体像
このライブラリで重要なモジュールは以下の3つ。
LanguageServer::Protocol::Transport
モジュールLanguageServer::Protocol::Interface
モジュールLanguageServer::Protocol::Constant
モジュール
LanguageServer::Protocol::Transport
モジュール
LanguageServer::Protocol::Transport::Io::Reader
クラスおよび LanguageServer::Protocol::Transport::Io::Writer
クラスの実装を持っているモジュール。
これらのクラスは非常にシンプルで Reader クラスは JSON-RPC リクエストの読み込みを、Writer クラスは JSON-RPC レスポンスの書き込みを、それぞれ任意のIOで行う責務をもつ。このことは以下のサンプルコードを見るとわかりやすい。
require "language_server-protocol" LSP = LanguageServer::Protocol writer = LSP::Transport::Stdio::Writer.new reader = LSP::Transport::Stdio::Reader.new subscribers = { initialize: -> { LSP::Interface::InitializeResult.new( capabilities: LSP::Interface::ServerCapabilities.new( text_document_sync: LSP::Interface::TextDocumentSyncOptions.new( change: LSP::Constant::TextDocumentSyncKind::FULL ), completion_provider: LSP::Interface::CompletionOptions.new( resolve_provider: true, trigger_characters: %w(.) ), definition_provider: true ) ) } } reader.read do |request| result = subscribers[request[:method].to_sym].call writer.write(id: request[:id], result: result) exit end
この例では標準入出力を指定IOとしているため以下のようなコマンドでプログラムを実行することができる。
$ echo "Content-Length: 55\r\n\r\n{\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"params\":{}}" | ruby ./test/example.rb Content-Length: 185 {"id":null,"result":{"capabilities":{"textDocumentSync":{"change":1},"completionProvider":{"triggerCharacters":["."],"resolveProvider":true},"definitionProvider":true}},"jsonrpc":"2.0"}%
LanguageServer::Protocol::Interface
モジュール
このモジュールには LSPの仕様において定義されている LSP メソッドのインターフェースやデータ構造(リクエストやレスポンスのフォーマットなど)が含まれている。
例えば LSP::Interface::ServerCapabilities
というクラスは microsoft/language-server-protocol の _specifications/lsp/3.18/general/initialize.md の定義が元になっている。
module LanguageServer module Protocol module Interface class ServerCapabilities def initialize # 引数は省略 @attributes = {} @attributes[:positionEncoding] = position_encoding if position_encoding # 省略 @attributes.freeze end # # The position encoding the server picked from the encodings offered # by the client via the client capability `general.positionEncodings`. # # If the client didn't provide any position encodings the only valid # value that a server can return is 'utf-16'. # # If omitted it defaults to 'utf-16'. # # @return [string] def position_encoding attributes.fetch(:positionEncoding) end # 省略 end end end
interface ServerCapabilities { /** * The position encoding the server picked from the encodings offered * by the client via the client capability `general.positionEncodings`. * * If the client didn't provide any position encodings the only valid * value that a server can return is 'utf-16'. * * If omitted it defaults to 'utf-16'. * * @since 3.17.0 */ positionEncoding?: PositionEncodingKind; /** * Defines how text documents are synced. Is either a detailed structure * defining each notification or for backwards compatibility the * TextDocumentSyncKind number. If omitted it defaults to * `TextDocumentSyncKind.None`. */ textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind; // 省略 }
なおこれらの Ruby プログラムは人力で書かれているわけではなく https://github.com/microsoft/language-server-protocol から自動生成されている。自動生成の仕組みについては後述する。
LanguageServer::Protocol::Constant
モジュール
このモジュールには LSPの仕様において定義されている定数(エラーコード、イベントの種類、操作の種類など)が含まれている。
例えば LSP::Constant::TextDocumentSyncKind::FULL
という定数は microsoft/language-server-protocol の _specifications/lsp/3.18/specification.md の定義が元になっている。
module LanguageServer module Protocol module Constant # # Defines how the host (editor) should sync document changes to the language # server. # module TextDocumentSyncKind # # Documents should not be synced at all. # NONE = 0 # # Documents are synced by always sending the full content # of the document. # FULL = 1 # # Documents are synced by sending the full content on open. # After that only incremental updates to the document are # sent. # INCREMENTAL = 2 end end end end
/** * Defines how the host (editor) should sync document changes to the language * server. */ export namespace TextDocumentSyncKind { /** * Documents should not be synced at all. */ export const None = 0; /** * Documents are synced by always sending the full content * of the document. */ export const Full = 1; /** * Documents are synced by sending the full content on open. * After that only incremental updates to the document are * sent. */ export const Incremental = 2; } export type TextDocumentSyncKind = 0 | 1 | 2;
こちらも https://github.com/microsoft/language-server-protocol から自動生成されている(後述)。
自動生成
前述の LanguageServer::Protocol::Interface
モジュールと LanguageServer::Protocol::Constant
モジュールは LSP の公式仕様である https://github.com/microsoft/language-server-protocol から自動生成されており、そのロジックは https://github.com/mtsmfm/language_server-protocol-ruby/blob/v3.17.0.3/scripts/generateFiles.ts で見ることができる。
見るとわかるが、なぜか LSP の仕様は markdown と markdown 内のコードブロック (TS) で管理されており、このスクリプトはそれらの markdown を気合いでパースして Ruby プログラムを出力する内容になっている。
一方で LSP 3.17 からは markdown ではなく json で仕様を管理する動きも見られる。mtsmfm/language_server-protocol-ruby もその流れに乗っかって脱 markdown パースを目指しているが 2023/12 時点では json 側の仕様が一部欠けているため実用には至っていない様子。 参考: https://github.com/mtsmfm/language_server-protocol-ruby/pull/49#issuecomment-1247458822