Ruby LSP アドオンをつくってみた 〜 ruby-lsp-rbs_rails を例に添えて 〜


2025年 11月 04日

以前、 RubyKaigi 2025 follow up イベントで rbs_rails LSP サーバを紹介 しました。
その際に 友人 から「LSP サーバを新たに作るのではなく、Ruby LSP のアドオンとして実装すべきだ」という提案をもらいました。

この提案はとてもよい指摘です。RubyKaigi 2024 の The state of Ruby dev tooling でも、開発補助ツール(の組み合わせ)が増えすぎると、選択肢が膨大になり開発者の混乱のもととなる、と説明されています。

個人的にはツールがいくつもあり、多様性の中で切磋琢磨するという牧歌的な考え方も捨てがたいのですが、開発環境の (デファクト) スタンダードがあり、開発環境がかんたんに用意できることと便利である、というのはもっともな意見です。

情報収集と設定の微調整をし続けたものだけがよい開発環境を構築できる、というのは望ましくはないですしね。

ということで、先日 ruby-lsp-rbs_rails という Ruby LSP アドオンを公開しました 。これにより、この gem をインストールするだけで、rbs_rails による型定義ファイルの自動生成ができるようになりました。

機能そのものは以前の提案と同じものですので、詳細は 前回の記事 を御覧ください。

今回は rbs_rails アドオンを作るに当たって得た知見である Ruby LSP アドオンの作り方を紹介します。

Ruby LSP アドオンの作り方

gem を作る

Ruby LSP は インストールされている gem から ruby_lsp/**/addon.rb というファイルを検索 し、アドオンとして扱います。そのため、Ruby LSP アドオンを作る際は以下のことを守る必要があります。

  • rubygems としてパッケージを提供する
  • アドオンのエントリーとして ruby_lsp/**/addon.rb を用意する

この規約に従うため、gem の名前は ruby_lsp-[addon_name] (ruby_lsp の後はハイフン) とするのがよいでしょう。

$ bundle gem ruby_lsp-rbs_rails
Creating gem 'ruby_lsp-rbs_rails'...
MIT License enabled in config
Code of conduct enabled in config
RuboCop enabled in config

Initializing git repo in /Users/tkomiya/ruby_lsp-rbs_rails
      create  ruby_lsp-rbs_rails/Gemfile
      create  ruby_lsp-rbs_rails/lib/ruby_lsp/rbs_rails.rb
      create  ruby_lsp-rbs_rails/lib/ruby_lsp/rbs_rails/version.rb
      create  ruby_lsp-rbs_rails/sig/ruby_lsp/rbs_rails.rbs
      create  ruby_lsp-rbs_rails/ruby_lsp-rbs_rails.gemspec
      create  ruby_lsp-rbs_rails/Rakefile
      create  ruby_lsp-rbs_rails/README.md
      create  ruby_lsp-rbs_rails/bin/console
      create  ruby_lsp-rbs_rails/bin/setup
      create  ruby_lsp-rbs_rails/.gitignore
      create  ruby_lsp-rbs_rails/.github/workflows/main.yml
      create  ruby_lsp-rbs_rails/LICENSE.txt
      create  ruby_lsp-rbs_rails/CODE_OF_CONDUCT.md
      create  ruby_lsp-rbs_rails/.rubocop.yml

Gem 'ruby_lsp-rbs_rails' was successfully created. For more information on making a RubyGem visit https://bundler.io/guides/creating_gem.html

この gem の名称にすると、自動的に lib/ruby_lsp/[addon_name]/ というディレクトリが作成されるので、gem 作成後に addon.rb を配置するだけで済みます。

なお、残念ながら拙作の ruby-lsp-rbs_rails はこの命名規則に従っていません。
rubygems.org で公開されているアドオン群が ruby-lsp (ハイフン) という名前を使っている ので、それに倣ってこのパッケージ名を選んだのですが、アドオンの規約どおりのファイルが生成されずに手作業でディレクトリ構成を調整する羽目になりました。 lib/ruby/lsp/rbs_rails.rb というファイルができてしまうんですよね…。

この記事を書くに当たって振り返ったところ、パッケージ名を ruby-lsp (ハイフン) よりも ruby_lsp (アンダースコア) にするほうがコード生成や、パッケージ名的に自然だということに気づきました。
ですので、ruby_lsp-… という名前を使うのが良いかもしれません。

アドオンを実装する

さて、プロジェクトが準備できたところで、アドオンの実装を進めていきましょう。

まずは Ruby LSP がアドオンを検知できるよう、 lib/ruby_lsp/**/addon.rb というファイルを作成します。
今回は rbs_rails 用のアドオンを作るのが目的ですから、 lib/ruby_lsp/rbs_rails/addon.rb というファイルを作成します。

このファイルにはアドオンのクラスを定義します。このクラスは RubyLsp::Addon クラスを継承するようにしてください。なお、クラス名やモジュール階層には特に指定はないようです。

ここではファイル名に合わせて RubyLsp::RbsRails::Addon というクラスを作ります。

# frozen_string_literal: true

require "ruby_lsp/addon"

module RubyLsp
  module RbsRails
    class Addon < ::RubyLsp::Addon
      def activate(global_state, message_queue)
      end

      def deactivate
      end

      def name
        "Ruby LSP RBS Rails"
      end

      def version
        "0.1.0"
      end
    end
  end
end

アドオンクラスには #activate, #deactivate, #name, #version という 4つのメソッドを実装します。それぞれ次のような役割を持ちます。

  • #activate: LSP サーバが起動したときに一度だけ呼び出されるメソッド。アドオンの初期化処理を実装する。
  • #deactivate: LSP サーバが終了するときに呼び出されるメソッド。リソースの開放などを行う。
  • #name: アドオンの名前を返すメソッド。
  • #version: アドオンのバージョンを返すメソッド。

ここまでで、何もしない Ruby LSP アドオンが完成しました。
ここからは、エディタの操作に応じてアドオンが動作するように仕立てていきます。

初期化処理/終了処理を書く

先程用意した #activate メソッドと #deactivate メソッドに、アドオンの初期化処理と終了処理を実装します。

まずは #activate メソッドから見ていきます。このメソッドは global_statemessage_queue という 2つの引数を受け取ります。

  • global_state: Ruby LSP のグローバルな状態を表すオブジェクト
  • message_queue: LSP サーバと LSP クライアント間でメッセージを送受信するためのキューオブジェクト

最初の引数である global_state は Ruby LSP の内部状態にアクセスできるオブジェクトです。プロジェクトの状態や設定値などが読み込めるようです。rbs_rails アドオンではプロジェクトのルートディレクトリを取得するために global_state.workspace_path を参照しています。

もうひとつの引数である message_queue は LSP の通信に必要なオブジェクトです。LSP のメッセージのやり取りに必要となるため、インスタンス変数に保存しておくのが良いでしょう。

rbs_rails では、変換の進捗状況をログ出力するために message_queue を利用しています。

def activate(global_state, message_queue)
  @message_queue = message_queue

  # @message_queue << RubyLsp::Notification.window_log_message("hello world") という呼び出しでログが記録できる
end

rbs_rails アドオンでは、他にも初期化処理として 以下のような処理を実装しています

  • Rails アプリケーションのロード
  • zeitwerk オートローダーの設定
  • rbs_rails 設定ファイルの読み込み

もうひとつの #deactivate メソッドは、サブプロセスの終了など、アドオンがアンロードされるときに呼び出されます。そのため、リソースの解放といったクリーンアップ処理がある場合に #deactivate メソッドで処理をします。

rbs_rails アドオンでは特にクリーンアップ処理は必要なかったので、空のままにしています。

ファイルの変更を検知する

rbs_rails アドオンでは、ファイルの変更を検知して型定義ファイルの生成処理を実行しています。

Ruby LSP では、アドオンクラスに #workspace_did_change_watched_files というメソッドを定義することで、ファイルの変更を検知できます。

この名前でピンときた方もいるかも知れません。このメソッドは LSP で定義されている workspace/didChangeWatchedFiles イベント そのものです。

#workspace_did_change_watched_files メソッドは、 changes という引数をひとつ受け取ります。changes にはファイルの変更イベントが配列で指定されています。

このファイル変更イベントは :uri, :type というキーを持つハッシュデータで、 :uri には変更されたファイルの URI が、:type には変更の種類(作成: 1, 変更: 2, 削除: 4)が、それぞれ格納されています。

def workspace_did_change_watched_files(changes)
  # changes == [
  #   { uri: "file:///path/to/changed_file.rb", type: 2 },
  #   { uri: "file:///path/to/deleted_file.rb", type: 4 }
  # ]
end

:uri には変更されたファイルの URI が、 :type には変更の種類(作成: 1, 変更: 2, 削除: 4)が、それぞれ格納されています。

rbs_rails アドオンでは、変更の内容に応じて処理を切り替えています。

  • ファイルの追加、変更
  • 更新されたファイルが config/routes.rb または config/routes/*.rb だった場合は、ルーティングの型定義を生成
  • 更新されたファイルが db/schema.rb だった場合は、すべてのモデルの型定義を生成
  • 更新されたファイルがモデルだった場合は、そのモデルの型定義を生成
  • ファイルの削除
  • パスから型定義ファイルを推測し、存在すれば削除

大まかにコードで表現すると次のようなコードが実装されています (実際にはもう少し複雑です)。

def workspace_did_change_watched_files(changes)
  changes.each do |change|
    case change[:type]
    when 1, 2 # Created or Changed
      case change[:uri]
      when /config\/routes(?:\/.*)?\.rb$/
        generate_routes_signature
      when /db\/schema\.rb$/
        generate_all_model_signatures
      when /app\/models\/(.*)\.rb$/
        model_name = Regexp.last_match(1)
        generate_model_signature(model_name)
      end
    when 4 # Deleted
      delete_signature(change[:uri])
    end
  end
end

詳しくは rbs_rails アドオンの 実際の実装
読んでみてください。rbs_rails の型生成ロジックを呼び出すための薄いグルーコードですので、読みやすいと思います。

その他

rbs_rails アドオンでは利用していませんが、Ruby LSP アドオンは他にもさまざまなイベントに対応するメソッドを提供しています。

  • ホバー表示
  • コード補完
  • シンボルの検索
  • その他 LSP の各種イベントのフック

これらの詳細については Add-ons | Ruby LSP を読んでみてください。

出来上がりはこちら

ここまでのステップを元に、ひととおり機能を実装した rbs_rails アドオンがこちらです。

ソースコードは tk0miya/ruby-lsp-rbs_rails で公開しています。具体的なコードはリポジトリを確認してみてください。Ruby LSP が共通部分を提供してくれているため、ほんの 200 行強のコードで rbs_rails を LSP 化できました。

まとめ

  • ツールの多様化、分散は利用者にとっては混乱の元になる
  • あらたな LSP サーバを作るのではなく、Ruby LSP のアドオンとして実装することにした
  • Ruby LSP アドオンの作り方を紹介した

ruby-lsp-rbs_rails は、当初の提案より簡単に使えるようになったので、ぜひ試してみてください。

また、これをきっかけに便利な Ruby LSP アドオンを書いてみてください。

< 前の記事へ 次の記事へ >