├── 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 | 
12 |
13 | ### Example with custom params (cargo doc --open)
14 |
15 | 
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
--------------------------------------------------------------------------------