├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── Makefile ├── README.md ├── reformatter-tests.el └── reformatter.el /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: sanityinc 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | commit-message: 9 | prefix: "chore" 10 | include: "scope" 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | paths-ignore: 7 | - '**.md' 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: purcell/setup-emacs@master 14 | with: 15 | version: 29.1 16 | - uses: actions/checkout@v4 17 | - name: Run tests 18 | run: make package-lint 19 | 20 | build: 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | emacs_version: 25 | - 24.3 26 | - 24.5 27 | - 25.1 28 | - 25.3 29 | - 26.1 30 | - 26.3 31 | - 27.1 32 | - 27.2 33 | - 28.1 34 | - 28.2 35 | - 29.1 36 | - snapshot 37 | steps: 38 | - uses: cachix/install-nix-action@v31 39 | with: 40 | nix_path: nixpkgs=channel:nixos-unstable 41 | - uses: purcell/setup-emacs@master 42 | with: 43 | version: ${{ matrix.emacs_version }} 44 | - uses: actions/checkout@v4 45 | - name: Install deps for tests 46 | run: nix profile install 'nixpkgs#shfmt' 47 | - name: Run tests 48 | run: make compile test 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.elc 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EMACS ?= emacs 2 | 3 | # A space-separated list of required package names 4 | DEPS = 5 | 6 | INIT_PACKAGES="(progn \ 7 | (require 'package) \ 8 | (push '(\"melpa\" . \"https://melpa.org/packages/\") package-archives) \ 9 | (package-initialize) \ 10 | (dolist (pkg '(PACKAGES)) \ 11 | (unless (package-installed-p pkg) \ 12 | (unless (assoc pkg package-archive-contents) \ 13 | (package-refresh-contents)) \ 14 | (package-install pkg))) \ 15 | )" 16 | 17 | all: compile package-lint test clean-elc 18 | 19 | package-lint: 20 | ${EMACS} -Q --eval $(subst PACKAGES,package-lint,${INIT_PACKAGES}) -batch -f package-lint-batch-and-exit reformatter.el 21 | 22 | test: 23 | ${EMACS} -Q --eval $(subst PACKAGES,${DEPS},${INIT_PACKAGES}) -batch -l reformatter.el -l reformatter-tests.el -f ert-run-tests-batch-and-exit 24 | 25 | compile: clean-elc 26 | ${EMACS} -Q --eval $(subst PACKAGES,${DEPS},${INIT_PACKAGES}) -L . -batch -f batch-byte-compile *.el 27 | 28 | clean-elc: 29 | rm -f f.elc 30 | 31 | .PHONY: all compile clean-elc package-lint 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Melpa Status](http://melpa.org/packages/reformatter-badge.svg)](http://melpa.org/#/reformatter) 2 | [![Melpa Stable Status](http://stable.melpa.org/packages/reformatter-badge.svg)](http://stable.melpa.org/#/reformatter) 3 | [![NonGNU ELPA](https://elpa.nongnu.org/nongnu/reformatter.svg)](https://elpa.nongnu.org/nongnu/reformatter.html) 4 | [![Build Status](https://github.com/purcell/emacs-reformatter/actions/workflows/test.yml/badge.svg)](https://github.com/purcell/emacs-reformatter/actions/workflows/test.yml) 5 | Support me 6 | 7 | # Define commands which run reformatters on the current Emacs buffer 8 | 9 | This library lets elisp authors easily define an idiomatic command to 10 | reformat the current buffer using a command-line program, together 11 | with an optional minor mode which can apply this command automatically 12 | on save. 13 | 14 | By default, reformatter.el expects programs to read from stdin and 15 | write to stdout, and you should prefer this mode of operation where 16 | possible. If this isn't possible with your particular formatting 17 | program, refer to the options for `reformatter-define`, and see the 18 | examples in the package's tests. 19 | 20 | In its initial release it supports only reformatters which can read 21 | from stdin and write to stdout, but a more versatile interface will 22 | be provided as development continues. 23 | 24 | As an example, let's define a reformat command that applies the "dhall 25 | format" command. We'll assume here that we've already defined a 26 | variable `dhall-command` which holds the string name or path of the 27 | dhall executable: 28 | 29 | ```el 30 | (reformatter-define dhall-format 31 | :program dhall-command 32 | :args '("format") 33 | :lighter " DF") 34 | ``` 35 | 36 | The `reformatter-define` macro expands to code which generates 37 | `dhall-format-buffer` and `dhall-format-region` interactive commands, 38 | and a local minor mode called `dhall-format-on-save-mode`. The `:args` 39 | and `:program` expressions will be evaluated at runtime, so they can 40 | refer to variables that may (later) have a buffer-local value. A 41 | custom variable will be generated for the mode lighter, with the 42 | supplied value becoming the default. 43 | 44 | The generated minor mode allows idiomatic per-directory or per-file 45 | customisation, via the "modes" support baked into Emacs' file-local 46 | and directory-local variables mechanisms. For example, users of the 47 | above example might add the following to a project-specific 48 | `.dir-locals.el` file: 49 | 50 | ```el 51 | ((dhall-mode 52 | (mode . dhall-format-on-save))) 53 | ``` 54 | 55 | See the documentation for `reformatter-define`, which provides a 56 | number of options for customising the generated code. 57 | 58 | Library authors might like to provide autoloads for the generated 59 | code, e.g.: 60 | 61 | ```el 62 | ;;;###autoload (autoload 'dhall-format-buffer "current-file" nil t) 63 | ;;;###autoload (autoload 'dhall-format-region "current-file" nil t) 64 | ;;;###autoload (autoload 'dhall-format-on-save-mode "current-file" nil t) 65 | ``` 66 | 67 | ## Examples of usage in the wild 68 | 69 | To find reverse dependencies, look for "Needed by" on the [MELPA page 70 | for reformatter](https://melpa.org/#/reformatter). Here are some 71 | specific examples: 72 | 73 | * [dhall-mode.el](https://github.com/psibi/dhall-mode/blob/master/dhall-mode.el) 74 | * [elm-format.el](https://github.com/jcollard/elm-mode/blob/master/elm-format.el), in `elm-mode` 75 | * [sqlformat.el](https://github.com/purcell/sqlformat/blob/master/sqlformat.el) 76 | * [Here](https://github.com/purcell/emacs.d/blob/14f645a9bde04498ce2b60de268c2cbafa13604a/lisp/init-purescript.el#L18-L19) is the author defining a reformatter in his own configuration 77 | 78 | ## Rationale 79 | 80 | I contribute to a number of Emacs programming language modes and 81 | tools, and increasingly use code reformatters in my daily work. It's 82 | surprisingly difficult to write robust, correct code to apply these 83 | reformatters, given that it must consider such issues as: 84 | 85 | * Missing programs 86 | * Buffers not yet saved to a file 87 | * Displaying error output 88 | * Colorising ANSI escape sequences in any error output 89 | * Handling file encodings correctly 90 | 91 | With this library, I hope to help the community standardise on best 92 | practices, and make things easier for tool authors and end users 93 | alike. 94 | 95 | ## FAQ 96 | 97 | ### How is this different from [format-all.el](https://github.com/lassik/emacs-format-all-the-code)? 98 | 99 | `format-all` is a very different approach: it aims to provide a single 100 | minor mode which you then enable and configure to do the right thing 101 | (including nothing) for all the languages you use. It even tries to 102 | tell you how to install missing programs. It's an interesting project, 103 | but IMO it's hard to design the configuration for such a grand unified 104 | approach, and it can get complex. For example, you'd have to be able 105 | to configure which of two possible reformatters you want to use for a 106 | specific language, and to be able to do that on a per-project basis. 107 | 108 | In contrast reformatter produces small, self-contained and separate 109 | formatters and minor modes which all work consistently and are 110 | individually configured. It makes it possible to replace existing 111 | formatter code, and it's also very convenient for users to define 112 | their own ad-hoc reformatter wrappers 113 | 114 | ## Installation 115 | 116 | ### Manual 117 | 118 | Ensure `reformatter.el` is in a directory on your load-path, and add 119 | the following to your `~/.emacs` or `~/.emacs.d/init.el`: 120 | 121 | ```elisp 122 | (require 'reformatter) 123 | ``` 124 | 125 | ### MELPA 126 | 127 | If you're an Emacs 24 user or you have a recent version of 128 | `package.el` you can install `reformatter` from the 129 | [MELPA](http://melpa.org) repository. The version of 130 | `reformatter` there will always be up-to-date. 131 | 132 | ## About 133 | 134 | Author: Steve Purcell 135 | 136 | Homepage: https://github.com/purcell/emacs-reformatter 137 | 138 |
139 | 140 | [💝 Support this project and my other Open Source work](https://www.patreon.com/sanityinc) 141 | 142 | [💼 LinkedIn profile](https://uk.linkedin.com/in/stevepurcell) 143 | 144 | [✍ sanityinc.com](https://www.sanityinc.com/) 145 | -------------------------------------------------------------------------------- /reformatter-tests.el: -------------------------------------------------------------------------------- 1 | ;;; reformatter-tests.el --- Test suite for reformatter -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2020 Steve Purcell 4 | 5 | ;; Author: Steve Purcell 6 | ;; Keywords: 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; Just a few basic regression tests 24 | 25 | ;;; Code: 26 | 27 | (require 'reformatter) 28 | (require 'ert) 29 | 30 | (defgroup reformatter-tests nil "Reformatter tests" :group 'test) 31 | 32 | ;; We use `shfmt' because it can operate in a few modes 33 | 34 | ;; Pure stdin/stdout 35 | (reformatter-define reformatter-tests-shfmt-stdio 36 | :program "shfmt" 37 | :args nil 38 | :mode nil) 39 | 40 | (ert-deftest reformatter-tests-pure-stdio-no-args () 41 | (with-temp-buffer 42 | (insert "[ foo ] && echo yes\n") 43 | (reformatter-tests-shfmt-stdio-buffer) 44 | (should (equal "[ foo ] && echo yes\n" (buffer-string))))) 45 | 46 | ;; Read from stdin/stdout 47 | (reformatter-define reformatter-tests-shfmt-tempfile-in-stdout 48 | :program "shfmt" 49 | :stdin nil 50 | :args (list input-file)) 51 | 52 | (ert-deftest reformatter-tests-tempfile-in-stdout () 53 | (with-temp-buffer 54 | (insert "[ foo ] && echo yes\n") 55 | (reformatter-tests-shfmt-tempfile-in-stdout-buffer) 56 | (should (equal "[ foo ] && echo yes\n" (buffer-string))))) 57 | 58 | ;; Same as `reformatter-tests-shfmt-tempfile-in-stdout', but with a 59 | ;; slash in the symbol name. 60 | (reformatter-define reformatter-tests-tempfile/with-slash-in-symbol-name 61 | :program "shfmt" 62 | :stdin nil 63 | :args (list input-file)) 64 | 65 | (ert-deftest reformatter-tests-tempfile-with-slash-in-symbol-name () 66 | (with-temp-buffer 67 | (insert "[ foo ] && echo yes\n") 68 | (reformatter-tests-tempfile/with-slash-in-symbol-name-buffer) 69 | (should (equal "[ foo ] && echo yes\n" (buffer-string))))) 70 | 71 | ;; Modify a file in place 72 | (reformatter-define reformatter-tests-shfmt-in-place 73 | :program "shfmt" 74 | :stdin nil 75 | :stdout nil 76 | :args (list "-w" input-file)) 77 | 78 | (ert-deftest reformatter-tests-tempfile-in-place () 79 | (with-temp-buffer 80 | (insert "[ foo ] && echo yes\n") 81 | (reformatter-tests-shfmt-in-place-buffer) 82 | (should (equal "[ foo ] && echo yes\n" (buffer-string))))) 83 | 84 | ;; Formatting commands tagged for specific modes: `command-modes' checks which 85 | ;; modes they're defined to be interactively usable in, but it's only available 86 | ;; in Emacs 28 and newer. 87 | (when (fboundp 'command-modes) 88 | (reformatter-define reformatter-tests-shfmt-no-interactive-modes 89 | :program "shfmt") 90 | 91 | (ert-deftest reformatter-tests-no-interactive-modes () 92 | (should (not (command-modes 'reformatter-tests-shfmt-no-interactive-modes-buffer))) 93 | (should (not (command-modes 'reformatter-tests-shfmt-no-interactive-modes-region)))) 94 | 95 | (reformatter-define reformatter-tests-shfmt-single-interactive-mode 96 | :program "shfmt" 97 | :interactive-modes (sh-mode)) 98 | 99 | (ert-deftest reformatter-tests-single-interactive-mode () 100 | (should (equal (command-modes 'reformatter-tests-shfmt-single-interactive-mode-buffer) 101 | '(sh-mode))) 102 | (should (equal (command-modes 'reformatter-tests-shfmt-single-interactive-mode-region) 103 | '(sh-mode)))) 104 | 105 | (reformatter-define reformatter-tests-shfmt-multiple-interactive-modes 106 | :program "shfmt" 107 | :interactive-modes (sh-mode haskell-mode)) 108 | 109 | (ert-deftest reformatter-tests-multiple-interactive-modes () 110 | (should (equal (command-modes 'reformatter-tests-shfmt-multiple-interactive-modes-buffer) 111 | '(sh-mode haskell-mode))) 112 | (should (equal (command-modes 'reformatter-tests-shfmt-multiple-interactive-modes-region) 113 | '(sh-mode haskell-mode))))) 114 | 115 | 116 | (provide 'reformatter-tests) 117 | ;;; reformatter-tests.el ends here 118 | -------------------------------------------------------------------------------- /reformatter.el: -------------------------------------------------------------------------------- 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 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 . 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 &optional working-directory) 87 | "Do the work of reformatter called NAME. 88 | Reformats the current buffer's region from BEG to END using PROGRAM and 89 | ARGS. When DISPLAY-ERRORS is non-nil, shows a buffer if the formatting 90 | fails. For args STDIN, STDOUT, INPUT-FILE, EXIT-CODE-SUCCESS-P and 91 | WORKING-DIRECTORY see the documentation of 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 | (default-directory (or working-directory default-directory))) 108 | (unwind-protect 109 | (progn 110 | (write-region beg end input-file nil :quiet) 111 | (let* ((error-buffer (get-buffer-create (format "*%s errors*" name))) 112 | (retcode 113 | (condition-case e 114 | (apply 'call-process program 115 | (when stdin input-file) 116 | (list (list :file stdout-file) stderr-file) 117 | nil 118 | args) 119 | (error e)))) 120 | (with-current-buffer error-buffer 121 | (let ((inhibit-read-only t)) 122 | (insert-file-contents stderr-file nil nil nil t) 123 | (unless (integerp retcode) 124 | (insert (error-message-string retcode))) 125 | (ansi-color-apply-on-region (point-min) (point-max))) 126 | (special-mode)) 127 | (if (and (integerp retcode) (funcall exit-code-success-p retcode)) 128 | (progn 129 | (save-restriction 130 | ;; This replacement method minimises 131 | ;; disruption to marker positions and the 132 | ;; undo list 133 | (narrow-to-region beg end) 134 | (reformatter-replace-buffer-contents-from-file (if stdout 135 | stdout-file 136 | input-file))) 137 | ;; If there are no errors then we hide the error buffer 138 | (delete-windows-on error-buffer)) 139 | (if display-errors 140 | (display-buffer error-buffer) 141 | (message (concat (symbol-name name) " failed: see %s") (buffer-name error-buffer)))))) 142 | (delete-file stderr-file) 143 | (delete-file stdout-file)))) 144 | 145 | ;;;###autoload 146 | (cl-defmacro reformatter-define (name &key program args (mode t) (stdin t) (stdout t) input-file lighter keymap group (exit-code-success-p 'zerop) working-directory interactive-modes) 147 | "Define a reformatter command with NAME. 148 | 149 | When called, the reformatter will use PROGRAM and any ARGS to 150 | reformat the current buffer. The contents of the buffer will be 151 | passed as standard input to the reformatter, which should output 152 | them to standard output. A nonzero exit code will be reported as 153 | failure, and the output of the command to standard error will be 154 | displayed to the user. 155 | 156 | The macro accepts the following keyword arguments: 157 | 158 | PROGRAM (required) 159 | 160 | Provides a form which should evaluate to a string at runtime, 161 | e.g. a literal string, or the name of a variable which holds 162 | the program path. 163 | 164 | ARGS 165 | 166 | Command-line arguments for the program. If provided, this is a 167 | form which evaluates to a list of strings at runtime. Default 168 | is the empty list. This form is evaluated at runtime so that 169 | you can use buffer-local variables to influence the args passed 170 | to the reformatter program: the variable `input-file' will be 171 | lexically bound to the path of a file containing the text to be 172 | reformatted: see the keyword options INPUT-FILE, STDIN and 173 | STDOUT for more information. 174 | 175 | STDIN 176 | 177 | When non-nil (the default), the program is passed the input 178 | data on stdin. Set this to nil when your reformatter can only 179 | operate on files in place. In such a case, your ARGS should 180 | include a reference to the `input-file' variable, which will be 181 | bound to an input path when evaluated. 182 | 183 | STDOUT 184 | 185 | When non-nil (the default), the program is expected to write 186 | the reformatted text to stdout. Set this to nil if your 187 | reformatter can only operate on files in place, in which case 188 | the contents of the temporary input file will be used as the 189 | replacement text. 190 | 191 | INPUT-FILE 192 | 193 | Sometimes your reformatter program might expect files to be in 194 | a certain directory or have a certain file extension. This option 195 | lets you handle that. 196 | 197 | If provided, it is a form which will be evaluated before each 198 | run of the formatter, and is expected to return a temporary 199 | file path suitable for holding the region to be reformatted. 200 | It must not produce the same path as the current buffer's file 201 | if that is set: you shouldn't be operating directly on the 202 | buffer's backing file. The temporary input file will be 203 | deleted automatically. You might find the functions 204 | `reformatter-temp-file-in-current-directory' and 205 | `reformatter-temp-file' helpful. 206 | 207 | MODE 208 | 209 | Unless nil, also generate a minor mode that will call the 210 | reformatter command from `before-save-hook' when enabled. 211 | Default is t. 212 | 213 | GROUP 214 | 215 | If provided, this is the custom group used for any generated 216 | modes or custom variables. Don't forget to declare this group 217 | using a `defgroup' form. 218 | 219 | LIGHTER 220 | 221 | If provided, this is a mode lighter string which will be used 222 | for the \"-on-save\" minor mode. It should have a leading 223 | space. The supplied value will be used as the default for a 224 | generated custom variable which specifies the mode lighter. 225 | Default is nil, ie. no lighter. 226 | 227 | KEYMAP 228 | 229 | If provided, this is the symbol name of the \"-on-save\" mode's 230 | keymap, which you must declare yourself. Default is no keymap. 231 | 232 | EXIT-CODE-SUCCESS-P 233 | 234 | If provided, this is a function object callable with `funcall' 235 | which accepts an integer process exit code, and returns non-nil 236 | if that exit code is considered successful. This could be a 237 | lambda, quoted symbol or sharp-quoted symbol. If not supplied, 238 | the code is considered successful if it is `zerop'. 239 | 240 | WORKING-DIRECTORY 241 | 242 | Directory where your reformatter program is started. If provided, this 243 | should be a form that evaluates to a string at runtime. Default is the 244 | value of `default-directory' in the buffer. 245 | 246 | INTERACTIVE-MODES 247 | 248 | If provided, this is a list of mode names (as unquoted 249 | symbols). The created commands for formatting regions and 250 | buffers are then tagged for interactive use in these modes, 251 | making them compatible with some built-in predicate functions 252 | for `read-extended-command-predicate', like 253 | `command-completion-default-include-p'." 254 | (declare (indent defun)) 255 | (cl-assert (symbolp name)) 256 | (cl-assert (functionp exit-code-success-p)) 257 | (cl-assert program) 258 | ;; Note: we skip using `gensym' here because the macro arguments are only 259 | ;; referred to once below, but this may have to change later. 260 | (let* ((buffer-fn-name (intern (format "%s-buffer" name))) 261 | (region-fn-name (intern (format "%s-region" name))) 262 | (minor-mode-form 263 | (when mode 264 | (let ((on-save-mode-name (intern (format "%s-on-save-mode" name))) 265 | (lighter-name (intern (format "%s-on-save-mode-lighter" name)))) 266 | `(progn 267 | (defcustom ,lighter-name ,lighter 268 | ,(format "Mode lighter for `%s'." on-save-mode-name) 269 | :group ,group 270 | :type 'string) 271 | (define-minor-mode ,on-save-mode-name 272 | ,(format "When enabled, call `%s' when this buffer is saved. 273 | 274 | To enable this unconditionally in a major mode, add this mode 275 | to the major mode's hook. To enable it in specific files or directories, 276 | use the local variables \"mode\" mechanism, e.g. in \".dir-locals.el\" you 277 | might use: 278 | 279 | ((some-major-mode 280 | (mode . %s-on-save))) 281 | " buffer-fn-name name) 282 | :global nil 283 | :lighter ,lighter-name 284 | :keymap ,keymap 285 | :group ,group 286 | (if ,on-save-mode-name 287 | (add-hook 'before-save-hook ',buffer-fn-name nil t) 288 | (remove-hook 'before-save-hook ',buffer-fn-name t)))))))) 289 | `(progn 290 | (defun ,region-fn-name (beg end &optional display-errors) 291 | "Reformats the region from BEG to END. 292 | When called interactively, or with prefix argument 293 | DISPLAY-ERRORS, shows a buffer if the formatting fails." 294 | (interactive "rp" ,@interactive-modes) 295 | (let ((input-file ,(if input-file 296 | input-file 297 | `(reformatter--make-temp-file ',name)))) 298 | ;; Evaluate args with input-file bound 299 | (unwind-protect 300 | (progn 301 | (reformatter--do-region 302 | ',name beg end 303 | ,program ,args ,stdin ,stdout input-file 304 | #',exit-code-success-p display-errors ,working-directory)) 305 | (when (file-exists-p input-file) 306 | (delete-file input-file))))) 307 | 308 | (defun ,buffer-fn-name (&optional display-errors) 309 | "Reformats the current buffer. 310 | When called interactively, or with prefix argument 311 | DISPLAY-ERRORS, shows a buffer if the formatting fails." 312 | (interactive "p" ,@interactive-modes) 313 | (message "Formatting buffer") 314 | (,region-fn-name (point-min) (point-max) display-errors)) 315 | 316 | ,minor-mode-form))) 317 | 318 | 319 | (defun reformatter-replace-buffer-contents-from-file (file) 320 | "Replace the accessible portion of the current buffer with the contents of FILE." 321 | ;; While the function `replace-buffer-contents' exists in recent 322 | ;; Emacs versions, it exhibits pathologically slow behaviour in many 323 | ;; cases, and the simple replacement approach we use instead is well 324 | ;; proven and typically preserves point and markers to a reasonable 325 | ;; degree. 326 | (insert-file-contents file nil nil nil t)) 327 | 328 | (defun reformatter-temp-file (&optional default-extension) 329 | "Make a temp file re-using the current extension. 330 | If the current file is not backed by a file, then use 331 | DEFAULT-EXTENSION, which should not contain a leading dot. 332 | 333 | The working directory for the command will always be the 334 | `default-directory' of the calling buffer." 335 | (let ((extension (if buffer-file-name 336 | (file-name-extension buffer-file-name) 337 | default-extension))) 338 | (make-temp-file "reformatter" nil 339 | (when extension 340 | (concat "." extension))))) 341 | 342 | (defun reformatter-temp-file-in-current-directory (&optional default-extension) 343 | "Make a temp file in the current directory re-using the current extension. 344 | If the current file is not backed by a file, then use 345 | DEFAULT-EXTENSION, which should not contain a leading dot." 346 | (let ((temporary-file-directory default-directory)) 347 | (reformatter-temp-file default-extension))) 348 | 349 | (provide 'reformatter) 350 | ;;; reformatter.el ends here 351 | --------------------------------------------------------------------------------