Emacs + ruby-lsp + Standardで快適コーディング

環境

まとめ

Ruby LSP | An opinionated language server for Ruby. Batteries included!

  • ruby-lsp はちょっと分かりにくいけど、分かれば気が利いていてインストールも割と簡単
    • 少なくとも Yard 依存の Solargraph よりは手間は少ない
  • Linter が add-on として対応しているケースもあり、エディタ / IDE 側の対応は一本化されるのかも
  • VS Code extension は問題なく動作
  • Emacs + lsp-mode ではハマりポイントあり

ruby-lsp gemの導入

  • gem install ruby-lsp(初回のみでおk)
  • ruby-lsp を利用するプロジェクト向けの Gemfile を準備(中身は空でよい)

この結果、ruby-lsp を起動すると自動的にプロジェクトの Gemfile を解釈してプロジェクトの .ruby-lsp/ 以下に ruby-lsp が利用する Gemfile.lock ファイルが生成される

逆に Gemfile がなくてプロジェクトの状態になっていないコードに対しては ruby-lsp を動作させる方法は今のところない。

なんとなく試してみるかーでゼロベースの Ruby コードに対して動かそうとしてもそれは無理ってこと。

add-on

プロジェクト側の Gemfile に gem を追加するとそれを自動的に起動時に検出して先ほどの .ruby-lsp/ 以下にインストールして(.ruby-lsp/ 以下の) Gemfile.lock に反映してくれる。ruby-lsp ではこれを composed bundle と読んでいる。

そのうち

Add-ons | Ruby LSP

にあるような一定のルールに従うコードを add-on として取り扱ってくれる。

プロジェクトの Gemfile にはこんな感じで追加しておく。後ほど詳しく触れるが、自分は Linter に Standard を利用しているので、それを追加している。

group :development do
  gem "standard", "<= 1.49", require: false
end

バージョンを明示している点は後ほど触れるが、ruby-lsp gem との互換性の問題が出るので、気をつけておく必要がある。

デフォルトで使う場合はとりあえず気にする必要はないが、デフォルトでは linter は何も指定されていないので動作しないし、Gemfile に追加するだけだと .ruby-lsp/ 用の bundle に追加はされるが、LSP クライアントの設定が済んでいないと動作はしない。

エディタ / IDE側の拡張

VS Code Extention

特に困ることはない。デフォルトですべての機能が使え、formatter, linter の変更も自由に extension の settings から行える。他の拡張と何も変わらないように見える。

Emacs + lsp-mode

Emacs で LSP を利用する方法として

  • Eglot ( Emacs 29.1 以降標準に取り込まれた )
  • lsp-mode サードパーティだがこっちも人気

があるが、ここでは lsp-mode を扱う。

単に使いたいだけなら以下のようにプロジェクトローカルに設定すると利用できる。

.dir-locals.el というファイルを用意して以下のように LSP client を指定する。

((ruby-mode . ((lsp-enabled-clients . (ruby-lsp-ls)))))

気をつけるのは ruby-lsp-ls という名前で、

  • ruby-lsp はエコシステムと gem の名前
  • language server の名前としては ruby-lsp-ls
    • ls と言っているのに上記のコードのように client 名としても機能する

というなかなかややこしい感じになっている。

カスタマイズはできないので新しくクライアントを作る

lsp-mode では対応する elisp に値を設定するクチがない場合、新しいクライアントを作って登録する方法で設定を加えることになるらしく、基本系はこんな感じになる。(use-package を使っている前提)

(defun my-ruby-lsp-initialization-options ()
  "Return initialization options for ruby-lsp."
  '(:formatter "standard"
    :linters ["standard"]))

(use-package lsp-mode
  :config
  (lsp-register-client
    (make-lsp-client
      :new-connection (lsp-stdio-connection "ruby-lsp")
      :major-modes '(ruby-mode ruby-ts-mode)
      :server-id 'my-ruby-lsp
      :initialization-options #'my-ruby-lsp-initialization-options
     ))
  )

これで

  • ruby-mode で
  • ruby-lsp コマンドを叩いて STDIO でやりとりする
  • my-ruby-lsp という名前の

LSP client を作成して登録している。で、

.dir-locals.el にて

((ruby-mode . ((lsp-enabled-clients . (my-ruby-lsp)))))

さっき指定した my-ruby-lsp という名前の client を起動するように設定している。必要最低限はこんな感じ。これは実際に自分が使っている設定である。

注意事項は

  • ruby-lsp-ls という既存の名前にぶつけても設定を上書きすることはできないので別な名前を付ける
  • 付けた名前で LSP client 側の設定を行う

設定内容は

Editors | Ruby LSP

にある initializationOptions で与える。デフォルトで全機能 ON なので何もしなければいちばんリッチな状態で使える。

上記の設定は

個人的に Linter は Standardrb に寄せてある

ので、その部分だけ追加しているものになる。

もう一つの注意点は :linters に与えるのは Vector であること。List を与えても JSON RPC で転送する内容としては Array になるが、lsp-mode が正しく解釈できず、language server からの response を待たずに lsp-mode が異常終了してしまう。1

上の Gemfile の内容も合わせて正しく設定できると、

Ruby LSP> Running bundle install for the composed bundle. This may take a while...
Ruby LSP> (snip
Resolving dependencies...
The Gemfile's dependencies are satisfied
[Standard Ruby] Activating Standard Ruby LSP addon v1.49.0
[Standard Ruby] Initialized Standard Ruby LSP addon 1.49.0
[RuboCop] Activating RuboCop LSP addon 1.75.7.
[RuboCop] Initialized RuboCop LSP addon 1.75.7.

こんな感じの出力を得られて ruby-lsp gem + standard add-on の構成で ruby-lsp 越しに Standard で lint できるようになった。

従来の linting は以下のように Flycheck から直接 standard を叩く形だが、

これが以下のように LSP 経由で動作するようになる。実は Flycheck には LSP と協調するモードがあり、何も指定がなければ LSP が有効になると Flycheck は自身でセットアップしているツール群よりも LSP の返答を優先するようになっており(無効化もできる)、これが本来の動作なのだ。

一見無駄な階層が増えているようにも見えるが、個人的には Flycheck の設定って意外と適切に反映するのに手間が掛かって分かりにくいので、これはこれでよいかもと思った。少なくとも ruby-lsp add-on で linter の追加ができるので、何かオリジナルの規約があっても比較的簡単に追加できるのはいろいろアイディアが膨らみそう。

ただし、ruby-lsp gem のバージョンによって standard など依存する add-on の gem のバージョンが決まるので、初期化の際に最新の ruby-lsp にアップデートされて ruby-lsp のバージョンとプロジェクトの Gemfile に入れた Linter のバージョンが合わない、といったことは容易に起きそう。注意が必要。

壊れたらいったん全部削除して入れ直すのが確実な気がする…。

またはdiagnosticsプロバイダとしての動作を無効に

さっき作った新しいクライアントにて LSP を通じた diagnostics を無効にする設定をすると Flycheck は自前の設定で動作するようになる。

(use-package lsp-mode
  :ensure t
  :config
  (lsp-register-client
    (make-lsp-client
      :new-connection (lsp-stdio-connection "ruby-lsp")
      :major-modes '(ruby-mode ruby-ts-mode)
      :server-id 'my-ruby-lsp
      :initialization-options #'my-ruby-lsp-initialization-options
      ))
  :custom
  (lsp-diagnostics-provider :none) ; <-
  )

これで add-on 経由でなく従来通りにグローバルインストールしたコマンドに linter を担わせることもできなくはない。

ただいずれにせよ標準の ruby-lsp-ls だとどうにもならない

別な方法として Flycheck 側に設定を加えて

(setq-default flycheck-disabled-checkers '(lsp))

上のようにすると LSP に制御を譲らないように見えるけど、これは

  • lsp が selected になる
  • lsp が disabled になる

両方動作して詰む。(なんでやねん)

  1. lsp-mode のドキュメントはぶっちゃけまったく分かりやすくはないと思う。 

More