373 lines
14 KiB
EmacsLisp
373 lines
14 KiB
EmacsLisp
;;; rust-rustfmt.el --- Support for rustfmt -*- lexical-binding:t -*-
|
|
;;; Commentary:
|
|
|
|
;; This library implements support for "rustfmt", a tool for
|
|
;; formatting Rust code according to style guidelines.
|
|
|
|
;;; Code:
|
|
;;; Options
|
|
|
|
(defcustom rust-format-on-save nil
|
|
"Format future rust buffers before saving using rustfmt."
|
|
:type 'boolean
|
|
:safe #'booleanp
|
|
:group 'rust-mode)
|
|
|
|
(defcustom rust-format-show-buffer t
|
|
"Show *rustfmt* buffer if formatting detected problems."
|
|
:type 'boolean
|
|
:safe #'booleanp
|
|
:group 'rust-mode)
|
|
|
|
(defcustom rust-format-goto-problem t
|
|
"Jump to location of first detected problem when formatting buffer."
|
|
:type 'boolean
|
|
:safe #'booleanp
|
|
:group 'rust-mode)
|
|
|
|
(defcustom rust-rustfmt-bin "rustfmt"
|
|
"Path to rustfmt executable."
|
|
:type 'string
|
|
:group 'rust-mode)
|
|
|
|
(defcustom rust-rustfmt-switches '("--edition" "2018")
|
|
"Arguments to pass when invoking the `rustfmt' executable."
|
|
:type '(repeat string)
|
|
:group 'rust-mode)
|
|
|
|
;;; _
|
|
|
|
(defconst rust-rustfmt-buffername "*rustfmt*")
|
|
|
|
(defun rust--format-call (buf)
|
|
"Format BUF using rustfmt."
|
|
(with-current-buffer (get-buffer-create rust-rustfmt-buffername)
|
|
(view-mode +1)
|
|
(let ((inhibit-read-only t))
|
|
(erase-buffer)
|
|
(insert-buffer-substring buf)
|
|
(let* ((tmpf (make-temp-file "rustfmt"))
|
|
(ret (apply #'call-process-region
|
|
(point-min)
|
|
(point-max)
|
|
rust-rustfmt-bin
|
|
t
|
|
`(t ,tmpf)
|
|
nil
|
|
rust-rustfmt-switches)))
|
|
(unwind-protect
|
|
(cond
|
|
((zerop ret)
|
|
(if (not (string= (buffer-string)
|
|
(with-current-buffer buf (buffer-string))))
|
|
;; replace-buffer-contents was in emacs 26.1, but it
|
|
;; was broken for non-ASCII strings, so we need 26.2.
|
|
(if (and (fboundp 'replace-buffer-contents)
|
|
(version<= "26.2" emacs-version))
|
|
(with-current-buffer buf
|
|
(replace-buffer-contents rust-rustfmt-buffername))
|
|
(copy-to-buffer buf (point-min) (point-max))))
|
|
(kill-buffer))
|
|
((= ret 3)
|
|
(if (not (string= (buffer-string)
|
|
(with-current-buffer buf (buffer-string))))
|
|
(copy-to-buffer buf (point-min) (point-max)))
|
|
(erase-buffer)
|
|
(insert-file-contents tmpf)
|
|
(rust--format-fix-rustfmt-buffer (buffer-name buf))
|
|
(error "Rustfmt could not format some lines, see %s buffer for details"
|
|
rust-rustfmt-buffername))
|
|
(t
|
|
(erase-buffer)
|
|
(insert-file-contents tmpf)
|
|
(rust--format-fix-rustfmt-buffer (buffer-name buf))
|
|
(error "Rustfmt failed, see %s buffer for details"
|
|
rust-rustfmt-buffername))))
|
|
(delete-file tmpf)))))
|
|
|
|
;; Since we run rustfmt through stdin we get <stdin> markers in the
|
|
;; output. This replaces them with the buffer name instead.
|
|
(defun rust--format-fix-rustfmt-buffer (buffer-name)
|
|
(save-match-data
|
|
(with-current-buffer (get-buffer rust-rustfmt-buffername)
|
|
(let ((inhibit-read-only t)
|
|
(fixed (format "--> %s:" buffer-name)))
|
|
(goto-char (point-min))
|
|
(while (re-search-forward "--> \\(?:<stdin>\\|stdin\\):" nil t)
|
|
(replace-match fixed))))))
|
|
|
|
;; If rust-mode has been configured to navigate to source of the error
|
|
;; or display it, do so -- and return true. Otherwise return nil to
|
|
;; indicate nothing was done.
|
|
(defun rust--format-error-handler ()
|
|
(let ((ok nil))
|
|
(when rust-format-show-buffer
|
|
(display-buffer (get-buffer rust-rustfmt-buffername))
|
|
(setq ok t))
|
|
(when rust-format-goto-problem
|
|
(rust-goto-format-problem)
|
|
(setq ok t))
|
|
ok))
|
|
|
|
(defun rust-goto-format-problem ()
|
|
"Jumps to problem reported by rustfmt, if any.
|
|
|
|
In case of multiple problems cycles through them. Displays the
|
|
rustfmt complain in the echo area."
|
|
(interactive)
|
|
;; This uses position in *rustfmt* buffer to know which is the next
|
|
;; error to jump to, and source: line in the buffer to figure which
|
|
;; buffer it is from.
|
|
(let ((rustfmt (get-buffer rust-rustfmt-buffername)))
|
|
(if (not rustfmt)
|
|
(message "No %s, no problems." rust-rustfmt-buffername)
|
|
(let ((target-buffer (with-current-buffer rustfmt
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(when (re-search-forward "--> \\([^:]+\\):" nil t)
|
|
(match-string 1)))))
|
|
(target-point (with-current-buffer rustfmt
|
|
;; No save-excursion, this is how we cycle through!
|
|
(let ((regex "--> [^:]+:\\([0-9]+\\):\\([0-9]+\\)"))
|
|
(when (or (re-search-forward regex nil t)
|
|
(progn (goto-char (point-min))
|
|
(re-search-forward regex nil t)))
|
|
(cons (string-to-number (match-string 1))
|
|
(string-to-number (match-string 2)))))))
|
|
(target-problem (with-current-buffer rustfmt
|
|
(save-excursion
|
|
(when (re-search-backward "^error:.+\n" nil t)
|
|
(forward-char (length "error: "))
|
|
(let ((p0 (point)))
|
|
(if (re-search-forward "\nerror:.+\n" nil t)
|
|
(buffer-substring p0 (point))
|
|
(buffer-substring p0 (point-max)))))))))
|
|
(when (and target-buffer (get-buffer target-buffer) target-point)
|
|
(switch-to-buffer target-buffer)
|
|
(goto-char (point-min))
|
|
(forward-line (1- (car target-point)))
|
|
(forward-char (1- (cdr target-point))))
|
|
(message target-problem)))))
|
|
|
|
(defconst rust--format-word "\
|
|
\\b\\(else\\|enum\\|fn\\|for\\|if\\|let\\|loop\\|\
|
|
match\\|struct\\|union\\|unsafe\\|while\\)\\b")
|
|
(defconst rust--format-line "\\(\n\\)")
|
|
|
|
;; Counts number of matches of regex beginning up to max-beginning,
|
|
;; leaving the point at the beginning of the last match.
|
|
(defun rust--format-count (regex max-beginning)
|
|
(let ((count 0)
|
|
save-point
|
|
beginning)
|
|
(while (and (< (point) max-beginning)
|
|
(re-search-forward regex max-beginning t))
|
|
(setq count (1+ count))
|
|
(setq beginning (match-beginning 1)))
|
|
;; try one more in case max-beginning lies in the middle of a match
|
|
(setq save-point (point))
|
|
(when (re-search-forward regex nil t)
|
|
(let ((try-beginning (match-beginning 1)))
|
|
(if (> try-beginning max-beginning)
|
|
(goto-char save-point)
|
|
(setq count (1+ count))
|
|
(setq beginning try-beginning))))
|
|
(when beginning (goto-char beginning))
|
|
count))
|
|
|
|
;; Gets list describing pos or (point).
|
|
;; The list contains:
|
|
;; 1. the number of matches of rust--format-word,
|
|
;; 2. the number of matches of rust--format-line after that,
|
|
;; 3. the number of columns after that.
|
|
(defun rust--format-get-loc (buffer &optional pos)
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(let ((pos (or pos (point)))
|
|
words lines columns)
|
|
(goto-char (point-min))
|
|
(setq words (rust--format-count rust--format-word pos))
|
|
(setq lines (rust--format-count rust--format-line pos))
|
|
(if (> lines 0)
|
|
(if (= (point) pos)
|
|
(setq columns -1)
|
|
(forward-char 1)
|
|
(goto-char pos)
|
|
(setq columns (current-column)))
|
|
(let ((initial-column (current-column)))
|
|
(goto-char pos)
|
|
(setq columns (- (current-column) initial-column))))
|
|
(list words lines columns)))))
|
|
|
|
;; Moves the point forward by count matches of regex up to max-pos,
|
|
;; and returns new max-pos making sure final position does not include another match.
|
|
(defun rust--format-forward (regex count max-pos)
|
|
(when (< (point) max-pos)
|
|
(let ((beginning (point)))
|
|
(while (> count 0)
|
|
(setq count (1- count))
|
|
(re-search-forward regex nil t)
|
|
(setq beginning (match-beginning 1)))
|
|
(when (re-search-forward regex nil t)
|
|
(setq max-pos (min max-pos (match-beginning 1))))
|
|
(goto-char beginning)))
|
|
max-pos)
|
|
|
|
;; Gets the position from a location list obtained using rust--format-get-loc.
|
|
(defun rust--format-get-pos (buffer loc)
|
|
(with-current-buffer buffer
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(let ((max-pos (point-max))
|
|
(words (pop loc))
|
|
(lines (pop loc))
|
|
(columns (pop loc)))
|
|
(setq max-pos (rust--format-forward rust--format-word words max-pos))
|
|
(setq max-pos (rust--format-forward rust--format-line lines max-pos))
|
|
(when (> lines 0) (forward-char))
|
|
(let ((initial-column (current-column))
|
|
(save-point (point)))
|
|
(move-end-of-line nil)
|
|
(when (> (current-column) (+ initial-column columns))
|
|
(goto-char save-point)
|
|
(forward-char columns)))
|
|
(min (point) max-pos)))))
|
|
|
|
(defun rust-format-diff-buffer ()
|
|
"Show diff to current buffer from rustfmt.
|
|
|
|
Return the created process."
|
|
(interactive)
|
|
(unless (executable-find rust-rustfmt-bin)
|
|
(error "Could not locate executable %S" rust-rustfmt-bin))
|
|
(let* ((buffer
|
|
(with-current-buffer
|
|
(get-buffer-create "*rustfmt-diff*")
|
|
(let ((inhibit-read-only t))
|
|
(erase-buffer))
|
|
(current-buffer)))
|
|
(proc
|
|
(apply #'start-process
|
|
"rustfmt-diff"
|
|
buffer
|
|
rust-rustfmt-bin
|
|
"--check"
|
|
(cons (buffer-file-name)
|
|
rust-rustfmt-switches))))
|
|
(set-process-sentinel proc #'rust-format-diff-buffer-sentinel)
|
|
proc))
|
|
|
|
(defun rust-format-diff-buffer-sentinel (process _e)
|
|
(when (eq 'exit (process-status process))
|
|
(if (> (process-exit-status process) 0)
|
|
(with-current-buffer "*rustfmt-diff*"
|
|
(let ((inhibit-read-only t))
|
|
(diff-mode))
|
|
(pop-to-buffer (current-buffer)))
|
|
(message "rustfmt check passed."))))
|
|
|
|
(defun rust--format-buffer-using-replace-buffer-contents ()
|
|
(condition-case err
|
|
(progn
|
|
(rust--format-call (current-buffer))
|
|
(message "Formatted buffer with rustfmt."))
|
|
(error
|
|
(or (rust--format-error-handler)
|
|
(signal (car err) (cdr err))))))
|
|
|
|
(defun rust--format-buffer-saving-position-manually ()
|
|
(let* ((current (current-buffer))
|
|
(base (or (buffer-base-buffer current) current))
|
|
buffer-loc
|
|
window-loc)
|
|
(dolist (buffer (buffer-list))
|
|
(when (or (eq buffer base)
|
|
(eq (buffer-base-buffer buffer) base))
|
|
(push (list buffer
|
|
(rust--format-get-loc buffer nil))
|
|
buffer-loc)))
|
|
(dolist (frame (frame-list))
|
|
(dolist (window (window-list frame))
|
|
(let ((buffer (window-buffer window)))
|
|
(when (or (eq buffer base)
|
|
(eq (buffer-base-buffer buffer) base))
|
|
(let ((start (window-start window))
|
|
(point (window-point window)))
|
|
(push (list window
|
|
(rust--format-get-loc buffer start)
|
|
(rust--format-get-loc buffer point))
|
|
window-loc))))))
|
|
(condition-case err
|
|
(unwind-protect
|
|
;; save and restore window start position
|
|
;; after reformatting
|
|
;; to avoid the disturbing scrolling
|
|
(let ((w-start (window-start)))
|
|
(rust--format-call (current-buffer))
|
|
(set-window-start (selected-window) w-start)
|
|
(message "Formatted buffer with rustfmt."))
|
|
(dolist (loc buffer-loc)
|
|
(let* ((buffer (pop loc))
|
|
(pos (rust--format-get-pos buffer (pop loc))))
|
|
(with-current-buffer buffer
|
|
(goto-char pos))))
|
|
(dolist (loc window-loc)
|
|
(let* ((window (pop loc))
|
|
(buffer (window-buffer window))
|
|
(start (rust--format-get-pos buffer (pop loc)))
|
|
(pos (rust--format-get-pos buffer (pop loc))))
|
|
(unless (eq buffer current)
|
|
(set-window-start window start))
|
|
(set-window-point window pos))))
|
|
(error
|
|
(or (rust--format-error-handler)
|
|
(signal (car err) (cdr err)))))))
|
|
|
|
(defun rust-format-buffer ()
|
|
"Format the current buffer using rustfmt."
|
|
(interactive)
|
|
(unless (executable-find rust-rustfmt-bin)
|
|
(error "Could not locate executable \"%s\"" rust-rustfmt-bin))
|
|
;; If emacs version >= 26.2, we can use replace-buffer-contents to
|
|
;; preserve location and markers in buffer, otherwise we can try to
|
|
;; save locations as best we can, though we still lose markers.
|
|
(save-excursion
|
|
(if (version<= "26.2" emacs-version)
|
|
(rust--format-buffer-using-replace-buffer-contents)
|
|
(rust--format-buffer-saving-position-manually))))
|
|
|
|
(defun rust-enable-format-on-save ()
|
|
"Enable formatting using rustfmt when saving buffer."
|
|
(interactive)
|
|
(setq-local rust-format-on-save t))
|
|
|
|
(defun rust-disable-format-on-save ()
|
|
"Disable formatting using rustfmt when saving buffer."
|
|
(interactive)
|
|
(setq-local rust-format-on-save nil))
|
|
|
|
;;; Hooks
|
|
|
|
(defun rust-before-save-method ()
|
|
(when rust-format-on-save
|
|
(condition-case e
|
|
(rust-format-buffer)
|
|
(message (format "rust-before-save-hook: %S %S"
|
|
(car e)
|
|
(cdr e))))))
|
|
|
|
(defun rust-after-save-method ()
|
|
(when rust-format-on-save
|
|
(if (not (executable-find rust-rustfmt-bin))
|
|
(error "Could not locate executable \"%s\"" rust-rustfmt-bin)
|
|
(when (get-buffer rust-rustfmt-buffername)
|
|
;; KLDUGE: re-run the error handlers -- otherwise message area
|
|
;; would show "Wrote ..." instead of the error description.
|
|
(or (rust--format-error-handler)
|
|
(message "rustfmt detected problems, see %s for more."
|
|
rust-rustfmt-buffername))))))
|
|
|
|
;;; _
|
|
(provide 'rust-rustfmt)
|
|
;;; rust-rustfmt.el ends here
|