プログラミング
IntelliJからの脱却: Emacs EglotでのScalaおよびKotlin LSP
Escape IntelliJ: Scala and Kotlin LSPs on Emacs Eglot (jointhefreeworld.org)
要約
この記事は、EmacsのEglotクライアントを使用してScalaやKotlinといったJVM言語の開発環境をIntelliJ IDEAから移行する方法について解説しています。Eglotの軽量さとEmacs Lispによるカスタマイズ性を活用し、言語サーバーの設定や高度なワークアラウンドを駆使することで、IntelliJに匹敵する、あるいはそれを超える開発体験を実現する方法が紹介されています。
全文翻訳
Emacs 29でEglotが組み込みのデフォルト言語サーバープロトコル(LSP)クライアントになったとき、多くの人が歓喜しました。軽量で高速、Emacsの哲学に厳密に従っており、車輪の再発明を試みません。しかし、ミニマルであるということは、LSPサーバーが規律を破ったり風変わりな動作をしたりした場合、Eglotはそれを修正するためのカスタマイズ可能なトグルを数百万個提供しないということです。代わりに、Emacs Lispの力を活用することを期待しています。
この投稿では、Scala、Kotlin(および一部のJava)で日常業務に使用している、本番環境対応のEglotセットアップ(私のheks-emacs構成の一部)を詳細に解説します。参考として、私の完全なEglot構成はここにあります: https://codeberg.org/jjba23/heks-emacs/src/branch/trunk/src/modules/eglot.el
基本的な言語セットアップ、特殊なワークスペース構成の処理、そしてScala(Metals)とKotlinのための高度なJSON-RPCおよびadviceベースの回避策について深く掘り下げ、Emacsからの開発を真にシームレスにし、IntelliJから解放します☺️。
完璧ではありませんが、私に言わせればほぼ完璧に近く、それが可能にする開発者体験と速度は驚異的です。Emacs、GNU、Eglotに感謝します!🐂
コードを見る前に、なぜこれを行うのかについて話しましょう。長年、JVM言語、特にScalaやKotlinを書くならIntelliJ IDEAを使わなければならないというのが一般的な考え方でした。これらの言語は標準的なテキストエディタには複雑すぎるという物語がありました。しかし、IntelliJで実際に得られるものは何でしょうか?頻繁に8GB以上のRAMを消費し、「インデックス作成済みバイナリのインデックス作成中」にシステムをロックアップし、閉鎖的なプロプライエタリなエコシステムにあなたを閉じ込める、巨大でモノリシックなJavaアプリケーションです。
Emacsは、3つの主要な強みによってこのパラダイムを覆します:
LSPのUnix哲学: コードのコンパイル、インデックス作成、レンダリングを同時に行う単一のIDEの代わりに、Emacsはこれらのタスクを分割します。Eglotは、JSON-RPC経由で専用の言語サーバーと通信する、軽量でプロトコル優先のトランスポートレイヤーとして機能します。
無限のハック可能性: IntelliJのKotlinコードの自動補完にバグがあった場合、JetBrainsがパッチを発行するのを待つしかありません。Emacsでは、ネットワークペイロードをインターセプトしてエディタバッファでバグをライブで修正する10行のLisp advice関数を書くことができます。
統一されたインターフェース: Nix式を調整する場合でも、Markdownファイルを編集する場合でも、大規模なScalaサービスをリファクタリングする場合でも、同じテキスト操作ユーティリティ、テキストジャンプツール(xref)、および補完フレームワーク(corfu、companyなど)を使用します。
フック、キーバインディング、初期構成
Eglotの初期化方法から始めましょう。Elpacaとuse-packageを使用して構成を管理します。これにより、組み込みパッケージであるため外部パッケージをダウンロードしないことが保証されます(:ensure nil)。次に、特定のモードで言語サーバーを自動的に開始するためのフックを追加します。
(use-package eglot :ensure nil :hook ((scala-ts-mode . eglot-ensure) (sh-mode . eglot-ensure) (markdown-mode . eglot-ensure) (markdown-ts-mode . eglot-ensure) (nix-ts-mode . eglot-ensure) (html-mode . eglot-ensure) (css-mode . eglot-ensure) (css-ts-mode . eglot-ensure) (html-ts-mode . eglot-ensure) (js-mode . eglot-ensure) (js-ts-mode . eglot-ensure) (kotlin-ts-mode . eglot-ensure) (yaml-mode . eglot-ensure) (yaml-ts-mode . eglot-ensure) ;; formatting (before-save . eglot-format-buffer)) ;; .................. ;; more config )
Eglot-Ensure Everywhere: 使用するほとんどすべてのプログラミングモードにeglot-ensureをフックし、クラシックモードとモダンなTree-sitter(*-ts-mode)の両方の代替に対応します。
自動フォーマット: before-saveにeglot-format-bufferを追加することで、ファイルがディスクに保存されるたびにコードスタイルコンプライアンスが自動的に保証されます。
キーバインディングはC-c iプレフィックスの下にネストされており、記憶しやすく、言語間で一貫性があります。ニーモニックキーワードは「IDE」です。
:bind (("C-c i i" . eglot-find-implementation) ("C-c i e" . eglot) ("C-c i k" . eglot-shutdown-all) ("C-c i r" . eglot-rename) ("C-c i x" . eglot-reconnect) ("C-c i a" . eglot-code-actions) ("C-c i m" . eglot-menu) ("C-c i f" . eglot-format-buffer) ("C-c i h" . eglot-inlay-hints-mode))
:init (setq eglot-autoshutdown t eglot-confirm-server-edits nil eglot-report-progress t eglot-extend-to-xref t eglot-sync-connect 1 eglot-connect-timeout 60 eglot-autoreconnect t)
これらの:init設定により:
eglo-autoshutdownは、それによって管理されている最後のバッファが閉じられるとすぐに言語サーバープロセスをクリーンアップします。
eglo-extend-to-xrefは、Emacsのクロスリファレンスコマンドがワークスペースディレクトリ外の外部ライブラリファイルにスムーズに移行できるようにします。
サーバー定義とワークスペースの微調整
:configブロックの下で、特定の言語サーバーの最適化を開始します。たとえば、デフォルト構成を削除してからカスタムエントリを再追加することで、競合を防ぎます。
(setopt eglot-code-action-indications nil) ;; Emacs 31の視覚的なノイズをクリーンアップ
;; ScalaとKotlinのクリーンなスレート
(setq eglot-server-programs (assq-delete-all 'scala-mode eglot-server-programs))
(setq eglot-server-programs (assq-delete-all 'scala-ts-mode eglot-server-programs))
(setq eglot-server-programs (assoc-delete-all 'scala-ts-mode eglot-server-programs))
(add-to-list 'eglot-server-programs `(scala-ts-mode . ("metals" "-Xmx4G" "-XX:+UseZGC" "-Dmetals.http=true" :initializationOptions (:isHttpEnabled t))))
(setq eglot-server-programs (assoc-delete-all 'kotlin-ts-mode eglot-server-programs))
(add-to-list 'eglot-server-programs '(kotlin-ts-mode . ("intellij-server" "--stdio")))
これらの変更の理由は?
Scala (Metals): Metalsに直接JVMチューニングフラグを渡します(快適な4GBヒープを割り当て、レイテンシを最小限に抑えるためにZガベージコレクタを使用します)。また、初期化オプションを介してMetals HTTP通信を有効にすると、必要に応じて特殊なUI機能にフックできます。
Kotlin: 標準オプションをIntelliJベースのKotlin言語サーバー(intellij-server --stdio)に切り替えます。
グローバルワークスペース構成
eglo-workspace-configurationを使用すると、カスタマイズされた変数を言語サーバーに下流に渡すことができます。私の構成のこのセクションは、ユニバーサルなsettings.jsonのように機能します。
(setq-default eglot-workspace-configuration '
(:metals
(:autoImportBuild "all"
:isHttpEnabled t
:superMethodLensesEnabled t
:showInferredType t
:enableSemanticHighlighting t
:inlayHints
(:inferredTypes (:enable t)
:implicitArguments (:enable nil)
:implicitConversions (:enable nil)
:typeParameters (:enable t)
:hintsInPatternMatch (:enable nil))
:bloopJvmProperties ["-Xmx4G"])
:haskell (:formattingProvider "ormolu")
:typescript (:format (:baseIndentSize 0 :convertTabsToSpaces t :indentSize 2 :semicolons "remove" :tabSize 2))
:javascript (:format (:baseIndentSize 0 :convertTabsToSpaces t :indentSize 2 :semicolons "remove" :tabSize 2))
:rust-analyzer (:check (:command "clippy")
:cargo (:sysroot "discover" :features "all" :buildScripts (:enable t))
:diagnostics (:disabled ["macro-error"])
:procMacro (:enable t))
:yaml
(:format (:enable t)
:validate t
:hover t
:completion t
:schemas
(https://codeberg.org/jjba23/pop-test/raw/branch/trunk/resources/json-schema/pop-test.json ["golden-test.yaml" "golden-test.yml" "pop-test.yaml" "pop-test.yml"])
(https://raw.githubusercontent.com/Vandebron/gh-mpyl/refs/heads/main/src/mpyl/schema/project.schema.yml ["project.yml"])
(https://json.schemastore.org/yamllint.json ["/*.yml"])
:schemaStore (:enable t))
:nil (:formatting (:command ["nixfmt"])))
)
ここでの注目すべき構成:
Metals: 推論された型と型パラメータに対して、きめ細かなinlayヒントがアクティブ化され、バッファを読みやすく保つために暗黙的な変換はミュートされます。(その他のオプションはこちら: https://scalameta.org/metals/docs/editors/user-configuration/)
YAMLスキーママッピング: 異なるインターネットホストのJSONスキーマを、YAMLファイルのパターンに直接自動的にマッピングします。
深掘り: 回避策
ここからが面白くなります。