Emacs 編輯器配置 Rust 開發(fā)環(huán)境

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

demo.png

過去的兩年時間 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

rusticrust-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 鍵兩次再按其它鍵):

rustic-shortcuts-1.png

你可以使用 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)的當前模塊大綱。

imenu.png

代碼操作(Code Actions)

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

rust-lsp-demo-2.gif

可用的代碼操作的數(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-commandsnil 來完全關(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、matchfn 。

行內(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-actionC-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。下面是我的操作步驟:

  1. 通過 homebrew 安裝 llvm 和 cmake
  2. 檢出 lldb-mi 代碼庫
  3. 構(gòu)建 lldb-mi 可執(zhí)行文件
  4. 將目錄鏈接到我的 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ā)生上面錯誤的失敗(注意未展開的 ~/):

dap-fail.png

我需要指定完整的路徑 /Users/robert/projects/rust/emacs/test-project/target/debug/test-project。

一旦成功執(zhí)行看起來應該如下:

B站視頻地址傳送

上面示例我首先使用 C-c C-c d 激活 dab-hydra。然后使用 d d 選擇 Rust 調(diào)試目標(提前使用 cargo 構(gòu)建的)。在這之前還用 d p 設(shè)置了一個斷點。然后我使用 ni 在代碼中步進。注意你也可以使用鼠標設(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 的維護人員以及我記不太清的參與其中的所有人!


  1. Racer 曾經(jīng)是配置 Emacs IDE特性(代碼導航等)的最佳選擇。它是比 RLS 和 rust-analyzer 都快的非 LSP 解決方案。然而有很多有關(guān)代碼補全的特性已經(jīng)不如 rust-analyzer 了。
  2. Emacs 也通過 GUD 內(nèi)置了對 gdb 的支持, 但需要直接控制 gdb 進程。DAP 更類似于 LSP,因為它用于遠程控制調(diào)試過程,使編輯器更容易集成它。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容