Decoupling JVM Development from IntelliJ: Production-Grade Scala and Kotlin via Emacs Eglot
Emacs 29's native LSP client, Eglot, offers a lightweight, protocol-first alternative to resource-heavy JVM IDEs. By leveraging Elisp advice and custom workspace configurations, systems engineers can resolve LSP deviations in Metals and the Kotlin language server to achieve seamless, multi-language navigation.
Architectural Motivation
The conventional approach to JVM development relies on monolithic IDEs like IntelliJ IDEA, which routinely demand upward of 8GB of RAM and degrade system responsiveness during binary indexing. By contrast, an Emacs-based setup using Eglot adheres to the Unix philosophy by decoupling transport from processing.
Eglot functions as a minimal, protocol-first transport layer that communicates with backend language servers via JSON-RPC. This architecture provides a unified interface for text manipulation, cross-referencing, and autocompletion while remaining lightweight and hackable.
Server Provisioning and Initialization
Eglot is configured as a built-in package (`:ensure nil`) initialized via `use-package` hooks linked to major tree-sitter modes such as `scala-ts-mode` and `kotlin-ts-mode`. To optimize performance and lifecycle management, settings like `eglot-autoshutdown` automatically terminate server processes when their managing buffers are closed, while `eglot-extend-to-xref` enables cross-referencing to resolve symbols outside the immediate workspace directory.
For Scala development, the Metals server is initiated with specific JVM tuning parameters: a 4GB heap limit (`-Xmx4G`) and the low-latency Z Garbage Collector (`-XX:+UseZGC`). Metals HTTP communication is explicitly enabled. For Kotlin, the setup bypasses standard tools in favor of the JetBrains-backed Kotlin Language Server (`intellij-server --stdio`).
Fine-Tuning Workspace Settings
Customizing downstream variables is managed via `eglot-workspace-configuration`. For Metals, this controls inlay hint density—enabling hints for inferred types and type parameters while disabling them for implicit arguments and conversions to prevent visual clutter in code buffers. Additionally, Bloop JVM properties are configured with a 4GB heap.
The global workspace configuration also maps distinct YAML schemas to remote URLs based on filename pattern matches. This ensures real-time structural validation of configurations directly in the editor.
Engineering Workarounds for Eldoc and URI Translation
Because minimal LSP clients do not provide complex out-of-the-box toggles for non-conforming servers, developers must use Emacs Lisp to address protocol quirks.
First, to prevent Eldoc from becoming saturated by generic hover data, Flymake diagnostics are prioritized. This is achieved by prepending `flymake-eldoc-function` to `eldoc-documentation-functions` and setting `eldoc-documentation-strategy` to `eldoc-documentation-compose`.
Second, during dependency navigation, the Kotlin language server returns file references formatted as `jar:///path/to/library.jar!/File.kt`. Because Emacs cannot natively resolve this URI scheme, jumping to definitions fails.
The solution wraps Eglot's URI translators with advice to rewrite the protocol prefix. By advising `eglot-uri-to-path` and `eglot-path-to-uri` with `:around` filters, custom Lisp functions map `jar:///` to `jar:file:///`. This translation enables companion packages like `jarchive` to correctly extract and open read-only dependency files.