原文鏈接:https://robert.kra.hn/posts/2021-02-07_rust-with-emacs/。翻譯有錯漏歡迎評論區(qū)指正吐槽??。

過去的兩年時間 Emacs 對 Rust 支持有了很大的提升。本文主要配置 Emacs 開發(fā)環(huán)境,功能如下:
- 源代碼導航(跳轉(zhuǎn)到實現(xiàn)、引用列表、模塊大綱)
- 代碼補全
- 代碼片段
- 錯誤和警告行內(nèi)高亮
- 代碼修復和重構(gòu)
- 自動導入定義(如特性)
- rustfmt 代碼格式化
- 構(gòu)建和運行其它 cargo 命令
本配置基于 rust-analyzer,這是一個處于活躍開發(fā)狀態(tài)并使 VS Code 支持 Rust 的 LSP 服務。
本文可以做為參考或直接去 Github 倉庫 獲取源碼直接運行(如下)。已測試可行的環(huán)境:Emacs 27.1、rust stable 1.49.0、macOS 11.1、Ubuntu 18.4、Win10。
對于想了解 Emacs-racer 的相關(guān)配置可以查看 David Crook 的指南。
內(nèi)容目錄:
- 快速開始
- 前置需求
- Rust
- rust-analyzer
- Emacs
- Rust Eamcs 詳細配置
- rustic
- lsp-mode 和 lsp-ui-mode
- 代碼導航跳轉(zhuǎn)
- 代碼操作
- 代碼補全和片段
- 行內(nèi)錯誤
- 行內(nèi)類型提示
- 附加包
- Debug 調(diào)試
- 感謝
快速開始
如果你已經(jīng)安裝了 Rust 和 Emacs 那可以直接快速開始而不用對現(xiàn)有配置做任何修改??梢允褂萌缦旅钤趩?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。如果你不確定或是很清楚這里描述的內(nèi)容,這種方式都是最簡單的配置。
所有的依賴都會在第一次啟動時被安裝,也就是第一次啟動會多花些時間。
Windows 系統(tǒng)可以在快捷方式中添加這些參數(shù)啟動 Emacs。如果是 macOS 并且安裝的是 Emacs.app 則需要使用如下命令行:
/Applications//Emacs.app/Contents/MacOS/Emacs -q --load ./emacs-rust-config/standalone.el
先決條件
開始配置 Emacs 前,請確保你的系統(tǒng)已經(jīng)安裝了下面這些軟件:
Rust
安裝 Rust 工具鏈及 cargo,這些使用 rustup 很容易安裝。安裝穩(wěn)定版的 rust 并確保 .cargo/bin 已經(jīng)添加到環(huán)境變量,rustup 可以默認完成這些操作。rust-analyzer 依賴 Rust 源碼,可以運行命令 rustup component add rust-src 進行安裝。
rust-analyzer
需要 rust-analyzer 服務的二進制包??梢詤⒖?rust-analyzer 手冊進行安裝,有預編譯好的二進制包。然而,由于 rust-analyzer 開發(fā)非?;钴S,我通常是下載 github 倉庫源碼再自行編譯。這種方式更便于升級版本(可能也需要降級)。
$ git clone https://github.com/rust-analyzer/rust-analyzer.git
$ cd rust-analyzer
$ cargo xtask install --server # 會安裝 rust-analyzer 到 $HOME/.cargo/bin 目錄
經(jīng)常會發(fā)生新版不能正常運行的問題。這種情況我建議查看 rust-analyzer 改動日志,日志包含鏈接到每周更新的 git 提交。如果不能正常運行,可以試著構(gòu)建早一些的版本,或許可以成功。寫本文時(2021.11.15)我用的是7366833,這個版本在 穩(wěn)定版Rust 1.56.1 以及 Ubuntu、MacOS和Windows系統(tǒng)都工作正常。
Emacs
我測試過可以配置的版本是 Emacs 27.1。Mac上我通常使用 emacsformacosx。Windows 上我使用 “附近的 GNU 鏡像”鏈接為 gnu.org/software/emacs。在Ubuntu需要添加第三方 apt 倉庫。注意此配置在較老的emacs 版本也可以工作,但 Emacs 27 在 JSON 解析方面有實質(zhì)性的改進大大提高了 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 調(diào)用 cargo run 運行程序。有可能需要你指定一些參數(shù)例如使用發(fā)布模式運行可以指定 --release 或要運行名稱為 "other-bin" 的目標程序使用參數(shù) --bin other-bin(替換 mina.rs)。 要給可執(zhí)行程序本身傳遞參數(shù)使用 -- --arg1 --arg2。
快捷鍵 C-c C-c C-c 會運行測試。非常方便執(zhí)行內(nèi)聯(lián)測試而不用經(jīng)常的來切回在終端和 Emacs 之間切換。
C-c C-p 命令會打開一個固定位置的彈出緩沖區(qū)顯示上面的快捷命令。
Rustic 提供了一些和 cargo 很方便的集成,例如,M-x rustic-cargo-add 會允許你添加依賴到項目的 Cargo.toml (通過 cargo-edit 這個需要提前安裝好)。
如果你想分享代碼片段,M-x rstic-playpen 命令會把你當前緩沖區(qū)在 https://play.rust-lang.org 打開,可以讓你在線運行 Rust 代碼并且有一個可以分享的鏈接。
默認啟用了保存時使用 rustfmt 進行代碼格式化。要禁用它可以設(shè)置 (setq rustic-format-on-save nil)。也可以在需要時使用 C-c C-c C-o 格式化緩沖區(qū)。
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 是可選的,它提供在光標處標記并顯示內(nèi)聯(lián)彈層以及光標處的代碼修復。如果你發(fā)現(xiàn)它閃動不想開啟這個功能,只需要移除 :config (add-hook 'lsp-mode-hook 'lsp-ui-mode)。
上面的配置也關(guān)閉了 lsp-ui 內(nèi)聯(lián)顯示的文檔功能。這個比較符合我的習慣,由于它經(jīng)常遮住源代碼。如果你也想關(guān)閉在 mini 緩沖區(qū)顯示的文檔可以添加 (setq lsp-eldoc-hook nil)。在光標移動時想操作的更少可以考慮 (setq lsp-signature-auto-activate nil) 和 (setq lsp-enable-symbol-highlighting nil)。
代碼導航(Code Navigation)
配置好 lsp-mode 當你的光標在一個標記上面時你就可以使用 M-. 來跳轉(zhuǎn)到函數(shù)、結(jié)構(gòu)體、包等的定義處。M-, 可以再跳回來。使用 M-? 你可以列出標記的所有引用。如下演示:
[站外圖片上傳中...(image-c81695-1638114865455)]
使用 M-j 你可以打開允許你在函數(shù)和其它定義之間快速跳轉(zhuǎn)的當前模塊大綱。

代碼操作(Code Actions)
可以使用 M-x lsp-rename 和 lsp-execute-code-action 進行重構(gòu)。代碼操作基本上就是代碼轉(zhuǎn)換和修復。例如代碼檢查可能會發(fā)現(xiàn)更優(yōu)雅的代碼表達方式:

可用的代碼操作的數(shù)量還在持續(xù)增長。完整的列表可以查看 rust-analyzer 文檔。收藏的包括自動函數(shù)引入或完全的代碼合格化,例如,一個模塊還沒有引入 HashMap,輸入 HashMap 然后選擇選項可以引入 Import std::collections::HashMap。其他代碼操作允許你在匹配表達式中添加所有可能的分支,或者為定義實現(xiàn)轉(zhuǎn)換 #[derive(Trait)] 為必要的的代碼。還有很多很多。
如果你在開發(fā)宏,快速查看他們是如何擴展的將非常實用。使用 M-x lsp-rust-analyzer-expand-macro 或快捷鍵 C-c C-c e 來展開宏。
代碼補全和片段(Code completion and snippets)
lsp-mode 直接和 Emacs 的補全框架 company-mode 集成。它會顯示一個能被插入到光標處的可選符號列表。在使用不熟悉的庫(或 std 庫)時非常有用,不再需要經(jīng)常查看文檔。Rust 的類型系統(tǒng)被用作補全的來源,因此你可以插入有意義的內(nèi)容。
默認代碼補全彈框會在 company-idle-delay 設(shè)置的 0.5 秒后顯示。你可以修改這個值或者設(shè)置 company-begin-commands 為 nil 來完全關(guān)閉彈層。
(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 倉庫 列表??梢噪S意拷貝并修改他們。他們的工作方式是通過輸入固定的字符序列然后按 TAB 鍵。例如 for<TAB> 會展開為 for 循環(huán)。你可以自定義預填的內(nèi)容和展開的停止數(shù)量甚至執(zhí)行自定義的 elisp 代碼。具體查看 yasnippet 文檔。
要在點擊 TAB 鍵時啟用代碼片段展開、代碼補全和縮進,我們需要自定義在點擊 TAB 時執(zhí)行的命令:
(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 。
行內(nèi)錯誤
這個很簡單,rustic 做了很多繁重的任務。我位只需要確認代碼檢查已經(jīng)加載:
(use-package flycheck :ensure)
也可以執(zhí)行 M-x flycheck-list-errors 或點擊快捷鍵 C-c C-c l 來顯示一個錯誤和警告的列表。
行內(nèi)類型提示
Rust-analyzer 和 lsp-mode 可以顯示行內(nèi)類型注釋。通常當把光標放在定義的變量上時會通過 eldoc 進行顯示,使用注釋你可始終看到推斷的類型。 使用 (setq lsp-rust-analyzer-server-display-inlay-hints t) 來啟用它們。要真正的插入推斷的類型到源代碼,你可以移動光標到定義的變量并執(zhí)行 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)。
代碼調(diào)試
Emacs 通過 dap-mode 集成了 gdb 和 lldb。為了設(shè)置支持 Rust 調(diào)試,你需要做一些額外的配置和構(gòu)建步驟。特別是你需要有 lldb-mi(https://github.com/lldb-tools/lldb-mi),它不包含在 Apple 通過 XCode 提供的官方 llvm 發(fā)行版里。
我只在 macOS 上測試編譯了 lldb-mi。下面是我的操作步驟:
- 通過 homebrew 安裝 llvm 和 cmake
- 檢出 lldb-mi 代碼庫
- 構(gòu)建 lldb-mi 可執(zhí)行文件
- 將目錄鏈接到我的 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 能找到可執(zhí)行文件,你需要確保 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 目錄。我碰到有一個問題是這個安裝不是經(jīng)常會成功。如果最后你沒有 "webfreak.debug" 目錄你可能需要刪除 vscode/ 目錄然后再執(zhí)行 (dap-gdb-lldb-setup)。
我還需要執(zhí)行一次 sudo DevToolSecurity --enable 來允許調(diào)試器訪問進程。
另外還有一個問題是,當我啟動調(diào)試目標時我會看到:
Could not start debugger process, does the program exist in filesystem?
Error: spawn lldb-mi ENOENT
即使 lldb-mi 在我的環(huán)境變量并且我可以在 Emacs 里面啟動它。結(jié)果表明錯誤不是來自 lldb-mi 而是你啟動目標的目錄。當你使用 M-x dap-debug 或通過 dap-hydra d d 啟動調(diào)試,然后選擇 Rust::LLDB Run Configuration 時確保你想要調(diào)試的可執(zhí)行目標的目錄不是相對路徑也不能包含 ~。如果是絕對路徑就應該可以工作。
如下可能會發(fā)生上面錯誤的失敗(注意未展開的 ~/):

我需要指定完整的路徑 /Users/robert/projects/rust/emacs/test-project/target/debug/test-project。
一旦成功執(zhí)行看起來應該如下:
上面示例我首先使用 C-c C-c d 激活 dab-hydra。然后使用 d d 選擇 Rust 調(diào)試目標(提前使用 cargo 構(gòu)建的)。在這之前還用 d p 設(shè)置了一個斷點。然后我使用 n 和 i 在代碼中步進。注意你也可以使用鼠標設(shè)置斷點和步進。
配置調(diào)試并沒有預期的順暢,但一旦運行起來會非常有趣!
Rust playground
你或許已經(jīng)見識了在線的 Rust playgroud https://play.rust-lang.org/,可以讓快速運行和分享 Rust 代碼片段。Emacs 有一個類似的允許你快速創(chuàng)建(或移除)Rust草稿項目的項目是 [grafov/rust-playgroud](https://github.com/grafov/rust-playground)。默認 rust-playgroud 命令會在目錄 ~/.emacs.d/rust-playgroud/ 創(chuàng)建 Rust 項目,并打開 main.rs,使用綁定的快捷鍵快速運行項目(C-c C-c)。這個非常便于你快速測試 Rust 代碼片段或調(diào)試一個庫。這一切都來自于你自己的編輯器!
附加包
這還有一些 emacs 包本文就不再細說了,會極大的提升使用 Emacs 進行 Rust 或其它語言開發(fā)的體驗。如下:
- projectile:將項目的概念引入到 emacs 以及大量相關(guān)操作的命令。如在項目打開 shell、搜索項目代碼等。
- helm、selctrum、ivy:我們花了很多時間從列表中選擇一個還是多個選項。讓它可以打開文件、緩沖區(qū)間切換或執(zhí)行命令(M-x)。所有這些包讓在 emacs 中通過鍵盤輸入來選擇選項變得簡單,并能夠過濾大的列表。help 是我個人的日常驅(qū)動,但 selectrum 是一個更輕量的替代。它使用在相關(guān)的 github 項目的 standalone.el 版本中。
- shackle:Emacs 默認的窗口規(guī)則并不是最優(yōu)的。Shakle 允許定義匹配緩沖區(qū)名稱的規(guī)則。我默認的規(guī)則在這個 gist。
- dired:內(nèi)置于 Emacs。你最后需要一個文件管理器。
感謝這些包的開發(fā)者們!
最后要說聲謝謝!感謝所有本文中提到的開源軟件的開發(fā)和維護者們。Rust-analyzer 項目是令人驚嘆的,它極大的改善了 Rust Emacs 工具狀態(tài)。當然也離不開非常有用的 lsp-mode 和 lsp-ui。rustic 簡化了 rust-mode 模式相關(guān)的必要配置,并增加了非常有用的特性。在其它語言 company 和 flycheck 是我的默認配置。當然還要感謝所有 Emacs 的維護人員以及我記不太清的參與其中的所有人!