| 1 | ;;; reformatter.el --- Define commands which run reformatters on the current buffer -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;; Copyright (C) 2019 Steve Purcell |
| 4 | |
| 5 | ;; Author: Steve Purcell <steve@sanityinc.com> |
| 6 | ;; Keywords: convenience, tools |
| 7 | ;; Homepage: https://github.com/purcell/emacs-reformatter |
| 8 | ;; Package-Requires: ((emacs "24.3")) |
| 9 | ;; Package-Version: 0.8 |
| 10 | |
| 11 | ;; This program is free software; you can redistribute it and/or modify |
| 12 | ;; it under the terms of the GNU General Public License as published by |
| 13 | ;; the Free Software Foundation, either version 3 of the License, or |
| 14 | ;; (at your option) any later version. |
| 15 | |
| 16 | ;; This program is distributed in the hope that it will be useful, |
| 17 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 18 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 19 | ;; GNU General Public License for more details. |
| 20 | |
| 21 | ;; You should have received a copy of the GNU General Public License |
| 22 | ;; along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 23 | |
| 24 | ;;; Commentary: |
| 25 | |
| 26 | ;; This library lets elisp authors easily define an idiomatic command |
| 27 | ;; to reformat the current buffer using a command-line program, |
| 28 | ;; together with an optional minor mode which can apply this command |
| 29 | ;; automatically on save. |
| 30 | |
| 31 | ;; By default, reformatter.el expects programs to read from stdin and |
| 32 | ;; write to stdout, and you should prefer this mode of operation where |
| 33 | ;; possible. If this isn't possible with your particular formatting |
| 34 | ;; program, refer to the options for `reformatter-define', and see the |
| 35 | ;; examples in the package's tests. |
| 36 | |
| 37 | ;; As an example, let's define a reformat command that applies the |
| 38 | ;; "dhall format" command. We'll assume here that we've already defined a |
| 39 | ;; variable `dhall-command' which holds the string name or path of the |
| 40 | ;; dhall executable: |
| 41 | |
| 42 | ;; (reformatter-define dhall-format |
| 43 | ;; :program dhall-command |
| 44 | ;; :args '("format")) |
| 45 | |
| 46 | ;; The `reformatter-define' macro expands to code which generates |
| 47 | ;; `dhall-format-buffer' and `dhall-format-region' interactive |
| 48 | ;; commands, and a local minor mode called |
| 49 | ;; `dhall-format-on-save-mode'. The :args" and :program expressions |
| 50 | ;; will be evaluated at runtime, so they can refer to variables that |
| 51 | ;; may (later) have a buffer-local value. A custom variable will be |
| 52 | ;; generated for the mode lighter, with the supplied value becoming |
| 53 | ;; the default. |
| 54 | |
| 55 | ;; The generated minor mode allows idiomatic per-directory or per-file |
| 56 | ;; customisation, via the "modes" support baked into Emacs' file-local |
| 57 | ;; and directory-local variables mechanisms. For example, users of |
| 58 | ;; the above example might add the following to a project-specific |
| 59 | ;; .dir-locals.el file: |
| 60 | |
| 61 | ;; ((dhall-mode |
| 62 | ;; (mode . dhall-format-on-save))) |
| 63 | |
| 64 | ;; See the documentation for `reformatter-define', which provides a |
| 65 | ;; number of options for customising the generated code. |
| 66 | |
| 67 | ;; Library authors might like to provide autoloads for the generated |
| 68 | ;; code, e.g.: |
| 69 | |
| 70 | ;; ;;;###autoload (autoload 'dhall-format-buffer "current-file" nil t) |
| 71 | ;; ;;;###autoload (autoload 'dhall-format-region "current-file" nil t) |
| 72 | ;; ;;;###autoload (autoload 'dhall-format-on-save-mode "current-file" nil t) |
| 73 | |
| 74 | ;;; Code: |
| 75 | (eval-when-compile |
| 76 | (require 'cl-lib)) |
| 77 | (require 'ansi-color) |
| 78 | |
| 79 | (defun reformatter--make-temp-file (sym) |
| 80 | "Create a temporary file whose filename is based on SYM, but with |
| 81 | slashes replaced by underscores. `make-temp-file' fails |
| 82 | otherwise as it cannot create intermediate directories." |
| 83 | (make-temp-file |
| 84 | (replace-regexp-in-string "/" "_" (symbol-name sym)))) |
| 85 | |
| 86 | (defun reformatter--do-region (name beg end program args stdin stdout input-file exit-code-success-p display-errors) |
| 87 | "Do the work of reformatter called NAME. |
| 88 | Reformats the current buffer's region from BEG to END using |
| 89 | PROGRAM and ARGS. For args STDIN, STDOUT, INPUT-FILE, |
| 90 | EXIT-CODE-SUCCESS-P and DISPLAY-ERRORS see the documentation of |
| 91 | the `reformatter-define' macro." |
| 92 | (cl-assert input-file) |
| 93 | (cl-assert (functionp exit-code-success-p)) |
| 94 | (when (and input-file |
| 95 | (buffer-file-name) |
| 96 | (string= (file-truename input-file) |
| 97 | (file-truename (buffer-file-name)))) |
| 98 | (error "The reformatter must not operate on the current file in-place")) |
| 99 | (let* ((stderr-file (reformatter--make-temp-file name)) |
| 100 | (stdout-file (reformatter--make-temp-file name)) |
| 101 | ;; Setting this coding system might not universally be |
| 102 | ;; the best default, but was apparently necessary for |
| 103 | ;; some hand-rolled reformatter functions that this |
| 104 | ;; library was written to replace. |
| 105 | (coding-system-for-read 'utf-8) |
| 106 | (coding-system-for-write 'utf-8)) |
| 107 | (unwind-protect |
| 108 | (progn |
| 109 | (write-region beg end input-file nil :quiet) |
| 110 | (let* ((error-buffer (get-buffer-create (format "*%s errors*" name))) |
| 111 | (retcode |
| 112 | (condition-case e |
| 113 | (apply 'call-process program |
| 114 | (when stdin input-file) |
| 115 | (list (list :file stdout-file) stderr-file) |
| 116 | nil |
| 117 | args) |
| 118 | (error e)))) |
| 119 | (with-current-buffer error-buffer |
| 120 | (let ((inhibit-read-only t)) |
| 121 | (insert-file-contents stderr-file nil nil nil t) |
| 122 | (unless (integerp retcode) |
| 123 | (insert (error-message-string retcode))) |
| 124 | (ansi-color-apply-on-region (point-min) (point-max))) |
| 125 | (special-mode)) |
| 126 | (if (and (integerp retcode) (funcall exit-code-success-p retcode)) |
| 127 | (progn |
| 128 | (save-restriction |
| 129 | ;; This replacement method minimises |
| 130 | ;; disruption to marker positions and the |
| 131 | ;; undo list |
| 132 | (narrow-to-region beg end) |
| 133 | (reformatter-replace-buffer-contents-from-file (if stdout |
| 134 | stdout-file |
| 135 | input-file))) |
| 136 | ;; If there are no errors then we hide the error buffer |
| 137 | (delete-windows-on error-buffer)) |
| 138 | (if display-errors |
| 139 | (display-buffer error-buffer) |
| 140 | (message (concat (symbol-name name) " failed: see %s") (buffer-name error-buffer)))))) |
| 141 | (delete-file stderr-file) |
| 142 | (delete-file stdout-file)))) |
| 143 | |
| 144 | ;;;###autoload |
| 145 | (cl-defmacro reformatter-define (name &key program args (mode t) (stdin t) (stdout t) input-file lighter keymap group (exit-code-success-p 'zerop)) |
| 146 | "Define a reformatter command with NAME. |
| 147 | |
| 148 | When called, the reformatter will use PROGRAM and any ARGS to |
| 149 | reformat the current buffer. The contents of the buffer will be |
| 150 | passed as standard input to the reformatter, which should output |
| 151 | them to standard output. A nonzero exit code will be reported as |
| 152 | failure, and the output of the command to standard error will be |
| 153 | displayed to the user. |
| 154 | |
| 155 | The macro accepts the following keyword arguments: |
| 156 | |
| 157 | PROGRAM (required) |
| 158 | |
| 159 | Provides a form which should evaluate to a string at runtime, |
| 160 | e.g. a literal string, or the name of a variable which holds |
| 161 | the program path. |
| 162 | |
| 163 | ARGS |
| 164 | |
| 165 | Command-line arguments for the program. If provided, this is a |
| 166 | form which evaluates to a list of strings at runtime. Default |
| 167 | is the empty list. This form is evaluated at runtime so that |
| 168 | you can use buffer-local variables to influence the args passed |
| 169 | to the reformatter program: the variable `input-file' will be |
| 170 | lexically bound to the path of a file containing the text to be |
| 171 | reformatted: see the keyword options INPUT-FILE, STDIN and |
| 172 | STDOUT for more information. |
| 173 | |
| 174 | STDIN |
| 175 | |
| 176 | When non-nil (the default), the program is passed the input |
| 177 | data on stdin. Set this to nil when your reformatter can only |
| 178 | operate on files in place. In such a case, your ARGS should |
| 179 | include a reference to the `input-file' variable, which will be |
| 180 | bound to an input path when evaluated. |
| 181 | |
| 182 | STDOUT |
| 183 | |
| 184 | When non-nil (the default), the program is expected to write |
| 185 | the reformatted text to stdout. Set this to nil if your |
| 186 | reformatter can only operate on files in place, in which case |
| 187 | the contents of the temporary input file will be used as the |
| 188 | replacement text. |
| 189 | |
| 190 | INPUT-FILE |
| 191 | |
| 192 | Sometimes your reformatter program might expect files to be in |
| 193 | a certain directory or have a certain file extension. This option |
| 194 | lets you handle that. |
| 195 | |
| 196 | If provided, it is a form which will be evaluated before each |
| 197 | run of the formatter, and is expected to return a temporary |
| 198 | file path suitable for holding the region to be reformatted. |
| 199 | It must not produce the same path as the current buffer's file |
| 200 | if that is set: you shouldn't be operating directly on the |
| 201 | buffer's backing file. The temporary input file will be |
| 202 | deleted automatically. You might find the functions |
| 203 | `reformatter-temp-file-in-current-directory' and |
| 204 | `reformatter-temp-file' helpful. |
| 205 | |
| 206 | MODE |
| 207 | |
| 208 | Unless nil, also generate a minor mode that will call the |
| 209 | reformatter command from `before-save-hook' when enabled. |
| 210 | Default is t. |
| 211 | |
| 212 | GROUP |
| 213 | |
| 214 | If provided, this is the custom group used for any generated |
| 215 | modes or custom variables. Don't forget to declare this group |
| 216 | using a `defgroup' form. |
| 217 | |
| 218 | LIGHTER |
| 219 | |
| 220 | If provided, this is a mode lighter string which will be used |
| 221 | for the \"-on-save\" minor mode. It should have a leading |
| 222 | space. The supplied value will be used as the default for a |
| 223 | generated custom variable which specifies the mode lighter. |
| 224 | Default is nil, ie. no lighter. |
| 225 | |
| 226 | KEYMAP |
| 227 | |
| 228 | If provided, this is the symbol name of the \"-on-save\" mode's |
| 229 | keymap, which you must declare yourself. Default is no keymap. |
| 230 | |
| 231 | EXIT-CODE-SUCCESS-P |
| 232 | |
| 233 | If provided, this is a function object callable with `funcall' |
| 234 | which accepts an integer process exit code, and returns non-nil |
| 235 | if that exit code is considered successful. This could be a |
| 236 | lambda, quoted symbol or sharp-quoted symbol. If not supplied, |
| 237 | the code is considered successful if it is `zerop'." |
| 238 | (declare (indent defun)) |
| 239 | (cl-assert (symbolp name)) |
| 240 | (cl-assert (functionp exit-code-success-p)) |
| 241 | (cl-assert program) |
| 242 | ;; Note: we skip using `gensym' here because the macro arguments are only |
| 243 | ;; referred to once below, but this may have to change later. |
| 244 | (let* ((buffer-fn-name (intern (format "%s-buffer" name))) |
| 245 | (region-fn-name (intern (format "%s-region" name))) |
| 246 | (minor-mode-form |
| 247 | (when mode |
| 248 | (let ((on-save-mode-name (intern (format "%s-on-save-mode" name))) |
| 249 | (lighter-name (intern (format "%s-on-save-mode-lighter" name)))) |
| 250 | `(progn |
| 251 | (defcustom ,lighter-name ,lighter |
| 252 | ,(format "Mode lighter for `%s'." on-save-mode-name) |
| 253 | :group ,group |
| 254 | :type 'string) |
| 255 | (define-minor-mode ,on-save-mode-name |
| 256 | ,(format "When enabled, call `%s' when this buffer is saved. |
| 257 | |
| 258 | To enable this unconditionally in a major mode, add this mode |
| 259 | to the major mode's hook. To enable it in specific files or directories, |
| 260 | use the local variables \"mode\" mechanism, e.g. in \".dir-locals.el\" you |
| 261 | might use: |
| 262 | |
| 263 | ((some-major-mode |
| 264 | (mode . %s-on-save))) |
| 265 | " buffer-fn-name name) |
| 266 | :global nil |
| 267 | :lighter ,lighter-name |
| 268 | :keymap ,keymap |
| 269 | :group ,group |
| 270 | (if ,on-save-mode-name |
| 271 | (add-hook 'before-save-hook ',buffer-fn-name nil t) |
| 272 | (remove-hook 'before-save-hook ',buffer-fn-name t)))))))) |
| 273 | `(progn |
| 274 | (defun ,region-fn-name (beg end &optional display-errors) |
| 275 | "Reformats the region from BEG to END. |
| 276 | When called interactively, or with prefix argument |
| 277 | DISPLAY-ERRORS, shows a buffer if the formatting fails." |
| 278 | (interactive "rp") |
| 279 | (let ((input-file ,(if input-file |
| 280 | input-file |
| 281 | `(reformatter--make-temp-file ',name)))) |
| 282 | ;; Evaluate args with input-file bound |
| 283 | (unwind-protect |
| 284 | (progn |
| 285 | (reformatter--do-region |
| 286 | ',name beg end |
| 287 | ,program ,args ,stdin ,stdout input-file |
| 288 | #',exit-code-success-p display-errors)) |
| 289 | (when (file-exists-p input-file) |
| 290 | (delete-file input-file))))) |
| 291 | |
| 292 | (defun ,buffer-fn-name (&optional display-errors) |
| 293 | "Reformats the current buffer. |
| 294 | When called interactively, or with prefix argument |
| 295 | DISPLAY-ERRORS, shows a buffer if the formatting fails." |
| 296 | (interactive "p") |
| 297 | (message "Formatting buffer") |
| 298 | (,region-fn-name (point-min) (point-max) display-errors)) |
| 299 | |
| 300 | ,minor-mode-form))) |
| 301 | |
| 302 | |
| 303 | (defun reformatter-replace-buffer-contents-from-file (file) |
| 304 | "Replace the accessible portion of the current buffer with the contents of FILE." |
| 305 | ;; While the function `replace-buffer-contents' exists in recent |
| 306 | ;; Emacs versions, it exhibits pathologically slow behaviour in many |
| 307 | ;; cases, and the simple replacement approach we use instead is well |
| 308 | ;; proven and typically preserves point and markers to a reasonable |
| 309 | ;; degree. |
| 310 | (insert-file-contents file nil nil nil t)) |
| 311 | |
| 312 | (defun reformatter-temp-file (&optional default-extension) |
| 313 | "Make a temp file re-using the current extension. |
| 314 | If the current file is not backed by a file, then use |
| 315 | DEFAULT-EXTENSION, which should not contain a leading dot. |
| 316 | |
| 317 | The working directory for the command will always be the |
| 318 | `default-directory' of the calling buffer." |
| 319 | (let ((extension (if buffer-file-name |
| 320 | (file-name-extension buffer-file-name) |
| 321 | default-extension))) |
| 322 | (make-temp-file "reformatter" nil |
| 323 | (when extension |
| 324 | (concat "." extension))))) |
| 325 | |
| 326 | (defun reformatter-temp-file-in-current-directory (&optional default-extension) |
| 327 | "Make a temp file in the current directory re-using the current extension. |
| 328 | If the current file is not backed by a file, then use |
| 329 | DEFAULT-EXTENSION, which should not contain a leading dot." |
| 330 | (let ((temporary-file-directory default-directory)) |
| 331 | (reformatter-temp-file default-extension))) |
| 332 | |
| 333 | (provide 'reformatter) |
| 334 | ;;; reformatter.el ends here |