原文链接:https://robert.kra.hn/posts/2021-02-07_rust-with-emacs/。翻译有错漏欢迎评论区指正吐槽 😂。

过去的两年时间 Emacs 对 Rust 支持有了很大的提升。本文主要配置 Emacs 开发环境,功能如下:
- 源代码导航(跳转到实现、引用列表、模块大纲)
- 代码补全
- 代码片段
- 错误和警告行内高亮
- 代码修复和重构
- 自动导入定义(如特性)
- rustfmt 代码格式化
- 构建和运行其它 cargo 命令
本配置基于rust-analyzer,这是一个处于活跃开发状态并使 VS Code 支持 Rust 的 LSP 服务。
本文可以做为参考或直接去Github 仓库获取源码直接运行(如下)。已测试可行的环境:Emacs 27.1、rust stable 1.49.0、macOS 11.1、Ubuntu 18.4、Win10。
对于想了解 Emacs-racer 的相关配置可以查看David Crook 的指南。
内容目录:
- 快速开始
- 前置需求
- Rust
- rust-analyzer
- Emacs
- Rust Eamcs 详细配置
- rustic
- lsp-mode 和 lsp-ui-mode
- 代码导航跳转
- 代码操作
- 代码补全和片段
- 行内错误
- 行内类型提示
- 附加包
- Debug 调试
- 感谢
快速开始
如果你已经安装了 Rust 和 Emacs 那可以直接快速开始而不用对现有配置做任何修改。可以使用如下命令在启动 Emacs 时加载rksm/emacs-rust-config github 仓库的standalone.el配置文件:
git clone https://github.com/rksm/emacs-rust-config
emacs -q --load ./emacs-rust-config/standalone.el
此命令会在启动 Emacs 时使用检出仓库的目录的.emacs.d路径(以及不同的 elpa 文件夹)。意味着不会使用和修改你原有的$HOME/.emacs.d。如果你不确定或是很清楚这里描述的内容,这种方式都是最简单的配置。
所有的依赖都会在第一次启动时被安装,也就是第一次启动会多花些时间。
Windows 系统可以在快捷方式中添加这些参数启动 Emacs。如果是 macOS 并且安装的是 Emacs.app 则需要使用如下命令行:
/Applications//Emacs.app/Contents/MacOS/Emacs -q --load ./emacs-rust-config/standalone.el
先决条件
开始配置 Emacs 前,请确保你的系统已经安装了下面这些软件:
Rust
安装 Rust 工具链及 cargo,这些使用rustup很容易安装。安装稳定版的 rust 并确保.cargo/bin已经添加到环境变量,rustup 可以默认完成这些操作。rust-analyzer 依赖 Rust 源码,可以运行命令rustup component add rust-src进行安装。
rust-analyzer
需要 rust-analyzer 服务的二进制包。可以参考rust-analyzer 手册进行安装,有预编译好的二进制包。然而,由于 rust-analyzer 开发非常活跃,我通常是下载 github 仓库源码再自行编译。这种方式更便于升级版本(可能也需要降级)。
$ git clone https://github.com/rust-analyzer/rust-analyzer.git
$ cd rust-analyzer
$ cargo xtask install --server # 会安装 rust-analyzer 到 $HOME/.cargo/bin 目录
经常会发生新版不能正常运行的问题。这种情况我建议查看rust-analyzer 改动日志,日志包含链接到每周更新的 git 提交。如果不能正常运行,可以试着构建早一些的版本,或许可以成功。写本文时(2021.11.15)我用的是7366833,这个版本在 稳定版 Rust 1.56.1 以及 Ubuntu、MacOS 和 Windows 系统都工作正常。
Emacs
我测试过可以配置的版本是 Emacs 27.1。Mac 上我通常使用emacsformacosx。Windows 上我使用 “附近的 GNU 镜像”链接为gnu.org/software/emacs。在 Ubuntu 需要添加第三方 apt 仓库。注意此配置在较老的 emacs 版本也可以工作,但 Emacs 27 在 JSON 解析方面有实质性的改进大大提高了 LSP 客户端的速度。
注意,我使用use-package作为 Emacs 的包管理器。它将自动安装这个配置的独立版本。否则可以在你的init.el添加如下片段:
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
Rust Emacs 详细配置
用到的模式有:
- rustic
- lsp-mode
- company
- yasnippet
- flycheck
Rustic
rustic是rust-mode的一个分支并扩展了很多有用的功能(可以查看它的 github readme)。它是配置的核心,如果你只需要代码高亮和 emacs 绑定的 cargo 快捷键,那就这一个就够了不需要其它任何 Emacs 扩展包。
(use-package rustic
:ensure
:bind (:map rustic-mod-map
("M-j" . lsp-ui-imenu)
("M-?" . lsp-find-references)
("C-c C-c l" . flycheck-list-errors)
("C-c C-c a" . lsp-execute-code-action)
("C-c C-c r" . lsp-rename)
("C-c C-c q" . lsp-wordspace-restart)
("C-c C-c Q" . lsp-workspace-shutdown)
("C-c C-c s" . lsp-rust-analyzer-status))
:confi
;; 减少闪动可以取消这里的注释
;; (setq lsp-eldoc-hook nil)
;; (setq lsp-enable-symbol-highlighting nil)
;; (setq lsp-signature-auto-activate nil)
;; 注释下面这行可以禁用保存时 rustfmt 格式化
(setq rustic-format-on-save t)
(add-hook 'rustic-mode-hook 'rk/rustic-mode-hook))
(defun rk/rustic-mode-hook ()
;; 所以运行 C-c C-c C-r 无需确认就可以工作,但不要尝试保存不是文件访问的 rust 缓存。
;; 一旦 https://github.com/brotzeit/rustic/issues/253 问题处理了
;; 就不需要这个配置了
(when buffer-file-name
(setq-local buffer-save-without-query t)))
rustic 的大部分功能都绑定到C-c C-c前缀(也就是按 Control-c 键两次再按其它键):

你可以使用C-c C-c C-r调用cargo run运行程序。有可能需要你指定一些参数例如使用发布模式运行可以指定--release或要运行名称为 “other-bin” 的目标程序使用参数--bin other-bin(替换 mina.rs)。 要给可执行程序本身传递参数使用-- --arg1 --arg2。
快捷键C-c C-c C-c会运行测试。非常方便执行内联测试而不用经常的来切回在终端和 Emacs 之间切换。
C-c C-p命令会打开一个固定位置的弹出缓冲区显示上面的快捷命令。
Rustic 提供了一些和 cargo 很方便的集成,例如,M-x rustic-cargo-add会允许你添加依赖到项目的Cargo.toml(通过cargo-edit这个需要提前安装好)。
如果你想分享代码片段,M-x rstic-playpen命令会把你当前缓冲区在https://play.rust-lang.org打开,可以让你在线运行 Rust 代码并且有一个可以分享的链接。
默认启用了保存时使用 rustfmt 进行代码格式化。要禁用它可以设置(setq rustic-format-on-save nil)。也可以在需要时使用C-c C-c C-o格式化缓冲区。
lsp-mode and lsp-ui-mode
lsp-mode 提供了rust-analyzer的集成。启用了一些 IDE 的功能如源代码导航、通过 flycheck (如下)语法检查错误高亮以及为 company 提供代码自动补全(如下)。
(use-package lsp-mode
:ensure
:commands lsp
:custom
;; 保存时使用什么进行检查,默认是 "check",我更推荐 "clippy"
(lsp-rust-analyzer-cargo-watch-command "clippy")
(lsp-eldoc-render-all t)
(lsp-idle-delay 0.6)
(lsp-rust-analyzer-server-display-inlay-hints t)
:config
(add-hook 'lsp-mode-hook 'lsp-ui-mode))
(use-package lsp-ui
:ensuer
:commands lsp-ui-mode
:custom
(lsp-ui-peek-always-show t)
(lsp-ui-sideline-show-hover t)
(lsp-ui-doc-enable nil))
lsp-ui 是可选的,它提供在光标处标记并显示内联弹层以及光标处的代码修复。如果你发现它闪动不想开启这个功能,只需要移除:config (add-hook 'lsp-mode-hook 'lsp-ui-mode)。
上面的配置也关闭了 lsp-ui 内联显示的文档功能。这个比较符合我的习惯,由于它经常遮住源代码。如果你也想关闭在 mini 缓冲区显示的文档可以添加(setq lsp-eldoc-hook nil)。在光标移动时想操作的更少可以考虑(setq lsp-signature-auto-activate nil)和(setq lsp-enable-symbol-highlighting nil)。
Code Navigation
配置好 lsp-mode 当你的光标在一个标记上面时你就可以使用M-.来跳转到函数、结构体、包等的定义处。M-,可以再跳回来。使用M-?你可以列出标记的所有引用。如下演示:

使用M-j你可以打开允许你在函数和其它定义之间快速跳转的当前模块大纲。

代码操作(Code Actions)
可以使用M-x lsp-rename和lsp-execute-code-action进行重构。代码操作基本上就是代码转换和修复。例如代码检查可能会发现更优雅的代码表达方式:

可用的代码操作的数量还在持续增长。完整的列表可以查看rust-analyzer 文档。收藏的包括自动函数引入或完全的代码合格化,例如,一个模块还没有引入 HashMap,输入HashMap然后选择选项可以引入Import std::collections::HashMap。其他代码操作允许你在匹配表达式中添加所有可能的分支,或者为定义实现转换#[derive(Trait)]为必要的的代码。还有很多很多。
如果你在开发宏,快速查看他们是如何扩展的将非常实用。使用M-x lsp-rust-analyzer-expand-macro或快捷键C-c C-c e来展开宏。
代码补全和片段(Code completion and snippets)
lsp-mode 直接和 Emacs 的补全框架company-mode集成。它会显示一个能被插入到光标处的可选符号列表。在使用不熟悉的库(或 std 库)时非常有用,不再需要经常查看文档。Rust 的类型系统被用作补全的来源,因此你可以插入有意义的内容。
默认代码补全弹框会在company-idle-delay设置的 0.5 秒后显示。你可以修改这个值或者设置company-begin-commands为nil来完全关闭弹层。
(use-package company
:ensure
:custom
(company-idle-delay 0.5) ;; 弹层延迟显示时长
;; (company-begin-commands nil) ;; 取消注释可以禁用弹层
:bind
(:map compnay-active-map
("C-n". company-select-next)
("C-p". company-select-previous)
("M-<". company-select-first)
("M->". company-select-last)))
(use-package yasnippet
:ensure
:config
(yas-reload-all)
(add-hook 'prog-mode-hook 'yas-minor-mode)
(add-hook 'text-mode-hook 'yas-minor-mode)
)
这里也会通过yasnippet启用代码片段。我有一个常用片段 github 仓库列表。可以随意拷贝并修改他们。他们的工作方式是通过输入固定的字符序列然后按 TAB 键。例如for<TAB>会展开为 for 循环。你可以自定义预填的内容和展开的停止数量甚至执行自定义的 elisp 代码。具体查看 yasnippet 文档。
要在点击 TAB 键时启用代码片段展开、代码补全和缩进,我们需要自定义在点击 TAB 时执行的命令:
(use-package company
;; ... 接上面 ...
(:map company-mod-map
("<tab>". tab-indent-or-complete)
("TAB". tab-indent-or-complete)
)
)
(defun company-yasnippet-or-complete ()
(interactive)
(or (do-yas-expand)
(company-complete-common))
)
(defun check-expansion ()
(save-excursion
(if (looking-at "\\_>") t
(backward-char 1)
(if (looking-at "\\.") t
(backward-char 1)
(if (looking-at "::") t nil)
)
)
)
)
(defun do-yas-expand ()
(let ((yas/fallback-behavior 'return-nil))
(yas/expand)
)
)
(defun tab-indent-or-complete ()
(interactive)
(if (minibufferp)
(minibuffer-complete)
(if (or (not yas/minor-mod)
(null (do-yas-expand))
)
(if (check-expansion)
(company-complete-common)
(indent-for-tab-command)
)
)
)
)
大部分常用片段是for、log、ifl、match和fn。
行内错误
这个很简单,rustic 做了很多繁重的任务。我位只需要确认代码检查已经加载:
(use-package flycheck :ensure)
也可以执行M-x flycheck-list-errors或点击快捷键C-c C-c l来显示一个错误和警告的列表。
行内类型提示
Rust-analyzer 和 lsp-mode可以显示行内类型注释。通常当把光标放在定义的变量上时会通过 eldoc 进行显示,使用注释你可始终看到推断的类型。 使用(setq lsp-rust-analyzer-server-display-inlay-hints t)来启用它们。要真正的插入推断的类型到源代码,你可以移动光标到定义的变量并执行M-x lsp-execute-code-action或C-c C-c a。
注意它们可能和lsp-ui-sideline-mode交互的不是很好。如果你只需要提示而想禁用边线模式(sideline mode),你可以给rustic-mode-hook添加(lsp-ui-sideline-enable nil)。
代码调试
Emacs 通过dap-mode集成了 gdb 和 lldb。为了设置支持 Rust 调试,你需要做一些额外的配置和构建步骤。特别是你需要有lldb-mi(https://github.com/lldb-tools/lldb-mi),它不包含在 Apple 通过 XCode 提供的官方 llvm 发行版里。
我只在 macOS 上测试编译了lldb-mi。下面是我的操作步骤:
- 通过 homebrew 安装 llvm 和 cmake
- 检出 lldb-mi 代码库
- 构建 lldb-mi 可执行文件
- 将目录链接到我的 PATH
$ brew install cmake llvm
$ git clone https://github.com/lldb-tools/lldb-mi
$ mkdir -p lldb-mi/build
$ cd lldb-mi/build
$ cmake ..
$ cmake --build .
$ ln -s $PWD/src/lldb-mi /usr/local/bin/lldb-mi
为了让 Emacs 能找到可执行文件,你需要确保exec-path在启动时是正确配置的。完整的 dap-mode 配置如下:
(use-package exec-path-from-shell
:ensure
: init (exec-path-from-shell-initialize)
)
(use-package dap-mode
:ensure
:config
(dap-ui-mode)
(dap-ui-controls-mode 1)
(require 'dap-lldb)
(require 'dap-gdb-lldb)
;; 安装 .extendsion/vscode
(dap-gdb-lldb-setup)
(dap-register-debug-template
"Rust::LLDB Run Configuration"
(list :type "lldb"
:request "launch"
:name "LLDB::Run"
:gdbpath "rust-lldb"
:target nil
:cwd nil
)
)
)
(dp-gdb-lldb-setup)会安装一个 VSCode 扩展到user-emacs-dir/.extension/vscode/webfreak.debug目录。我碰到有一个问题是这个安装不是经常会成功。如果最后你没有 “webfreak.debug” 目录你可能需要删除vscode/目录然后再执行(dap-gdb-lldb-setup)。
我还需要执行一次sudo DevToolSecurity --enable来允许调试器访问进程。
另外还有一个问题是,当我启动调试目标时我会看到:
Could not start debugger process, does the program exist in filesystem?
Error: spawn lldb-mi ENOENT
即使lldb-mi在我的环境变量并且我可以在 Emacs 里面启动它。结果表明错误不是来自lldb-mi而是你启动目标的目录。当你使用M-x dap-debug或通过dap-hydra d d启动调试,然后选择Rust::LLDB Run Configuration时确保你想要调试的可执行目标的目录不是相对路径也不能包含~。如果是绝对路径就应该可以工作。
如下可能会发生上面错误的失败(注意未展开的~/):

我需要指定完整的路径/Users/robert/projects/rust/emacs/test-project/target/debug/test-project。
一旦成功执行看起来应该如下:
