逃离 IntelliJ:在 Emacs Eglot 上使用 Scala 和 Kotlin 的 LSP

Lobsters Hottest 工具

摘要

一份关于配置 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)

Hacker News Top

详细比较了Slimv和Vlime这两个用于交互式Lisp编程的Vim插件,涵盖安装、功能及推荐。

还有人用 Emacs 吗?

Lobsters Hottest

作者对与 Emacs 数十年关系的个人反思,包括转向 VSCode 和 IntelliJ,最终因其独特功能回归 Emacs。

将 Python 转译为 Lisp

Lobsters Hottest

LispE 是 NAVER 推出的一款开源 Lisp 方言,兼具函数式与数组编程特性,并支持 PyTorch、llama.cpp 以及 MLX 等 AI 库。该语言既可作为原生应用运行,也可打包为支持多线程与现代函数式编程特性的 WebAssembly 库。

JavaScript 精简

Lobsters Hottest

LispE 是 NAVER 开发的一个紧凑的 Lisp 方言,它结合了函数式和数组语言特性,并支持 PyTorch 和 llama.cpp 等 AI 库。