├── CHANGELOG.md ├── LICENSE ├── README.md ├── cargo-mode.el └── demo ├── demo1.gif └── demo2.gif /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.9 (2025-05-29) 4 | 5 | - fix: issue with rerun from compilation buffer ([#24](https://github.com/ayrat555/cargo-mode/pull/24)) 6 | 7 | ## 0.0.8 (2025-01-06) 8 | 9 | - feat: add Clippy command ([#22](https://github.com/ayrat555/cargo-mode/pull/22)) 10 | 11 | ## 0.0.7 (2024-11-21) 12 | 13 | - feat: use custom compilation mode with keybindings ([#19](https://github.com/ayrat555/cargo-mode/pull/19)) 14 | - fix: use beginning-of-defun-raw for current test ([#17](https://github.com/ayrat555/cargo-mode/pull/17)) 15 | 16 | ## 0.0.6 (2024-01-28) 17 | 18 | - feat: find right cargo for remote buffer ([#15](https://github.com/ayrat555/cargo-mode/pull/15)) 19 | 20 | ## 0.0.5 (2024-01-16) 21 | 22 | - Find binary at execution time rather than initialization ([#12](https://github.com/ayrat555/cargo-mode/pull/12)) 23 | 24 | ## 0.0.4 (2024-01-16) 25 | 26 | - Change the default keybindings for cargo-mode ([#8](https://github.com/ayrat555/cargo-mode/pull/8)) 27 | 28 | ## 0.0.3 (2024-01-15) 29 | 30 | - Add a parameter to run commands without comint mode ([#9](https://github.com/ayrat555/cargo-mode/pull/9)) 31 | 32 | ## 0.0.2 (2023-11-06) 33 | 34 | ### Enhancements 35 | - Listen to prompts from commands ([#5](https://github.com/ayrat555/cargo-mode/pull/5)) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ayrat Badykov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-mode.el 2 | 3 | Emacs minor mode which allows to dynamically select a Cargo command. 4 | 5 | Cargo is the Rust package manager. 6 | 7 | ## Demo 8 | 9 | ### Simple example (cargo clippy) 10 | 11 | ![Clippy](demo/demo1.gif) 12 | 13 | ### Example with custom params (cargo doc --open) 14 | 15 | ![Doc](demo/demo2.gif) 16 | 17 | ## Installation 18 | 19 | ### MELPA 20 | 21 | Set up the MELPA (or MELPA Stable) if you haven't already, and install with `M-x package-install RET cargo-mode RET`. 22 | 23 | The relevant form for `use-package` users is: 24 | 25 | ```el 26 | (use-package cargo-mode 27 | :hook 28 | (rust-mode . cargo-minor-mode) 29 | :config 30 | (setq compilation-scroll-output t)) 31 | ``` 32 | 33 | ### From file 34 | 35 | Add `cargo-mode.el` to your load path: 36 | 37 | ```el 38 | (add-to-list 'load-path "path/to/cargo-mode.el") 39 | ``` 40 | 41 | ## Setup 42 | 43 | Add a hook to the mode that you're using with Rust, for example, `rust-mode`: 44 | 45 | ```el 46 | (add-hook 'rust-mode-hook 'cargo-minor-mode) 47 | ``` 48 | 49 | Set `compilation-scroll-output` to non-nil to scroll the *cargo-compile-mode* buffer window as output appears. The value ‘first-error’ stops scrolling at the first error, and leaves point on its location in the *cargo-mode* buffer. For example: 50 | 51 | ```el 52 | (setq compilation-scroll-output t) 53 | ``` 54 | 55 | By default `cargo-mode` use `comint` mode for compilation buffer. Set `cargo-mode-use-comint` to nil to disable it. 56 | 57 | ```el 58 | (use-package cargo-mode 59 | :custom 60 | (cargo-mode-use-comint nil)) 61 | ``` 62 | 63 | ## Usage 64 | 65 | C-c a e - `cargo-execute-task` - List all available tasks and execute one of them. As a bonus, you'll get a documentation string because `cargo-mode.el` parses shell output of `cargo --list` directly. 66 | 67 | C-c a t - `cargo-mode-test` - Run all tests in the project (`cargo test`). 68 | 69 | C-c a l - `cargo-mode-last-command` - Execute the last executed command. 70 | 71 | C-c a b - `cargo-mode-build` - Build the project (`cargo build`). 72 | 73 | 74 | C-c a o - `cargo-mode-test-current-buffer` - Run all tests in the current buffer. 75 | 76 | C-c a f - `cargo-mode-test-current-test` - Run the current test where pointer is located. 77 | 78 | 79 | These are all commands that I use most frequently. You can execute any cargo command (fmt, clean etc) available in the project using `cargo-mode-execute-task`. If you have suggestions for additional commands to add keybindings to, please create an issue. 80 | 81 | To change the prefix (default C-c a) use: 82 | 83 | ```el 84 | (keymap-set cargo-minor-mode-map (kbd ...) 'cargo-mode-command-map) 85 | ``` 86 | 87 | ## Modify a command before execution 88 | 89 | Use prefix argument (default `C-u`) to add extra command line params before executing a command. 90 | 91 | ## Contributing 92 | 93 | 1. [Fork it!](https://github.com/ayrat555/cargo-mode/fork) 94 | 2. Create your feature branch (`git checkout -b my-new-feature`) 95 | 3. Commit your changes (`git commit -am 'Add some feature'`) 96 | 4. Push to the branch (`git push origin my-new-feature`) 97 | 5. Create new Pull Request 98 | 99 | ## Author 100 | 101 | Ayrat Badykov (@ayrat555) 102 | -------------------------------------------------------------------------------- /cargo-mode.el: -------------------------------------------------------------------------------- 1 | ;;; cargo-mode.el --- Cargo Major Mode. Cargo is the Rust package manager -*- lexical-binding: t; -*- 2 | 3 | ;; MIT License 4 | ;; 5 | ;; Copyright (c) 2021 Ayrat Badykov 6 | ;; 7 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 8 | ;; of this software and associated documentation files (the "Software"), to deal 9 | ;; in the Software without restriction, including without limitation the rights 10 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | ;; copies of the Software, and to permit persons to whom the Software is 12 | ;; furnished to do so, subject to the following conditions: 13 | ;; 14 | ;; The above copyright notice and this permission notice shall be included in all 15 | ;; copies or substantial portions of the Software. 16 | ;; 17 | ;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | ;; SOFTWARE. 24 | 25 | ;; Author: Ayrat Badykov 26 | ;; URL: https://github.com/ayrat555/cargo-mode 27 | ;; Version : 0.0.9 28 | ;; Keywords: tools 29 | ;; Package-Requires: ((emacs "25.1")) 30 | 31 | ;;; Commentary: 32 | 33 | ;; Add a hook to the mode that you're using with Rust, for example, `rust-mode`: 34 | ;; 35 | ;; (add-hook 'rust-mode-hook 'cargo-minor-mode) 36 | ;; 37 | 38 | ;;; C-c a e - `cargo-execute-task` - List all available tasks and execute one of them. As a bonus, you'll get a documentation string because `cargo-mode.el` parses shell output of `cargo --list` directly. 39 | ;;; C-c a t - `cargo-mode-test` - Run all tests in the project (`cargo test`). 40 | ;;; C-c a l - `cargo-mode-last-command` - Execute the last executed command. 41 | ;;; C-c a b - `cargo-mode-build` - Build the project (`cargo build`). 42 | ;;; C-c a o - `cargo-mode-test-current-buffer` - Run all tests in the current buffer. 43 | ;;; C-c a f - `cargo-mode-test-current-test` - Run the current test where pointer is located. 44 | ;;; C-c a c - `cargo-mode-clippy` - Run Clippy in the project (`cargo clippy`). 45 | ;;; 46 | ;;; Use `C-u` to add extra command line params before executing a command. 47 | 48 | ;;; Code: 49 | 50 | (require 'subr-x) 51 | 52 | (defcustom cargo-path-to-bin 53 | nil 54 | "Path to the cargo executable." 55 | :type 'file 56 | :group 'cargo-mode) 57 | 58 | (defcustom cargo-mode-use-comint t 59 | "If t `compile' runs with comint option paramater." 60 | :type 'boolean 61 | :group 'cargo-mode) 62 | 63 | (defcustom cargo-mode-command-test "test" 64 | "Subcommand used by `cargo-mode-test'." 65 | :type 'string 66 | :group 'cargo-mode) 67 | 68 | (defcustom cargo-mode-command-build "build" 69 | "Subcommand used by `cargo-mode-build'." 70 | :type 'string 71 | :group 'cargo-mode) 72 | 73 | (defcustom cargo-mode-command-clippy "clippy" 74 | "Subcommand used by `cargo-mode-clippy'." 75 | :type 'string 76 | :group 'cargo-mode) 77 | 78 | (defconst cargo-mode-test-mod-regexp "^[[:space:]]*mod[[:space:]]+[[:word:][:multibyte:]_][[:word:][:multibyte:]_[:digit:]]*[[:space:]]*{") 79 | 80 | (defconst cargo-mode-test-regexp "^[[:space:]]*fn[[:space:]]*" 81 | "Regex to find Rust unit test function.") 82 | 83 | (defvar cargo-mode--last-command nil "Last cargo command.") 84 | 85 | (defun cargo-mode--find-bin () 86 | "Find the full path to cargo, referencing cargo-path-to-bin first" 87 | (or cargo-path-to-bin (or (if (>= emacs-major-version 27) 88 | (executable-find "cargo" t) (executable-find "cargo")) 89 | "~/.cargo/bin/cargo"))) 90 | 91 | (define-derived-mode cargo-compilation-mode compilation-mode "Cargo" 92 | "Major mode for the Cargo buffer." 93 | (message "using custom mode") 94 | (setq buffer-read-only t) 95 | (setq-local truncate-lines t) 96 | (if cargo-mode-use-comint 97 | (compilation-shell-minor-mode)) 98 | (local-set-key (kbd "q") 'kill-buffer-and-window) 99 | (local-set-key (kbd "g") 'cargo-mode-last-command)) 100 | 101 | (defun cargo-mode--fetch-cargo-tasks (project-root) 102 | "Fetch list of raw commands from shell for project in PROJECT-ROOT." 103 | (let* ((default-directory (or project-root default-directory)) 104 | (cmd (concat (shell-quote-argument (cargo-mode--find-bin)) " --list")) 105 | (tasks-string (shell-command-to-string cmd)) 106 | (tasks (butlast (cdr (split-string tasks-string "\n"))))) 107 | (delete-dups tasks))) 108 | 109 | (defun cargo-mode--available-tasks (project-root) 110 | "List all available tasks in PROJECT-ROOT." 111 | (let* ((raw_tasks (cargo-mode--fetch-cargo-tasks project-root)) 112 | (commands (mapcar #'cargo-mode--split-command raw_tasks))) 113 | (cargo-mode--format-commands commands))) 114 | 115 | (defun cargo-mode--format-commands (commands) 116 | "Format and concat all COMMANDS." 117 | (let ((max-length (cargo-mode--max-command-length (car commands) (cdr commands)))) 118 | (mapcar 119 | (lambda (command) (cargo-mode--concat-command-and-doc command max-length)) 120 | commands))) 121 | 122 | (defun cargo-mode--concat-command-and-doc (command-with-doc max-command-length) 123 | "Concat the COMMAND-WITH-DOC with calcutated. 124 | Space between them is based on MAX-COMMAND-LENGTH." 125 | (let* ((command (car command-with-doc)) 126 | (doc (cdr command-with-doc)) 127 | (command-length (length command)) 128 | (whitespaces-number (- (+ max-command-length 1) command-length)) 129 | (whitespaces-string (make-string whitespaces-number ?\s))) 130 | (concat command whitespaces-string "# " doc))) 131 | 132 | (defun cargo-mode--split-command (raw-command) 133 | "Split command and doc string in RAW-COMMAND." 134 | (let* ((command-words (split-string raw-command)) 135 | (command (car command-words)) 136 | (doc-words (cdr command-words)) 137 | (doc (concat (mapconcat #'identity doc-words " ")))) 138 | (cons command doc))) 139 | 140 | (defun cargo-mode--max-command-length (first-arg more-args) 141 | "Recursively find the longest command. 142 | The current element is FIRST-ARG, remaining args are MORE-ARGS." 143 | (if more-args 144 | (let ((max-rest (cargo-mode--max-command-length (car more-args) (cdr more-args))) 145 | (first-arg-length (length (car first-arg)))) 146 | (if (> first-arg-length max-rest) 147 | first-arg-length 148 | max-rest)) 149 | (length (car first-arg)))) 150 | 151 | (defun cargo-mode--start (name command project-root &optional prompt) 152 | "Start the `cargo-mode` process with NAME and return the created process. 153 | Cargo command is COMMAND. 154 | The command is started from directory PROJECT-ROOT. 155 | If PROMPT is non-nil, modifies the command." 156 | (let* ((path-to-bin (shell-quote-argument (cargo-mode--find-bin))) 157 | (base-cmd (if (string-match-p path-to-bin command) 158 | command 159 | (concat path-to-bin " " command))) 160 | (cmd (cargo-mode--maybe-add-additional-params base-cmd prompt)) 161 | (default-directory (or project-root default-directory))) 162 | (cargo-mode--start-cmd name cmd project-root))) 163 | 164 | (defun cargo-mode--start-cmd (name cmd project-root) 165 | "Start the `cargo-mode` process with NAME and return the created process." 166 | (let* ((buffer (concat "*cargo-mode " name "*"))) 167 | (save-some-buffers (not compilation-ask-about-save) 168 | (lambda () 169 | (and project-root 170 | buffer-file-name 171 | (string-prefix-p project-root (file-truename buffer-file-name))))) 172 | (setq cargo-mode--last-command (list name cmd project-root)) 173 | (compilation-start cmd 'cargo-compilation-mode) 174 | (get-buffer-process buffer))) 175 | 176 | (defun cargo-mode--project-directory () 177 | "Find the project directory." 178 | (let ((closest-path (or buffer-file-name default-directory))) 179 | (locate-dominating-file closest-path "Cargo.toml"))) 180 | 181 | (defun cargo-mode--current-mod () 182 | "Return the current mod." 183 | (save-excursion 184 | (when (search-backward-regexp cargo-mode-test-mod-regexp nil t) 185 | (let* ((line (buffer-substring-no-properties (line-beginning-position) 186 | (line-end-position))) 187 | (line (string-trim-left line)) 188 | (lines (split-string line " \\|{")) 189 | (mod (cadr lines))) 190 | mod)))) 191 | 192 | (defun cargo-mode--defun-at-point-p () 193 | "Find fn at point." 194 | (string-match cargo-mode-test-regexp 195 | (buffer-substring-no-properties (line-beginning-position) 196 | (line-end-position)))) 197 | 198 | (defun cargo-mode--current-test () 199 | "Return the current test." 200 | (save-excursion 201 | (unless (cargo-mode--defun-at-point-p) 202 | (if beginning-of-defun-function 203 | (beginning-of-defun-raw) 204 | (user-error "%s needs a supported major mode like rust-mode or rustic-mode" 205 | this-command))) 206 | (beginning-of-line) 207 | (search-forward "fn ") 208 | (let* ((line (buffer-substring-no-properties (point) 209 | (line-end-position))) 210 | (lines (split-string line "(")) 211 | (function-name (car lines))) 212 | function-name))) 213 | 214 | (defun cargo-mode--current-test-fullname () 215 | "Return the current test's fullname." 216 | (let ((mod-name (cargo-mode--current-mod))) 217 | (if mod-name 218 | (concat mod-name 219 | "::" 220 | (cargo-mode--current-test)) 221 | (cargo-mode--current-test)))) 222 | 223 | (defun cargo-mode--maybe-add-additional-params (command prefix) 224 | "Prompt for additional cargo command COMMAND params. 225 | If PREFIX is nil, it does nothing" 226 | (if prefix 227 | (let ((params (read-string (concat "additional cargo command params for `" command "`: ")))) 228 | (concat command " " params)) 229 | command)) 230 | 231 | ;;;###autoload 232 | (defun cargo-mode-execute-task (&optional prefix) 233 | "Select and execute cargo task. 234 | If PREFIX is non-nil, prompt for additional params." 235 | (interactive "P") 236 | (let* ((project-root (cargo-mode--project-directory)) 237 | (available-commands (cargo-mode--available-tasks project-root)) 238 | (selected-command (completing-read "select cargo command: " available-commands)) 239 | (command-without-doc (car (split-string selected-command)))) 240 | (cargo-mode--start "execute" command-without-doc project-root prefix))) 241 | 242 | ;;;###autoload 243 | (defun cargo-mode-test (&optional prefix) 244 | "Run the `cargo test` command. 245 | If PREFIX is non-nil, prompt for additional params." 246 | (interactive "P") 247 | (let ((project-root (cargo-mode--project-directory))) 248 | (cargo-mode--start "test" cargo-mode-command-test project-root prefix))) 249 | 250 | ;;;###autoload 251 | (defun cargo-mode-build (&optional prefix) 252 | "Run the `cargo build` command. 253 | If PREFIX is non-nil, prompt for additional params." 254 | (interactive "P") 255 | (let ((project-root (cargo-mode--project-directory))) 256 | (cargo-mode--start "execute" cargo-mode-command-build project-root prefix))) 257 | 258 | ;;;###autoload 259 | (defun cargo-mode-clippy (&optional prefix) 260 | "Run the `cargo clippy` command. 261 | If PREFIX is non-nil, prompt for additional params." 262 | (interactive "P") 263 | (let ((project-root (cargo-mode--project-directory))) 264 | (cargo-mode--start "clippy" cargo-mode-command-clippy project-root prefix))) 265 | 266 | ;;;###autoload 267 | (defun cargo-mode-test-current-buffer (&optional prefix) 268 | "Run the cargo test for the current buffer. 269 | If PREFIX is non-nil, prompt for additional params." 270 | (interactive "P") 271 | (let* ((project-root (cargo-mode--project-directory)) 272 | (current-mod (print (cargo-mode--current-mod))) 273 | (command (concat cargo-mode-command-test " " current-mod))) 274 | (cargo-mode--start "test" command project-root prefix))) 275 | 276 | ;;;###autoload 277 | (defun cargo-mode-test-current-test (&optional prefix) 278 | "Run the Cargo test command for the current test. 279 | If PREFIX is non-nil, prompt for additional params." 280 | (interactive "P") 281 | (let* ((project-root (cargo-mode--project-directory)) 282 | (test-name (cargo-mode--current-test-fullname)) 283 | (command (concat cargo-mode-command-test " " test-name))) 284 | (cargo-mode--start "test" command project-root prefix))) 285 | 286 | ;;;###autoload 287 | (defun cargo-mode-last-command () 288 | "Re-execute the last `cargo-mode` task." 289 | (interactive) 290 | (if cargo-mode--last-command 291 | (apply #'cargo-mode--start-cmd cargo-mode--last-command) 292 | (message "Last command is not found."))) 293 | 294 | (defvar cargo-mode-command-map 295 | (let ((km (make-sparse-keymap))) 296 | (define-key km (kbd "b") 'cargo-mode-build) 297 | (define-key km (kbd "e") 'cargo-mode-execute-task) 298 | (define-key km (kbd "l") 'cargo-mode-last-command) 299 | (define-key km (kbd "t") 'cargo-mode-test) 300 | (define-key km (kbd "o") 'cargo-mode-test-current-buffer) 301 | (define-key km (kbd "f") 'cargo-mode-test-current-test) 302 | (define-key km (kbd "c") 'cargo-mode-clippy) 303 | km) 304 | "Cargo-mode keymap after prefix.") 305 | (fset 'cargo-mode-command-map cargo-mode-command-map) 306 | 307 | (defvar cargo-minor-mode-map 308 | (let ((map (make-sparse-keymap))) 309 | (define-key map (kbd "C-c a") 'cargo-mode-command-map) 310 | map) 311 | "Cargo-map keymap.") 312 | 313 | ;;;###autoload 314 | (define-minor-mode cargo-minor-mode 315 | "Cargo minor mode. Used for holding keybindings for `cargo-mode'. 316 | \\{cargo-minor-mode-map}" 317 | :init-value nil 318 | :lighter " cargo" 319 | :keymap cargo-minor-mode-map) 320 | 321 | (provide 'cargo-mode) 322 | ;;; cargo-mode.el ends here 323 | -------------------------------------------------------------------------------- /demo/demo1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayrat555/cargo-mode/b1fb87c17fcd22d798bb04115e65ecf83e8c929a/demo/demo1.gif -------------------------------------------------------------------------------- /demo/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayrat555/cargo-mode/b1fb87c17fcd22d798bb04115e65ecf83e8c929a/demo/demo2.gif --------------------------------------------------------------------------------