fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 21h ago
334 lines · 14 KB Raw Blame History
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