逃离 IntelliJ:在 Emacs Eglot 上使用 Scala 和 Kotlin 的 LSP
摘要
一份关于配置 Emacs Eglot 用于 Scala 和 Kotlin 开发的详细指南,提供了 IntelliJ IDEA 的轻量级替代方案,并包含自定义 LSP 配置。
<p><a href="https://lobste.rs/s/ivtjcv/escape_intellij_scala_kotlin_lsps_on">评论</a></p>
查看缓存全文
缓存时间: 2026/07/21 10:38
# Emacs Eglot 配置 Scala 和 Kotlin(JVM)
来源:https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html
当 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 你实际上得到了什么?一个庞大、臃肿的 Java 应用程序,经常占用超过 8GB 的内存,在“索引预构建二进制文件”时锁死你的系统,并将你强行推进封闭的专有生态系统。
Emacs 通过三大核心优势颠覆了这种模式:
- **LSP 的 Unix 哲学**:Emacs 不像单一 IDE 那样同时尝试编译、索引和渲染代码,而是将职责分离。`Eglot` 作为一个精简的、以协议为先的传输层,通过 JSON-RPC 与专用的语言服务器通信。
- **无限的可定制性**:如果 IntelliJ 在自动补全 Kotlin 代码时存在 Bug,你只能等待 JetBrains 发布补丁。而在 Emacs 中,你可以编写一个 10 行的 Lisp advice 函数来拦截网络负载,并在编辑器缓冲区中实时修复该 Bug。
- **统一界面**:无论你是调整 Nix 表达式、编辑 Markdown 文件,还是重构大型 Scala 服务,你都使用相同的文本操作工具、文本跳转工具(`xref`)和补全框架(`corfu`、`company` 等)。
---
## 钩子、键绑定和初始配置# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#hooks-keybindings-and-initial-configurations)
让我们从如何初始化`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)
(before-save . eglot-format-buffer))
)
``
- **Eglot-Ensure 无处不在:** 我将 `eglot-ensure` 挂接到几乎所有我使用的编程模式中,同时适配经典模式和现代 Tree-sitter (`*-ts-mode`) 模式。
- **自动格式化:** 将 `eglot-format-buffer` 添加到 `before-save` 能确保每次文件保存到磁盘时自动符合代码风格。
我的键绑定嵌套在 `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` 设置:
- `eglot-autoshutdown` 会在最后一个由语言服务器管理的缓冲区被关闭后,自动清理语言服务器进程。
- `eglot-extend-to-xref` 允许 Emacs 的交叉引用命令平滑地跳转到工作区目录之外的外部库文件。
---
## 精细调整服务器定义和工作区# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#fine-tuning-server-definitions-and-workspaces)
在 `:config` 块中,我们开始优化特定语言服务器。例如,在重新添加自定义条目之前删除默认配置,以防止冲突。
``
:config
(setopt eglot-code-action-indications nil)
(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):** 我直接将特定的 JVM 调优参数传递给 Metals(分配舒适的 4GB 堆,并使用 Z 垃圾收集器以最小化延迟)。同时,通过初始化选项启用 Metals 的 HTTP 通信,以便在需要时接入专门的 UI 功能。
- **Kotlin:** 我将标准选项替换为 IntelliJ 支持的 Kotlin 语言服务器 (`intellij-server --stdio`)。
## 全局工作区配置# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#global-workspace-configurations)
`eglot-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 hints)被启用,专门用于推断的类型和类型参数,同时禁用隐式转换以保持缓冲区可读性。(更多选项在此:https://scalameta.org/metals/docs/editors/user-configuration/)
- **YAML Schema 映射:** 将不同的互联网托管的 JSON schema 直接映射到特定模式的 YAML 文件,自动生效。
---
## 深入探讨:变通方案# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#deep-dive-the-workarounds)
这是变得有趣的地方。有时服务器会违反标准的 LSP 预期,需要自定义的 Emacs Lisp 逻辑来弥合差距。
### 修复 Eldoc 过载# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#fixing-eldoc-overload)
默认情况下,`eldoc` 很容易被不同的反馈机制淹没。以下代码块将结构性诊断信息优先于通用的悬停数据:
``
(add-hook 'eglot-managed-mode-hook
(lambda ()
(setq eldoc-documentation-functions
(cons #'flymake-eldoc-function
(remove #'flymake-eldoc-function eldoc-documentation-functions)))
(setq eldoc-documentation-strategy #'eldoc-documentation-compose)))
``
### Kotlin 源代码导航(Jar URI 转换)# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#kotlin-source-navigation-jar-uri-translation)
当使用 Kotlin 进入依赖库时,服务器返回的文件引用格式为 `jar:///path/to/library.jar!/File.kt`。Emacs 默认无法直接解析这种格式,在你尝试跳转到定义时会抛出错误。
通过用 advice 包装 Eglot 的 URI 转换器,我们可以将此自定义格式映射为 Emacs 能理解的内容(特别是配合像 `jarchive` 这样的辅助扩展):
``
(defun heks/eglot-uri-to-path-kotlin (orig-fn uri &rest args)
(if (and (stringp uri) (string-prefix-p "jar:///" uri))
(apply orig-fn (replace-regexp-in-string "^jar:///" "jar:file:///" uri) args)
(apply orig-fn uri args)))
(defun heks/eglot-path-to-uri-kotlin (orig-fn path &rest args)
(if (and (stringp path) (string-prefix-p "jar:file:///" path))
(replace-regexp-in-string "^jar:file:///" "jar:///" path)
(apply orig-fn path args)))
(if (fboundp 'eglot-uri-to-path)
(progn
(advice-add 'eglot-uri-to-path :around #'heks/eglot-uri-to-path-kotlin)
(advice-add 'eglot-path-to-uri :around #'heks/eglot-path-to-uri-kotlin))
(progn
(advice-add 'eglot--uri-to-path :around #'heks/eglot-uri-to-path-kotlin)
(advice-add 'eglot--path-to-uri :around #'heks/eglot-path-to-uri-kotlin)))
``
### 拦截 Kotlin 自动补全中的空 `newText` Bug# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#intercepting-the-kotlin-empty-codenewtextcode-auto-completion-bug)
某些 Kotlin LSP 版本存在一个臭名昭著的自动补全问题。服务器报告了匹配的候选项,但错误地附带了一个包含空字符串的 `textEdit` 字段(`newText: ""`)。这会导致 Eglot 完全擦除你正在补全的单词。
为了解决这个问题,我截获了传入的 JSON-RPC 响应负载,包括同步和异步请求。如果 Kotlin 补全候选项返回了空字符串编辑,我们就完全剥离 `textEdit` 属性,迫使 Eglot 优雅地回退到标准前缀匹配。
``
(defun my-jsonrpc-request-kotlin-fix (orig-fn connection method params &rest args)
"修复 kotlin-lsp 的空 newText 错误,移除 textEdit 以触发 Eglot 回退。"
(let ((result (apply orig-fn connection method params args)))
(when (and (eq method :textDocument/completion)
(derived-mode-p 'kotlin-mode 'kotlin-ts-mode)
result)
(let ((items (if (vectorp result) result (plist-get result :items))))
(seq-do (lambda (item)
(let ((text-edit (plist-get item :textEdit)))
(when (and text-edit (equal (plist-get text-edit :newText) ""))
(plist-put item :textEdit nil))))
items)))
result))
(defun my-jsonrpc-async-request-kotlin-fix (orig-fn connection method params &rest args)
"修复 Eglot 异步请求中的 kotlin-lsp 空 newText 错误。"
(if (and (eq method :textDocument/completion)
(derived-mode-p 'kotlin-mode 'kotlin-ts-mode))
(let* ((orig-success (plist-get args :success-fn))
(new-success (lambda (result)
(let ((items (if (vectorp result) result (plist-get result :items))))
(seq-do (lambda (item)
(let ((text-edit (plist-get item :textEdit)))
(when (and text-edit (equal (plist-get text-edit :newText) ""))
(plist-put item :textEdit nil))))
items))
(funcall orig-success result)))
(new-args (plist-put (copy-sequence args) :success-fn new-success)))
(apply orig-fn connection method params new-args))
(apply orig-fn connection method params args)))
(advice-add 'jsonrpc-request :around #'my-jsonrpc-request-kotlin-fix)
(advice-add 'jsonrpc-async-request :around #'my-jsonrpc-async-request-kotlin-fix)
``
### 抑制 Metals 语义刷新闪烁# (https://jointhefreeworld.org/blog/articles/emacs/emacs-eglot-scala-kotlin/index.html#silencing-metals-semantic-refresh-flickering)
Scala Metals 会激进地强制进行完整的缓冲区语义令牌刷新。在大型项目中,这会导致视觉布局闪烁和不必要的 CPU 压力。禁用此功能还可以解决 Metals 的一些启动问题。
``
(defun my/eglot-disable-metals-semantic-refresh (orig-fn server)
(l
相似文章
Vim中的Lisp(2019)
详细比较了Slimv和Vlime这两个用于交互式Lisp编程的Vim插件,涵盖安装、功能及推荐。
在浏览器中试用 LispE
关于在浏览器中试用 LispE(一种 Lisp 方言)的简要介绍或链接。
还有人用 Emacs 吗?
作者对与 Emacs 数十年关系的个人反思,包括转向 VSCode 和 IntelliJ,最终因其独特功能回归 Emacs。
将 Python 转译为 Lisp
LispE 是 NAVER 推出的一款开源 Lisp 方言,兼具函数式与数组编程特性,并支持 PyTorch、llama.cpp 以及 MLX 等 AI 库。该语言既可作为原生应用运行,也可打包为支持多线程与现代函数式编程特性的 WebAssembly 库。
JavaScript 精简
LispE 是 NAVER 开发的一个紧凑的 Lisp 方言,它结合了函数式和数组语言特性,并支持 PyTorch 和 llama.cpp 等 AI 库。