原文链接: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-?
你可以列出标记的所有引用。如下演示:
[站外图片上传中...(image-c81695-1638114865455)]
使用 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
。
一旦成功执行看起来应该如下:
上面示例我首先使用 C-c C-c d
激活 dab-hydra
。然后使用 d d
选择 Rust 调试目标(提前使用 cargo 构建的)。在这之前还用 d p
设置了一个断点。然后我使用 n
和 i
在代码中步进。注意你也可以使用鼠标设置断点和步进。
配置调试并没有预期的顺畅,但一旦运行起来会非常有趣!
Rust playground
你或许已经见识了在线的 Rust playgroud https://play.rust-lang.org/,可以让快速运行和分享 Rust 代码片段。Emacs 有一个类似的允许你快速创建(或移除)Rust草稿项目的项目是 [grafov/rust-playgroud](https://github.com/grafov/rust-playground)
。默认 rust-playgroud
命令会在目录 ~/.emacs.d/rust-playgroud/
创建 Rust 项目,并打开 main.rs
,使用绑定的快捷键快速运行项目(C-c C-c
)。这个非常便于你快速测试 Rust 代码片段或调试一个库。这一切都来自于你自己的编辑器!
附加包
这还有一些 emacs 包本文就不再细说了,会极大的提升使用 Emacs 进行 Rust 或其它语言开发的体验。如下:
- projectile:将项目的概念引入到 emacs 以及大量相关操作的命令。如在项目打开 shell、搜索项目代码等。
- helm、selctrum、ivy:我们花了很多时间从列表中选择一个还是多个选项。让它可以打开文件、缓冲区间切换或执行命令(M-x)。所有这些包让在 emacs 中通过键盘输入来选择选项变得简单,并能够过滤大的列表。help 是我个人的日常驱动,但 selectrum 是一个更轻量的替代。它使用在相关的 github 项目的 standalone.el 版本中。
- shackle:Emacs 默认的窗口规则并不是最优的。Shakle 允许定义匹配缓冲区名称的规则。我默认的规则在这个 gist。
- dired:内置于 Emacs。你最后需要一个文件管理器。
感谢这些包的开发者们!
最后要说声谢谢!感谢所有本文中提到的开源软件的开发和维护者们。Rust-analyzer 项目是令人惊叹的,它极大的改善了 Rust Emacs 工具状态。当然也离不开非常有用的 lsp-mode 和 lsp-ui。rustic 简化了 rust-mode 模式相关的必要配置,并增加了非常有用的特性。在其它语言 company 和 flycheck 是我的默认配置。当然还要感谢所有 Emacs 的维护人员以及我记不太清的参与其中的所有人!