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:
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:
after-save-hook
, rather than
before-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.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.
To your init-file, add the following form:
(apheleia-global-mode +1)
The autoloading has been configured so that this will not cause Apheleia to be loaded until you save a file.
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:
apheleia-formatters
: Alist mapping names of formatters (symbols
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:
(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:
(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.
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 run isort
and black
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 then
black
on the result of isort
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 if black
is installed.
Warning: If a formatter uses file
(rather than filepath
or input
or none of these keywords), it can't be chained after
another formatter, because file
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
of file
in general.
apheleia-formatter
: Optional buffer-local variable specifying the
formatter to use in this buffer. Overrides apheleia-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 if
apheleia-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 in switch-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 from apheleia-global-mode
. If
one of these returns non-nil then apheleia-mode
is not enabled in
the buffer.
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 disabling apheleia-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:
apheleia-formatters
with how to run it. (See
the docstring of this variable for explanation about the available
keywords.)apheleia-mode-alist
.test/formatters/installers/yourformatter.bash
which
explains how to install the formatter on Ubuntu. This will be used
by CI.make fmt-build FORMATTERS=yourformatter
to do the
installation, then make fmt-docker
to start a shell with the
formatter available. Verify it runs in this environment.test/formatters/samplecode/yourformatter/in.whatever
and
test/formatters/samplecode/yourformatter/out.whatever
.make fmt-test FORMATTERS=yourformatter
from inside the fmt-docker
shell.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.