Apheleia
🌷 Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.
Install / Use
/learn @radian-software/ApheleiaREADME
Apheleia
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.
Installation
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.
Dependencies
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.
User guide
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 likeblackandprettier) 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-formattersfor thebeautyshformatter. The evaluated entries makes it so that the--tabflag is only passed tobeautyshwhen the value ofindent-tabs-modeis true. Similarly the indent-size flag is passed the exact value of thesh-basic-offsetvariable 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
fileandfilepathwill 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 offoo-bar.cand any temporary files will be suffixed with a.cextension. -
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
cdrof an entry to a list of formatters to run instead of a single formatter. For example you may want to runisortandblackone 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
isorton the current buffer and thenblackon the result ofisortand 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
isortisn't installed) then apheleia just doesn't format the buffer at all, even ifblackis installed.Warning: If a formatter uses
file(rather thanfilepathorinputor none of these keywords), it can't be chained after another formatter, becausefileimplies that the formatter must read from the original file, not an intermediate temporary file. For this reason it's suggested to avoid the use offilein 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-modeis 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
Related Skills
docs-writer
99.6k`docs-writer` skill instructions As an expert technical writer and editor for the Gemini CLI project, you produce accurate, clear, and consistent documentation. When asked to write, edit, or revie
model-usage
342.0kUse CodexBar CLI local cost usage to summarize per-model usage for Codex or Claude, including the current (most recent) model or a full model breakdown. Trigger when asked for model-level usage/cost data from codexbar, or when you need a scriptable per-model summary from codexbar cost JSON.
arscontexta
2.9kClaude Code plugin that generates individualized knowledge systems from conversation. You describe how you think and work, have a conversation and get a complete second brain as markdown files you own.
cursor-agent-tracking
134A repository that provides a structured system for maintaining context and tracking changes in Cursor's AGENT mode conversations through template files, enabling better continuity and organization of AI interactions.
