Emacs Eglot for Scala and Kotlin (JVM) - jointhefreeworldjointhefreeworld.org
Emacs Eglot for Scala and Kotlin (JVM)<br>estimated reading time: 17 minuteswritten on: 20/07/2026
When Emacs 29 made eglot the built-in, default Language Server Protocol (LSP) client, many of us rejoiced.
It is lightweight, fast, adheres strictly to Emacs philosophy, and doesn’t try to reinvent the wheel.
However, being minimal means that when an LSP server steps out of line or acts quirky, eglot doesn’t provide a million customizable toggles to fix it out-of-the-box. Instead, it expects you to leverage the power of Emacs Lisp.
In this post, I will dissect my production-ready eglot setup (part of my heks-emacs configuration) which I use in my day-to-day work, with Scala and Kotlin (and some Java).
For reference, find my full Eglot config here: https://codeberg.org/jjba23/heks-emacs/src/branch/trunk/src/modules/eglot.el
We will walk through basic language setups, specialized workspace configuration handling, and dive deep into some advanced JSON-RPC and advice-based workarounds for Scala (Metals) and Kotlin that make development truly seamless from Emacs and liberate you from IntelliJ ☺️.
It’s not perfect, but it’s pretty darn close to perfection if you ask me, and the developer experience and speed that it enables is just wild. Thank you Emacs, thank you GNU, thank you Eglot! 🐂
Before looking at the code, let’s talk about why we are doing this. For years, the conventional wisdom stated that if you write JVM languages, especially Scala or Kotlin, you must use IntelliJ IDEA. The narrative claimed that these languages are too complex for a standard text editor.
But what do you actually get with IntelliJ? A massive, monolithic Java application that frequently hogs 8GB+ of RAM, locks up your system while “indexing pre-built binaries,” and forces you into a closed proprietary ecosystem.
Emacs turns this paradigm on its head through three core strengths:
The Unix Philosophy of LSP: Instead of a single IDE trying to compile, index, and render your code simultaneously, Emacs splits these duties. Eglot acts as a lean, protocol-first transport layer that talks to dedicated language servers via JSON-RPC.
Infinite Hackability: If IntelliJ has a bug in how it auto-completes Kotlin code, you are stuck waiting for JetBrains to issue a patch. In Emacs, you can write a 10-line Lisp advice function to intercept the network payload and patch the bug live in your editor buffer.
Unified Interface: You use the same text-manipulation utilities, text-jumping tools (xref), and completion frameworks (corfu, company, etc.) whether you are adjusting a Nix expression, editing a Markdown file, or refactoring a massive Scala service.
Hooks, Keybindings, and Initial Configurations #
Let’s start with how eglot is initialized. I use Elpaca and use-package to manage the configuration, ensuring it doesn’t download an external package since it is built-in (:ensure nil). Then I add some hooks to automatically start the language server for certain modes.
(use-package eglot<br>:ensure nil<br>:hook ((scala-ts-mode . eglot-ensure)<br>(sh-mode . eglot-ensure)<br>(markdown-mode . eglot-ensure)<br>(markdown-ts-mode . eglot-ensure)<br>(nix-ts-mode . eglot-ensure)<br>(html-mode . eglot-ensure)<br>(css-mode . eglot-ensure)<br>(css-ts-mode . eglot-ensure)<br>(html-ts-mode . eglot-ensure)<br>(js-mode . eglot-ensure)<br>(js-ts-mode . eglot-ensure)<br>(kotlin-ts-mode . eglot-ensure)<br>(yaml-mode . eglot-ensure)<br>(yaml-ts-mode . eglot-ensure)<br>;; formatting<br>(before-save . eglot-format-buffer))<br>;; ..................<br>;; more config
Eglot-Ensure Everywhere: I hook eglot-ensure into almost every programming mode I use, adapting both classic modes and modern Tree-sitter (*-ts-mode) alternatives.
Auto-Formatting: Adding eglot-format-buffer to before-save guarantees code style compliance automatically every time a file hits the disk.
My keybindings are nested under the C-c i prefix, keeping them memorable and consistent across languages. The mnemonic keyword is “IDE” .
:bind (("C-c i i" . eglot-find-implementation)<br>("C-c i e" . eglot)<br>("C-c i k" . eglot-shutdown-all)<br>("C-c i r" . eglot-rename)<br>("C-c i x" . eglot-reconnect)<br>("C-c i a" . eglot-code-actions)<br>("C-c i m" . eglot-menu)<br>("C-c i f" . eglot-format-buffer)<br>("C-c i h" . eglot-inlay-hints-mode))<br>:init<br>(setq eglot-autoshutdown t<br>eglot-confirm-server-edits nil<br>eglot-report-progress t<br>eglot-extend-to-xref t<br>eglot-sync-connect 1<br>eglot-connect-timeout 60<br>eglot-autoreconnect t)
Then with these :init settings:
eglot-autoshutdown cleans up language server processes as soon as the last buffer managed by them is killed.
eglot-extend-to-xref allows Emacs’ cross-referencing commands to smoothly transition into external library files outside your workspace directory.
Fine-Tuning Server Definitions and Workspaces #
Under the :config block, we begin optimizing specific language servers. For instance,...