Rubyで簡単な言語サーバーを書いてみた

言語サーバー（language server）を自作できるようにしておくと何かと便利そうなので、自分用の最低限の叩き台みたいなものを書いてみました。




<!-- ■■■■■■■■ -->
# 今回作ったものの概要

- 言語サーバーを子プロセスとして起動して標準入力・標準出力で通信する一般的な方式
- 例として `test` という文字列を含む行に警告を表示する機能だけ
  - `textDocument/publishDiagnostics` を使う
- なくても動く部分は省略、エラーハンドリングは適当

---

動作イメージはこんな感じ:

![Peek 2025-03-23 09-11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/211607aa-30a5-42e8-8d5b-3b5ed55c04e9.gif)


<!-- ■■■■■■■■ -->
# 準備

サーバー側を書くのが目的なので動作確認できればクライアントは何でもよいです。今回は Emacs を使いました。好きなものを使いましょう。

- Emacs 29.1 以上
- 必要なファイルは `langserver.rb` と `test_init.el` だけ（下記参照）
  - この2つを同じディレクトリに置く

これだけで試せます。
Emacs 29.1 以降だと Eglot が標準で付いてくるようになった（ [NEWS.29.1](https://www.gnu.org/savannah-checkouts/gnu/emacs/news/NEWS.29.1) ）ので、用意するのはほんとにこれだけ。

---

VSCode をクライアントとして使いたい場合は以下の記事を参照してください。

<!-- Rubyで簡単な言語サーバーを書いてみた（VSCode版） #LSP - Qiita -->
https://qiita.com/sonota88/items/9f7d665736f89b4d4fb3



## test_init.el

Eglot 向けの設定です。

```emacs-lisp
(progn
  ;; (1) 既存の設定を一旦消す
  (setq eglot-server-programs '())

  (let* ((this-dir (file-name-directory (buffer-file-name)))              ; 作業ディレクトリのフルパス
         (cmd (list "ruby" (expand-file-name "langserver.rb" this-dir)))) ; 言語サーバーを起動するコマンド
    ;; (2) 設定を追加する
    (add-to-list 'eglot-server-programs
                 (cons 'sh-mode cmd))))
```

メジャーモードとサーバー起動コマンドの対応関係を連想リスト `eglot-server-programs` に登録する、という内容ですね。

何かしらのメジャーモードと紐付けないといけないので今回は適当に sh-mode を選びました。お試し用なのでこれは何でもよいです。


---

ちなみにマニュアルに乗っている設定例はこんな感じ。

```emacs-lisp
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(foo-mode . ("fools" "--stdio"))))
```

参考: [Setting Up LSP Servers (Eglot: The Emacs Client for the Language Server Protocol) (www.gnu.org)](https://www.gnu.org/software/emacs/manual/html_node/eglot/Setting-Up-LSP-Servers.html)







## langserver.rb

[Language Server Protocol に対応したミニ言語処理系を作る (zenn.dev / takl)](https://zenn.dev/takl/books/0fe11c6e177223) を参考にさせてもらいました。


https://gist.github.com/sonota88/0a0d5015abf2b64e7eca11d54e71df99

今回は素朴な書き方にしましたが

```ruby
on_request "initialize" do |msg|
  # ...
end
```

みたいに書けるようにすると Ruby っぽいかもしれません。



<!-- ■■■■■■■■ -->
# 動かす

- `test_init.el` を開いてバッファの内容を評価する
  - `M-x eval-buffer` とか `eval-last-sexp` とかで
- 適当なファイル `sample.sh` を用意して Emacs で開く
  - 自動的に sh-mode になる
- `M-x eglot` で言語サーバーを起動
- `sample.sh` を編集すると Eglot が言語サーバーとやりとりして画面上にフィードバックが表示される
  - 保存は不要。編集された現在のバッファの内容を元にフィードバックが表示される


動かす手順はこれだけです。Emacs + Eglot だとこれだけで試せるので手軽でよいですね。 クライアント側の用意が楽。 :thumbsup: 

---

編集時の挙動はこんな感じです:

![Peek 2025-03-23 09-11.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234055/211607aa-30a5-42e8-8d5b-3b5ed55c04e9.gif)

- `test` という文字列を含む行に赤い下線（と `!!`）が表示される
- `test` という文字列を含まなくなった行の赤い下線が消える
- 警告が出ている箇所にカーソルを合わせるとメッセージがエコーエリアに表示される
- マウスカーソルのホバーでもメッセージが表示される



<!-- ■■■■■■■■ -->
# メモ

- これを育てて、昔作った自作言語の言語サーバーも書いてみたい（そのうち……）
  - [（2週間ちょっとで）Rubyでかんたんな自作言語のコンパイラを作った](https://qiita.com/sonota88/items/2b95378b43a22109513c)
- レスポンスを print した後の `$stdout.flush` を忘れててちょっとハマった
- 今回は「`test` という文字列を含む行」に警告を表示するというだけの処理だったが、ここは他のコマンドやライブラリと連携させれば手軽にいろんなことができそう。
  - 参考: [Big Sky :: Lint ツールを Language Server に対応させるコマンド efm-langserver 作った。](https://mattn.kaoriya.net/software/lang/go/20190205190203.htm)
- `=>` を初めて使った
  - [パターンマッチ (Ruby 3.4 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/3.4/doc/spec=2fpattern_matching.html)




## Eglot

- company mode などを用意しなくても Eglot だけでも使える
- 挙動を把握しきれてないが、サーバーを再起動したいときは `M-x eglot` すればよいっぽい。
- 関連付けの登録方法と `M-x eglot` だけ知っていればとりあえず使える。標準で入ってるし使い始めのハードルが低くて良い。



<!-- ■■■■■■■■ -->
# バージョン

```
Ruby 3.2.3 ... Ubuntu 24.04 のデフォルト
Emacs 29.3
Eglot 1.12.29
```



<!-- ■■■■■■■■
# 参考

- [Language Server Protocol に対応したミニ言語処理系を作る (zenn.dev / takl)](https://zenn.dev/takl/books/0fe11c6e177223)
- [Language Server Protocol の仕様 及び実装方法 (zenn.dev / shiba)](https://zenn.dev/mtshiba/books/language_server_protocol)
 -->


<!-- ■■■■■■■■ -->
# この記事を読んだ人は（ひょっとしたら）こちらも読んでいます

<!-- Emacs Lispでシンプルな自作言語のコンパイラを書いた -->
https://qiita.com/sonota88/items/a8b612e75b8f13d55772