ここ数年、LookML, SQL, HCL などを書くことが多く、Ruby などのプログラミング言語を書くことが減っていましたが、今月から Rails 開発に戻ってきました。

そのため、重い腰を上げて Emacs で LSP (Language Server Protocol ) を使えるようにしてみました。

その過程で Tree-sitter も知ったので、合わせて設定しました。

※ LSP は Language Server が提供する高度なコード支援の仕組みで、Tree-sitter はコードから AST を高速に生成してハイライトや構造編集を可能にする構文解析エンジンだそうです。

使っている Emacs のバージョン

Emacs は以下の理由から、最新の 30.2 ではなく 29.1 を使っています。

LSP の導入

lsp-mode を採用した

Emacs 29 標準の eglot と、サードパーティの lsp-mode を比較しました。

出来れば標準の eglot にしたかったのですが、eglot が Ruby の Language Server として要求する Solargraph はパフォーマンスが良くない情報もあり、lsp-mode を採用することにしました。lsp-mode は Language Server として ruby-lsp を要求します。

Solargraph が本当にパフォーマンスが良くないかは、確認していません 😅

lsp-mode の設定例

まずは各言語に対応する Language Server を、Emacs が認識する PATH (exec-path) にインストールします。

各言語と Language Server の対応は、lsp-mode のドキュメントの Languages に記載されています。

以下は今回インストールした Language Server です。

Lang Language Server
Dockerfile dockerfile-language-server-nodejs
Go gopls
JSON vscode-json-languageserver
Ruby ruby-lsp , ruby-lsp-rails
TypeScript typescript-language-server

次に lsp-mode を Melpa からインストール します。

あとは、有効にしたい major-mode の hook に、lsp-deferred 関数を追加すれば完了です。

以下は ruby-mode への設定例です。

(add-hook 'ruby-mode-hook #'lsp-deferred)

これで M-. で定義元にジャンプして M-, で戻ったり、他にインデント崩れを指摘してくれたりします。他の機能は lsp-mode のドキュメント をご覧ください。

私は以下も設定しました。

(setq lsp-keymap-prefix "C-c C-l")

;; ruby-lsp がインストールされていても rubocop が優先されてしまうので、無効にする。
(setq lsp-disabled-clients '(rubocop-ls))

;; lsp-mode が有効な全バッファで、保存時にファイルを整形する。
(setq-default lsp-format-buffer-on-save t)

Emacs に xxx-ts-mode が追加されていることを知る

TypeScript の lsp-mode を設定しようとした時に、以前の Emacs になかった xxx-ts-mode があることに気づきました。

$ find /Applications/Emacs.app -type f -name '*-ts-mode.elc' | sort
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/c-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/cmake-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/dockerfile-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/go-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/java-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/json-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/ruby-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/rust-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/progmodes/typescript-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/textmodes/toml-ts-mode.elc
/Applications/Emacs.app/Contents/Resources/lisp/textmodes/yaml-ts-mode.elc

正規表現ベースで構文を解析する従来の xxx-mode に対して、xxx-ts-mode は Tree-sitter ベースで AST レベルで構文を解析するそうです。そのため、構文ハイライトの精度が高く、インデント・構造編集が安定しやすいとのこと。

スルーしようかと思いましたが、typescript-mode の README.md を見たら、以下のとおり開発が停止していることを知りました。

Essentially all major development of typescript-mode has come to a halt.

そのため、typescript-ts-mode を取っ掛かりに、xxx-ts-mode を導入することにしました。サードパーティの xxx-mode を捨てられるメリットも感じました。

typescript-ts-mode の導入

以下を init.el に記載することで、TypeScript 向けの Tree-sitter をインストールできます。

(setq treesit-language-source-alist
      '((tsx "https://github.com/tree-sitter/tree-sitter-typescript" "v0.23.2" "tsx/src")
        (typescript "https://github.com/tree-sitter/tree-sitter-typescript" "v0.23.2" "typescript/src")))

(dolist (element treesit-language-source-alist)
  (let ((lang (car element)))
    (unless (treesit-language-available-p lang)
      (treesit-install-language-grammar lang))))

これが評価されると、GitHub から取得したソースコードをビルドして出来た *.dylib~/.emacs.d/tree-sitter/ 直下にインストールされます。

  • libtree-sitter-tsx.dylib
  • libtree-sitter-typescript.dylib

💡 タグを指定しないとデフォルトブランチが参照されます。commit hash は指定できなかったので、セキュリティとバージョン固定の観点から、タグを指定しました。

あとは、*.ts*.tsx をそれぞれ typescript-ts-mode と tsx-ts-mode に紐づけるだけです。合わせて lsp-mode も有効化します。

※ tsx-ts-mode も typescript-ts-mode.el に定義されています。

;; TypeScript

(add-to-list 'auto-mode-alist '("\\.ts\\'" . typescript-ts-mode))

(defun typescript-ts-mode-hook-func ()
  (lsp-deferred))
(add-hook 'typescript-ts-mode-hook #'typescript-ts-mode-hook-func)

;; TSX

(add-to-list 'auto-mode-alist '("\\.tsx\\'" . tsx-ts-mode))

(defun tsx-ts-mode-hook-func ()
  (lsp-deferred))
(add-hook 'tsx-ts-mode-hook #'tsx-ts-mode-hook-func)

補足: tree-sitter-langs パッケージを採用しなかった理由

tree-sitter-langs パッケージでも *.dylib をインストールできますが、以下の理由から採用を見送りました。

  • ~/.emacs.d/tree-sitter/ ではなく、~/.emacs.d/elpa/tree-sitter-langs-20251019.1145/bin/ 以下にインストールされる
    • このディレクトリ名は tree-sitter-langs パッケージのバージョンアップで、変更されていく
  • ライブラリ名が libtree-sitter-typescript.dylib ではなく、typescript.dylib などという名前である

各言語の Tree-sitter parser の場所を確認するには良いと思います。

他 xxx-ts-mode の導入

前述の Emacs built-in の xxx-ts-mode のうち、利用頻度が高いものを導入しました。

  • dockerfile-ts-mode
  • go-ts-mode
  • json-ts-mode
  • ruby-ts-mode

例外で yaml-ts-mode は導入しませんでした。保存時にインデント 4 に整形され、2 に変更する方法が分からなかったためです。

最終的な LSP mode, Tree-sitter, xxx-ts-mode の設定

おまけ: auto-complete から company-mode への移行

lsp-mode のコードに、company-mode の利用箇所がありました。

company-mode は名前を聞いたことがある程度でしたが、どうやらコード入力を支援する補完フレームワークとのこと。

長年利用してきた auto-complete を company-mode に移行しました。

今は company-mode より、軽量な corfu が良いみたいな情報を見かけましたが、今回は簡単そうな company-mode の導入としました。

(setq company-minimum-prefix-length 2)
(setq company-show-quick-access t)

(add-hook 'after-init-hook #'global-company-mode)

課題

  • lsp-mode が有効な全バッファで lsp-format-buffer-on-save を有効にすると、他の人が書いたコードがゴリッと整形されるので微妙かも。特に YAML ファイル
  • global-company-mode はちょっと noisy なので、個々の major-mode への設定にしたほうが良いかも。あとで変更するかも

まとめ

今さらながら Emacs を LSP + Tree-sitter で現代的な設定に近づけました。ただ、まわりに Emacs ユーザーがほぼいないので、どこまで近づけたかはよく分かりません。

サードパーティのパッケージも少し減らせました。依存関係は出来るだけ減らしたいと思っているので、良かったです。

  • 追加:
    • company
    • lsp-mode
  • 削除:
    • auto-complete
    • dockerfile-mode
    • go-autocomplete
    • go-eldoc
    • go-mode

参考情報