Mui(無為)に TypeProf の LSP を使った型定義ファイルジャンプを実装した

はじめに

この記事は、僕が最近自作しているエディタであるMui(無為)でRubyの型定義ファイルである.rbsへ遷移できるようにした際の備忘録をまとめたものです。
実装時の詳細な情報などは個人的に利用しているesaにまとめてあるのですが、ちょうど最近技術ネタの記事をあまり書いていなかったのもあり、記事にしてみました。

ちなみに自作しているエディタのMui(無為)はヘイシャのブログで紹介しているのでそちらをご覧ください。

blog.agile.esm.co.jp

できたもの

以下のように、自作のエディタ上で動いているTypeProfのLSP経由で、型定義ファイル(.rbs)へ直接遷移できるようにしています。

やったこと

まずは自作のエディタ用のLSPプラグインには型定義ファイルへ遷移するためのコードを追加しました。

def type_definition(file_path:, line:, character:)
  # Only send to servers that support typeDefinitionProvider
  text_syncs = text_syncs_for(file_path).select do |ts|
    ts.client.server_capabilities["typeDefinitionProvider"]
  end

  if text_syncs.empty?
    # Fallback message: check if any server is running but doesn't support typeDefinition
    any_text_sync = text_syncs_for(file_path).first
    @editor.message = if any_text_sync
                                      "LSP: no server supports typeDefinition for this file"
                                    else
                                      server_unavailable_message(file_path)
                                    end
    return
  end

  uri = TextDocumentSync.path_to_uri(file_path)
  handler = Handlers::TypeDefinition.new(editor: @editor, client: text_syncs.first.client)

  # Collect results from all clients
  results_mutex = Mutex.new
  pending_count = text_syncs.size
  all_results = []

  text_syncs.each do |text_sync|
    text_sync.client.type_definition(uri: uri, line: line, character: character) do |result, _error|
      results_mutex.synchronize do
        all_results << result if result
        pending_count -= 1

        if pending_count.zero?
          merged = merge_locations(all_results)
          handler.handle(merged, nil)
        end
      end
    end
  end
end

ただ、手元の環境でTypeProfを使ってクラスの型定義ファイルへ遷移しようとすると、何故かクラスの定義位置へのみ遷移する挙動をしていました。
エディタ上での動きからして「そもそも型定義ファイルへの遷移は、まだ実装されていないのでは?」と考え、以下のようにRubyソースコードだった場合は型定義ファイルへ遷移するように変更しました。

def jump_to_type_file(file_path:, line: nil, character: nil)
  # For Ruby/RBS files, use custom toggle behavior
  if file_path&.end_with?(".rb", ".rbs")
    jump_to_ruby_type_file(file_path)
  else
    # For other languages, use LSP typeDefinition
    type_definition(file_path: file_path, line: line, character: character)
  end
end

Rubyのコードだけ特別扱いする形になるので実装としては筋がいいものではありませんが、一旦型情報を参照できるようにしたかったのでこれで妥協することにしました。

ただ、この実装で終わるのも面白くなかったので今回の実装で得た知見を活かして、TypeProf本体にもパッチを投げてみました。

github.com

このパッチで以下のことができるようになりました。

  • クラス名からクラス自体の型定義ファイルへ遷移できるようになった
  • foo = Foo.newのようにインスタンスを受け取っている変数からも型定義ファイルへ遷移できるようになった

実際の動作としては以下の動画のようになっています。

動作確認としてはVSCodeとMui(無為)それぞれで確認しています。
なので、このパッチがマージされれば他のエディタ上でも型定義ファイルへの遷移ができるようになると思います。

おわりに

今回の対応で.rbsへの遷移ができるようになりましたが、まだ型情報のホバー表示などには対応できていない状況です。
なので、今後はそのあたりを対応しつつ他の言語のLSPとの連携(たとえばTypeScriptの型情報への遷移とか)を試しつつ、開発を続けていきたいですね。