diff options
Diffstat (limited to '.emacs.d/lisp/rust-rustfmt.el')
| -rw-r--r-- | .emacs.d/lisp/rust-rustfmt.el | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/.emacs.d/lisp/rust-rustfmt.el b/.emacs.d/lisp/rust-rustfmt.el new file mode 100644 index 0000000..bb88647 --- /dev/null +++ b/.emacs.d/lisp/rust-rustfmt.el | |||
| @@ -0,0 +1,372 @@ | |||
| 1 | ;;; rust-rustfmt.el --- Support for rustfmt -*- lexical-binding:t -*- | ||
| 2 | ;;; Commentary: | ||
| 3 | |||
| 4 | ;; This library implements support for "rustfmt", a tool for | ||
| 5 | ;; formatting Rust code according to style guidelines. | ||
| 6 | |||
| 7 | ;;; Code: | ||
| 8 | ;;; Options | ||
| 9 | |||
| 10 | (defcustom rust-format-on-save nil | ||
| 11 | "Format future rust buffers before saving using rustfmt." | ||
| 12 | :type 'boolean | ||
| 13 | :safe #'booleanp | ||
| 14 | :group 'rust-mode) | ||
| 15 | |||
| 16 | (defcustom rust-format-show-buffer t | ||
| 17 | "Show *rustfmt* buffer if formatting detected problems." | ||
| 18 | :type 'boolean | ||
| 19 | :safe #'booleanp | ||
| 20 | :group 'rust-mode) | ||
| 21 | |||
| 22 | (defcustom rust-format-goto-problem t | ||
| 23 | "Jump to location of first detected problem when formatting buffer." | ||
| 24 | :type 'boolean | ||
| 25 | :safe #'booleanp | ||
| 26 | :group 'rust-mode) | ||
| 27 | |||
| 28 | (defcustom rust-rustfmt-bin "rustfmt" | ||
| 29 | "Path to rustfmt executable." | ||
| 30 | :type 'string | ||
| 31 | :group 'rust-mode) | ||
| 32 | |||
| 33 | (defcustom rust-rustfmt-switches '("--edition" "2018") | ||
| 34 | "Arguments to pass when invoking the `rustfmt' executable." | ||
| 35 | :type '(repeat string) | ||
| 36 | :group 'rust-mode) | ||
| 37 | |||
| 38 | ;;; _ | ||
| 39 | |||
| 40 | (defconst rust-rustfmt-buffername "*rustfmt*") | ||
| 41 | |||
| 42 | (defun rust--format-call (buf) | ||
| 43 | "Format BUF using rustfmt." | ||
| 44 | (with-current-buffer (get-buffer-create rust-rustfmt-buffername) | ||
| 45 | (view-mode +1) | ||
| 46 | (let ((inhibit-read-only t)) | ||
| 47 | (erase-buffer) | ||
| 48 | (insert-buffer-substring buf) | ||
| 49 | (let* ((tmpf (make-temp-file "rustfmt")) | ||
| 50 | (ret (apply #'call-process-region | ||
| 51 | (point-min) | ||
| 52 | (point-max) | ||
| 53 | rust-rustfmt-bin | ||
| 54 | t | ||
| 55 | `(t ,tmpf) | ||
| 56 | nil | ||
| 57 | rust-rustfmt-switches))) | ||
| 58 | (unwind-protect | ||
| 59 | (cond | ||
| 60 | ((zerop ret) | ||
| 61 | (if (not (string= (buffer-string) | ||
| 62 | (with-current-buffer buf (buffer-string)))) | ||
| 63 | ;; replace-buffer-contents was in emacs 26.1, but it | ||
| 64 | ;; was broken for non-ASCII strings, so we need 26.2. | ||
| 65 | (if (and (fboundp 'replace-buffer-contents) | ||
| 66 | (version<= "26.2" emacs-version)) | ||
| 67 | (with-current-buffer buf | ||
| 68 | (replace-buffer-contents rust-rustfmt-buffername)) | ||
| 69 | (copy-to-buffer buf (point-min) (point-max)))) | ||
| 70 | (kill-buffer)) | ||
| 71 | ((= ret 3) | ||
| 72 | (if (not (string= (buffer-string) | ||
| 73 | (with-current-buffer buf (buffer-string)))) | ||
| 74 | (copy-to-buffer buf (point-min) (point-max))) | ||
| 75 | (erase-buffer) | ||
| 76 | (insert-file-contents tmpf) | ||
| 77 | (rust--format-fix-rustfmt-buffer (buffer-name buf)) | ||
| 78 | (error "Rustfmt could not format some lines, see %s buffer for details" | ||
| 79 | rust-rustfmt-buffername)) | ||
| 80 | (t | ||
| 81 | (erase-buffer) | ||
| 82 | (insert-file-contents tmpf) | ||
| 83 | (rust--format-fix-rustfmt-buffer (buffer-name buf)) | ||
| 84 | (error "Rustfmt failed, see %s buffer for details" | ||
| 85 | rust-rustfmt-buffername)))) | ||
| 86 | (delete-file tmpf))))) | ||
| 87 | |||
| 88 | ;; Since we run rustfmt through stdin we get <stdin> markers in the | ||
| 89 | ;; output. This replaces them with the buffer name instead. | ||
| 90 | (defun rust--format-fix-rustfmt-buffer (buffer-name) | ||
| 91 | (save-match-data | ||
| 92 | (with-current-buffer (get-buffer rust-rustfmt-buffername) | ||
| 93 | (let ((inhibit-read-only t) | ||
| 94 | (fixed (format "--> %s:" buffer-name))) | ||
| 95 | (goto-char (point-min)) | ||
| 96 | (while (re-search-forward "--> \\(?:<stdin>\\|stdin\\):" nil t) | ||
| 97 | (replace-match fixed)))))) | ||
| 98 | |||
| 99 | ;; If rust-mode has been configured to navigate to source of the error | ||
| 100 | ;; or display it, do so -- and return true. Otherwise return nil to | ||
| 101 | ;; indicate nothing was done. | ||
| 102 | (defun rust--format-error-handler () | ||
| 103 | (let ((ok nil)) | ||
| 104 | (when rust-format-show-buffer | ||
| 105 | (display-buffer (get-buffer rust-rustfmt-buffername)) | ||
| 106 | (setq ok t)) | ||
| 107 | (when rust-format-goto-problem | ||
| 108 | (rust-goto-format-problem) | ||
| 109 | (setq ok t)) | ||
| 110 | ok)) | ||
| 111 | |||
| 112 | (defun rust-goto-format-problem () | ||
| 113 | "Jumps to problem reported by rustfmt, if any. | ||
| 114 | |||
| 115 | In case of multiple problems cycles through them. Displays the | ||
| 116 | rustfmt complain in the echo area." | ||
| 117 | (interactive) | ||
| 118 | ;; This uses position in *rustfmt* buffer to know which is the next | ||
| 119 | ;; error to jump to, and source: line in the buffer to figure which | ||
| 120 | ;; buffer it is from. | ||
| 121 | (let ((rustfmt (get-buffer rust-rustfmt-buffername))) | ||
| 122 | (if (not rustfmt) | ||
| 123 | (message "No %s, no problems." rust-rustfmt-buffername) | ||
| 124 | (let ((target-buffer (with-current-buffer rustfmt | ||
| 125 | (save-excursion | ||
| 126 | (goto-char (point-min)) | ||
| 127 | (when (re-search-forward "--> \\([^:]+\\):" nil t) | ||
| 128 | (match-string 1))))) | ||
| 129 | (target-point (with-current-buffer rustfmt | ||
| 130 | ;; No save-excursion, this is how we cycle through! | ||
| 131 | (let ((regex "--> [^:]+:\\([0-9]+\\):\\([0-9]+\\)")) | ||
| 132 | (when (or (re-search-forward regex nil t) | ||
| 133 | (progn (goto-char (point-min)) | ||
| 134 | (re-search-forward regex nil t))) | ||
| 135 | (cons (string-to-number (match-string 1)) | ||
| 136 | (string-to-number (match-string 2))))))) | ||
| 137 | (target-problem (with-current-buffer rustfmt | ||
| 138 | (save-excursion | ||
| 139 | (when (re-search-backward "^error:.+\n" nil t) | ||
| 140 | (forward-char (length "error: ")) | ||
| 141 | (let ((p0 (point))) | ||
| 142 | (if (re-search-forward "\nerror:.+\n" nil t) | ||
| 143 | (buffer-substring p0 (point)) | ||
| 144 | (buffer-substring p0 (point-max))))))))) | ||
| 145 | (when (and target-buffer (get-buffer target-buffer) target-point) | ||
| 146 | (switch-to-buffer target-buffer) | ||
| 147 | (goto-char (point-min)) | ||
| 148 | (forward-line (1- (car target-point))) | ||
| 149 | (forward-char (1- (cdr target-point)))) | ||
| 150 | (message target-problem))))) | ||
| 151 | |||
| 152 | (defconst rust--format-word "\ | ||
| 153 | \\b\\(else\\|enum\\|fn\\|for\\|if\\|let\\|loop\\|\ | ||
| 154 | match\\|struct\\|union\\|unsafe\\|while\\)\\b") | ||
| 155 | (defconst rust--format-line "\\(\n\\)") | ||
| 156 | |||
| 157 | ;; Counts number of matches of regex beginning up to max-beginning, | ||
| 158 | ;; leaving the point at the beginning of the last match. | ||
| 159 | (defun rust--format-count (regex max-beginning) | ||
| 160 | (let ((count 0) | ||
| 161 | save-point | ||
| 162 | beginning) | ||
| 163 | (while (and (< (point) max-beginning) | ||
| 164 | (re-search-forward regex max-beginning t)) | ||
| 165 | (setq count (1+ count)) | ||
| 166 | (setq beginning (match-beginning 1))) | ||
| 167 | ;; try one more in case max-beginning lies in the middle of a match | ||
| 168 | (setq save-point (point)) | ||
| 169 | (when (re-search-forward regex nil t) | ||
| 170 | (let ((try-beginning (match-beginning 1))) | ||
| 171 | (if (> try-beginning max-beginning) | ||
| 172 | (goto-char save-point) | ||
| 173 | (setq count (1+ count)) | ||
| 174 | (setq beginning try-beginning)))) | ||
| 175 | (when beginning (goto-char beginning)) | ||
| 176 | count)) | ||
| 177 | |||
| 178 | ;; Gets list describing pos or (point). | ||
| 179 | ;; The list contains: | ||
| 180 | ;; 1. the number of matches of rust--format-word, | ||
| 181 | ;; 2. the number of matches of rust--format-line after that, | ||
| 182 | ;; 3. the number of columns after that. | ||
| 183 | (defun rust--format-get-loc (buffer &optional pos) | ||
| 184 | (with-current-buffer buffer | ||
| 185 | (save-excursion | ||
| 186 | (let ((pos (or pos (point))) | ||
| 187 | words lines columns) | ||
| 188 | (goto-char (point-min)) | ||
| 189 | (setq words (rust--format-count rust--format-word pos)) | ||
| 190 | (setq lines (rust--format-count rust--format-line pos)) | ||
| 191 | (if (> lines 0) | ||
| 192 | (if (= (point) pos) | ||
| 193 | (setq columns -1) | ||
| 194 | (forward-char 1) | ||
| 195 | (goto-char pos) | ||
| 196 | (setq columns (current-column))) | ||
| 197 | (let ((initial-column (current-column))) | ||
| 198 | (goto-char pos) | ||
| 199 | (setq columns (- (current-column) initial-column)))) | ||
| 200 | (list words lines columns))))) | ||
| 201 | |||
| 202 | ;; Moves the point forward by count matches of regex up to max-pos, | ||
| 203 | ;; and returns new max-pos making sure final position does not include another match. | ||
| 204 | (defun rust--format-forward (regex count max-pos) | ||
| 205 | (when (< (point) max-pos) | ||
| 206 | (let ((beginning (point))) | ||
| 207 | (while (> count 0) | ||
| 208 | (setq count (1- count)) | ||
| 209 | (re-search-forward regex nil t) | ||
| 210 | (setq beginning (match-beginning 1))) | ||
| 211 | (when (re-search-forward regex nil t) | ||
| 212 | (setq max-pos (min max-pos (match-beginning 1)))) | ||
| 213 | (goto-char beginning))) | ||
| 214 | max-pos) | ||
| 215 | |||
| 216 | ;; Gets the position from a location list obtained using rust--format-get-loc. | ||
| 217 | (defun rust--format-get-pos (buffer loc) | ||
| 218 | (with-current-buffer buffer | ||
| 219 | (save-excursion | ||
| 220 | (goto-char (point-min)) | ||
| 221 | (let ((max-pos (point-max)) | ||
| 222 | (words (pop loc)) | ||
| 223 | (lines (pop loc)) | ||
| 224 | (columns (pop loc))) | ||
| 225 | (setq max-pos (rust--format-forward rust--format-word words max-pos)) | ||
| 226 | (setq max-pos (rust--format-forward rust--format-line lines max-pos)) | ||
| 227 | (when (> lines 0) (forward-char)) | ||
| 228 | (let ((initial-column (current-column)) | ||
| 229 | (save-point (point))) | ||
| 230 | (move-end-of-line nil) | ||
| 231 | (when (> (current-column) (+ initial-column columns)) | ||
| 232 | (goto-char save-point) | ||
| 233 | (forward-char columns))) | ||
| 234 | (min (point) max-pos))))) | ||
| 235 | |||
| 236 | (defun rust-format-diff-buffer () | ||
| 237 | "Show diff to current buffer from rustfmt. | ||
| 238 | |||
| 239 | Return the created process." | ||
| 240 | (interactive) | ||
| 241 | (unless (executable-find rust-rustfmt-bin) | ||
| 242 | (error "Could not locate executable %S" rust-rustfmt-bin)) | ||
| 243 | (let* ((buffer | ||
| 244 | (with-current-buffer | ||
| 245 | (get-buffer-create "*rustfmt-diff*") | ||
| 246 | (let ((inhibit-read-only t)) | ||
| 247 | (erase-buffer)) | ||
| 248 | (current-buffer))) | ||
| 249 | (proc | ||
| 250 | (apply #'start-process | ||
| 251 | "rustfmt-diff" | ||
| 252 | buffer | ||
| 253 | rust-rustfmt-bin | ||
| 254 | "--check" | ||
| 255 | (cons (buffer-file-name) | ||
| 256 | rust-rustfmt-switches)))) | ||
| 257 | (set-process-sentinel proc #'rust-format-diff-buffer-sentinel) | ||
| 258 | proc)) | ||
| 259 | |||
| 260 | (defun rust-format-diff-buffer-sentinel (process _e) | ||
| 261 | (when (eq 'exit (process-status process)) | ||
| 262 | (if (> (process-exit-status process) 0) | ||
| 263 | (with-current-buffer "*rustfmt-diff*" | ||
| 264 | (let ((inhibit-read-only t)) | ||
| 265 | (diff-mode)) | ||
| 266 | (pop-to-buffer (current-buffer))) | ||
| 267 | (message "rustfmt check passed.")))) | ||
| 268 | |||
| 269 | (defun rust--format-buffer-using-replace-buffer-contents () | ||
| 270 | (condition-case err | ||
| 271 | (progn | ||
| 272 | (rust--format-call (current-buffer)) | ||
| 273 | (message "Formatted buffer with rustfmt.")) | ||
| 274 | (error | ||
| 275 | (or (rust--format-error-handler) | ||
| 276 | (signal (car err) (cdr err)))))) | ||
| 277 | |||
| 278 | (defun rust--format-buffer-saving-position-manually () | ||
| 279 | (let* ((current (current-buffer)) | ||
| 280 | (base (or (buffer-base-buffer current) current)) | ||
| 281 | buffer-loc | ||
| 282 | window-loc) | ||
| 283 | (dolist (buffer (buffer-list)) | ||
| 284 | (when (or (eq buffer base) | ||
| 285 | (eq (buffer-base-buffer buffer) base)) | ||
| 286 | (push (list buffer | ||
| 287 | (rust--format-get-loc buffer nil)) | ||
| 288 | buffer-loc))) | ||
| 289 | (dolist (frame (frame-list)) | ||
| 290 | (dolist (window (window-list frame)) | ||
| 291 | (let ((buffer (window-buffer window))) | ||
| 292 | (when (or (eq buffer base) | ||
| 293 | (eq (buffer-base-buffer buffer) base)) | ||
| 294 | (let ((start (window-start window)) | ||
| 295 | (point (window-point window))) | ||
| 296 | (push (list window | ||
| 297 | (rust--format-get-loc buffer start) | ||
| 298 | (rust--format-get-loc buffer point)) | ||
| 299 | window-loc)))))) | ||
| 300 | (condition-case err | ||
| 301 | (unwind-protect | ||
| 302 | ;; save and restore window start position | ||
| 303 | ;; after reformatting | ||
| 304 | ;; to avoid the disturbing scrolling | ||
| 305 | (let ((w-start (window-start))) | ||
| 306 | (rust--format-call (current-buffer)) | ||
| 307 | (set-window-start (selected-window) w-start) | ||
| 308 | (message "Formatted buffer with rustfmt.")) | ||
| 309 | (dolist (loc buffer-loc) | ||
| 310 | (let* ((buffer (pop loc)) | ||
| 311 | (pos (rust--format-get-pos buffer (pop loc)))) | ||
| 312 | (with-current-buffer buffer | ||
| 313 | (goto-char pos)))) | ||
| 314 | (dolist (loc window-loc) | ||
| 315 | (let* ((window (pop loc)) | ||
| 316 | (buffer (window-buffer window)) | ||
| 317 | (start (rust--format-get-pos buffer (pop loc))) | ||
| 318 | (pos (rust--format-get-pos buffer (pop loc)))) | ||
| 319 | (unless (eq buffer current) | ||
| 320 | (set-window-start window start)) | ||
| 321 | (set-window-point window pos)))) | ||
| 322 | (error | ||
| 323 | (or (rust--format-error-handler) | ||
| 324 | (signal (car err) (cdr err))))))) | ||
| 325 | |||
| 326 | (defun rust-format-buffer () | ||
| 327 | "Format the current buffer using rustfmt." | ||
| 328 | (interactive) | ||
| 329 | (unless (executable-find rust-rustfmt-bin) | ||
| 330 | (error "Could not locate executable \"%s\"" rust-rustfmt-bin)) | ||
| 331 | ;; If emacs version >= 26.2, we can use replace-buffer-contents to | ||
| 332 | ;; preserve location and markers in buffer, otherwise we can try to | ||
| 333 | ;; save locations as best we can, though we still lose markers. | ||
| 334 | (save-excursion | ||
| 335 | (if (version<= "26.2" emacs-version) | ||
| 336 | (rust--format-buffer-using-replace-buffer-contents) | ||
| 337 | (rust--format-buffer-saving-position-manually)))) | ||
| 338 | |||
| 339 | (defun rust-enable-format-on-save () | ||
| 340 | "Enable formatting using rustfmt when saving buffer." | ||
| 341 | (interactive) | ||
| 342 | (setq-local rust-format-on-save t)) | ||
| 343 | |||
| 344 | (defun rust-disable-format-on-save () | ||
| 345 | "Disable formatting using rustfmt when saving buffer." | ||
| 346 | (interactive) | ||
| 347 | (setq-local rust-format-on-save nil)) | ||
| 348 | |||
| 349 | ;;; Hooks | ||
| 350 | |||
| 351 | (defun rust-before-save-method () | ||
| 352 | (when rust-format-on-save | ||
| 353 | (condition-case e | ||
| 354 | (rust-format-buffer) | ||
| 355 | (message (format "rust-before-save-hook: %S %S" | ||
| 356 | (car e) | ||
| 357 | (cdr e)))))) | ||
| 358 | |||
| 359 | (defun rust-after-save-method () | ||
| 360 | (when rust-format-on-save | ||
| 361 | (if (not (executable-find rust-rustfmt-bin)) | ||
| 362 | (error "Could not locate executable \"%s\"" rust-rustfmt-bin) | ||
| 363 | (when (get-buffer rust-rustfmt-buffername) | ||
| 364 | ;; KLDUGE: re-run the error handlers -- otherwise message area | ||
| 365 | ;; would show "Wrote ..." instead of the error description. | ||
| 366 | (or (rust--format-error-handler) | ||
| 367 | (message "rustfmt detected problems, see %s for more." | ||
| 368 | rust-rustfmt-buffername)))))) | ||
| 369 | |||
| 370 | ;;; _ | ||
| 371 | (provide 'rust-rustfmt) | ||
| 372 | ;;; rust-rustfmt.el ends here | ||
