Good code is automatically formatted by tools like Black or Prettier so that you and your team spend less time on formatting and more time on building features. It's best if your editor can run code formatters each time you save a file, so that you don't have to look at badly formatted code or get surprised when things change just before you commit. However, running a code formatter on save suffers from the following two problems:
- It takes some time (e.g. around 200ms for Black on an empty file), which makes the editor feel less responsive.
- It invariably moves your cursor (point) somewhere unexpected if the changes made by the code formatter are too close to point's position.
Apheleia is an Emacs package which solves both of these problems comprehensively for all languages, allowing you to say goodbye to language-specific packages such as Blacken and prettier-js.
The approach is as follows:
- Run code formatters on
after-save-hook
, rather thanbefore-save-hook
, and do so asynchronously. Once the formatter has finished running, check if the buffer has been modified since it started; only apply the changes if not. - After running the code formatter, generate an RCS patch showing the changes and then apply it to the buffer. This prevents changes elsewhere in the buffer from moving point. If a patch region happens to include point, then use a dynamic programming algorithm for string alignment to determine where point should be moved so that it remains in the same place relative to its surroundings. Finally, if the vertical position of point relative to the window has changed, adjust the scroll position to maintain maximum visual continuity. (This includes iterating through all windows displaying the buffer, if there are more than one.) The dynamic programming algorithm runs in quadratic time, which is why it is only applied if necessary and to a single patch region.
Apheleia is available on MELPA. It is easiest
to install it using
straight.el
:
(straight-use-package 'apheleia)
However, you may install using any other package manager if you prefer.
Emacs 27 or later is supported. Apheleia does not include any
formatters. You must install any formatter separately that you wish to
use. As long as it is on $PATH
then Apheleia will pick it up
automatically; missing formatters will silently be skipped, but errors
from invoking installed formatters will be reported on buffer save.
It is recommended to have Bash installed, as this is used as a dependency for Apheleia to invoke certain formatters (e.g. Node.js-based formatters).
Windows support is not guaranteed due to lack of support for common open standards on this platform. Pull requests adjusting Apheleia for improved cross-platform portability will be accepted, but no guarantees are made about stability on Windows.
The simplest way to to enable Apheleia globally, is to add the following to your user-init-file
.
(apheleia-global-mode +1)
By default, Apheleia is configured to format with
Black,
Prettier, and
Gofmt on save in all relevant major
modes. To configure this, you can adjust the values of the following
variables:
Using customize
interactively will remember preferences in
custom-file
. If you prefer to organize preferences in
user-init-file
, a use-package
is helpful (and also provides
dependency management, automatic installation, and lazy loading). The
following code will enable Apheleia globally to use the popular
formatters in apheleia-formatters
if associated with popular modes
in apheleia-mode-alist
.
apheleia-formatters
: Alist mapping names of formatters (symbols
(use-package apheleia
:ensure t
:demand t
:config
(apheleia-global-mode)
)
Apheleia includes support for many modes and formatters, but not all of them are enabled by default. This example shows how to enable a popular formatter for a less popular mode:
(use-package apheleia
:ensure t
:demand t
:config
(apheleia-global-mode)
;; Enable less popular 'markdown-mode' to use its popular formatter
;; ('prettier-markdown'), as defined in 'apheleia-formatters'.
(add-to-list 'apheleia-mode-alist
'(markdown-mode . prettier-markdown))
)
This example shows how to define a custom formatter prettier-markdown-with-newer-indent
that uses npx(1)
to run prettier(1)
and associate it with markdown-mode
. The formatter was copied from a standard formatter (prettier-markdown
defined in apheleia-mode-alist
), then modified to replaces the obsolete apheleia-formatters-js-indent
function with the newer apheleia-formatters-indent
function to avoid warnings:
(use-package apheleia
:ensure t
:demand t
:config
(apheleia-global-mode)
;; Define new formatter based on popular formatter
;; `prettier-markdown`, but replacing obsolescent
;; `apheleia-formatters-js-indent` with `apheleia-formatter-indent`.
(add-to-list 'apheleia-formatters
`(prettier-markdown-with-newer-indent . ("apheleia-npx" "prettier"
"--stdin-filepath" filepath
"--parser=markdown"
(apheleia-formatters-indent "--use-tabs" "--tab-width"))))
;; Enable less popular mode ('markdown') to use new formatter
(add-to-list 'apheleia-mode-alist
'(markdown-mode . prettier-markdown-with-newer-indent))
)
Apheleia is configured to load lazily, meaning it will not be loaded
until you save a file. Note: This deferred loading behavior may trigger
this warning in some LSP systems (e.g., eslint
):
The function ‘apheleia-global-mode’ might not be defined at runtime.
This warning is expected and does not affect functionality.
By default, Apheleia is configured to format files on save using popular formatters such as:
like black
and prettier
) to commands used to run those
formatters (such as ("black" "-")
and (npx "prettier" input)
).
See the docstring for more information.
You can manipulate this alist using standard Emacs functions.
* For example, to add some command-line options to Black, you could use:
```elisp
(setf (alist-get 'black apheleia-formatters)
'("black" "--option" "..." "-"))
```
* There are a list of symbols that are interpreted by apheleia
specially when formatting a command (example: `npx`). Any
non-string entries in a formatter that doesn't equal one of
these symbols is evaluated and replaced in place. This can be
used to pass certain flags to the formatter process depending on
the state of the current buffer. For example:
```elisp
(push '(shfmt . ("beautysh"
"-filename" filepath
(when-let ((indent (bound-and-true-p sh-basic-offset)))
(list "--indent-size" (number-to-string indent)))
(when indent-tabs-mode "--tab")
"-"))
apheleia-formatters)
```
This adds an entry to `apheleia-formatters` for the `beautysh`
formatter. The evaluated entries makes it so that the `--tab`
flag is only passed to `beautysh` when the value of
`indent-tabs-mode` is true. Similarly the indent-size flag is
passed the exact value of the `sh-basic-offset` variable
only when it is bound. Observe that one of these evaluations
returns a list of flags whereas the other returns a single
string. These are substituted into the command as you'd expect.
* You can also use Apheleia to format buffers that have no underlying
files. In this case the value of `file` and `filepath` will be
the name of the current buffer with any special characters for
the file-system (such as `*` on windows) being stripped out.
This is also how the extension for any temporary files apheleia
might create will be determined. If you're using a formatter
that determines the file-type from the extension you should name
such buffers such that their suffixed with the extension. For
example a buffer called `*foo-bar.c*` that has no associated
file will have an implicit file-name of `foo-bar.c` and any
temporary files will be suffixed with a `.c` extension.
* You can implement formatters as arbitrary Elisp functions which
operate directly on a buffer, without needing to invoke an
external command. This can be useful to integrate with e.g.
language servers. See the docstring for more information on the
expected interface for Elisp formatters.
-
apheleia-mode-alist
: Alist mapping major modes and filename regexps to names of formatters to use in those modes and files. See the docstring for more information.-
You can use this variable to configure multiple formatters for the same buffer by setting the
cdr
of an entry to a list of formatters to run instead of a single formatter. For example you may want to runisort
andblack
one after the other.(setf (alist-get 'isort apheleia-formatters) '("isort" "--stdout" "-")) (setf (alist-get 'python-mode apheleia-mode-alist) '(isort black))
This will make apheleia run
isort
on the current buffer and thenblack
on the result ofisort
and then use the final output to format the current buffer.Warning: At the moment there's no smart or configurable error handling in place. This means if one of the configured formatters fail (for example if
isort
isn't installed) then apheleia just doesn't format the buffer at all, even ifblack
is installed.Warning: If a formatter uses
file
(rather thanfilepath
orinput
or none of these keywords), it can't be chained after another formatter, becausefile
implies that the formatter must read from the original file, not an intermediate temporary file. For this reason it's suggested to avoid the use offile
in general.
-
-
apheleia-formatter
: Optional buffer-local variable specifying the formatter to use in this buffer. Overridesapheleia-mode-alist
. You can set this in a local variables list, or in.dir-locals.el
(e.g.((python-mode . ((apheleia-formatter . (isort black)))))
), or in a custom hook of your own that sets the local variable conditionally. -
apheleia-inhibit
: Optional buffer-local variable, if set to non-nil then Apheleia does not turn on automatically even ifapheleia-global-mode
is on.
You can run M-x apheleia-mode
to toggle automatic formatting on save
in a single buffer, or M-x apheleia-global-mode
to toggle the
default setting for all buffers. Also, even if apheleia-mode
is not
enabled, you can run M-x apheleia-format-buffer
to manually invoke
the configured formatter for the current buffer. Running with a prefix
argument will cause the command to prompt you for which formatter to
run.
Apheleia does not currently support TRAMP, and is therefore automatically disabled for remote files.
If an error occurs while formatting, a message is displayed in the
echo area. You can jump to the error by invoking M-x apheleia-goto-error
, or manually switch to the log buffer mentioned
in the message.
You can configure error reporting using the following user options:
apheleia-hide-log-buffers
: By default, errors from formatters are put in buffers named like*apheleia-cmdname-log*
. If you customize this user option to non-nil then a space is prepended to the names of these buffers, hiding them by default inswitch-to-buffer
(you must type a space to see them).apheleia-log-only-errors
: By default, only failed formatter runs are logged. If you customize this user option to nil then all runs are logged, along with whether or not they succeeded. This could be helpful in debugging.
The following user options are also available:
apheleia-post-format-hook
: Normal hook run after Apheleia formats a buffer. Run if the formatting is successful, even when no changes are made to the buffer.apheleia-max-alignment-size
: The maximum number of characters that a diff region can have to be processed using Apheleia's dynamic programming algorithm for point alignment. This cannot be too big or Emacs will hang noticeably on large reformatting operations, since the DP algorithm is quadratic-time.apheleia-mode-lighter
:apheleia-mode
lighter displayed in the mode-line. If you don't want to display it, use nil. Otherwise, its value must be a string.
Apheleia exposes some hooks for advanced customization:
-
apheleia-formatter-exited-hook
: Abnormal hook which is run after a formatter has completely finished running for a buffer. Not run if the formatting was interrupted and no action was taken. Receives two arguments: the symbol for the formatter that was run (e.g.black
, or it could be a list if multiple formatters were run in a chain), and a boolean for whether there was an error. -
apheleia-inhibit-functions
: List of functions to run before turning on Apheleia automatically fromapheleia-global-mode
. If one of these returns non-nil thenapheleia-mode
is not enabled in the buffer. -
apheleia-skip-functions
: List of functions to run before each Apheleia formatter invocation. If one of these returns non-nil then the formatter is not run, even ifapheleia-mode
is enabled.
There is no configuration interface in Apheleia for formatter
behavior. The way to configure a formatter is by editing a standard
config file that it reads (e.g. .prettierrc.json
), or setting an
environment variable that it reads, or by changing the entry in
apheleia-formatters
to customize the command-line arguments.
There is one exception to this, which is that Apheleia's default command-line arguments for the built-in formatters will automatically check Emacs' indentation options for the corresponding major mode, and pass that information to the formatter. This way, the indentation (tabs vs spaces, and how many) applied by the formatter will match what electric indentation in Emacs is doing, preventing a shuffle back and forth as you type.
This behavior can be disabled by setting
apheleia-formatters-respect-indent-level
to nil.
Try running your formatter outside of Emacs to verify it works there.
Check what command-line options it is configured with in
apheleia-formatters
.
To debug internal bugs, race conditions, or performance issues, try
setting apheleia-log-debug-info
to non-nil and check the contents of
*apheleia-debug-log*
. It will have detailed trace information about
most operations performed by Apheleia.
process aphelieia-whatever no longer connected to pipe; closed it
: This happens on older Emacs versions when formatting a buffer with size greater than 65,536 characters. There is no known workaround besides disablingapheleia-mode
for the affected buffer, or upgrading to a more recent version of Emacs. See #20.
Please see the contributor guide for my projects for general information, and the following sections for Apheleia-specific details.
There's also a wiki that could do with additions/clarity. Any improvement suggestions should be submitted as an issue.
I have done my best to make it straightforward to add a formatter. You just follow these steps:
- Install your formatter on your machine so you can test.
- Create an entry in
apheleia-formatters
with how to run it. (See the docstring of this variable for explanation about the available keywords.) - Add entries for the relevant major modes in
apheleia-mode-alist
. - See if it works for you!
- Add a file at
test/formatters/installers/yourformatter.bash
which explains how to install the formatter on Ubuntu. This will be used by CI. - Test with
make fmt-build FORMATTERS=yourformatter
to do the installation, thenmake fmt-docker
to start a shell with the formatter available. Verify it runs in this environment. - Add an example input (pre-formatting) and output (post-formatting)
file at
test/formatters/samplecode/yourformatter/in.whatever
andtest/formatters/samplecode/yourformatter/out.whatever
. - Verify that the tests are passing, using
make fmt-test FORMATTERS=yourformatter
from inside thefmt-docker
shell. - Submit a pull request, CI should now be passing!
I got the idea for using RCS patches to avoid moving point too much from prettier-js, although that package does not implement the dynamic programming algorithm which Apheleia uses to guarantee stability of point even within a formatted region.
Note that despite this inspiration, Apheleia is a clean-room implementation which is free of the copyright terms of prettier-js.