Open-source project
radian-software/apheleia avatar
radian-software/apheleia

Apheleia and the Emacs problem of formatters moving your cursor

🌷 Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.

790 stars104 forksEmacs LispMIT

At a glance

What is it?
Apheleia is an Emacs package that runs code formatters on save without moving point, by applying an RCS patch and aligning point with dynamic programming when needed. It bundles no formatters, and it makes no stability promise on Windows.
Who is it for?
Apheleia fits Emacs users who format on save and lose their cursor position every time, and who already have Black, Prettier or Gofmt installed on their PATH. It does not fit a team that wants formatting guarantees enforced at commit time, since Apheleia only runs in the editor.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 6 days ago.
What is it written in?
Mainly Emacs Lisp, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two problems with running a formatter on save

The README frames Apheleia around two annoyances that every Emacs user who formats on save eventually hits. The first is latency. The README gives an example of around 200ms for Black on an empty file, and a pause of that length on every save makes the editor feel sluggish. The second is point movement. A formatter rewrites the buffer from the beginning, so if the changes it makes land too close to where the cursor was sitting, point ends up somewhere unexpected. The damage is not cosmetic. A save that silently relocates the cursor costs a new round of visual search, and in the worst case it happens at the exact moment you hit save after writing code.

The project's answer is not a faster formatter. It is a different scheduling and patching strategy that keeps the editor responsive and the cursor where you left it, while leaving the choice of formatter to the user.

Formatters run after the save, not before it

The first half of the mechanism is timing. Apheleia hooks into the `after-save-hook` rather than `before-save-hook`, and it starts the formatter asynchronously. The file on disk is therefore written first, and the formatting pass happens alongside whatever you do next. The README notes that the editor stays responsive because the formatter is not blocking the save.

Asynchronous work needs a staleness check, and Apheleia has one: once the formatter has finished, the buffer is checked to see whether it was modified in the meantime. The formatted output is applied only if the buffer was not touched while the formatter ran. If you kept typing during the formatter's run, the result is discarded rather than pasted over your newer edits. That single guard is what makes asynchronous formatting safe enough to leave switched on globally.

An RCS patch keeps unrelated edits from dragging point

The second half of the mechanism is the interesting part. Instead of replacing the buffer contents with the formatter output, Apheleia generates an RCS patch that describes the difference between the buffer and the formatted text, and then applies that patch to the buffer. A patch expresses edits as localized hunks, so changes elsewhere in the buffer do not shift the text above the cursor. Point survives because the surrounding lines stay where they were.

The README is explicit about the residual problem. If one of those patch regions happens to include point, then the patch alone is not enough, because the text at the cursor itself was rewritten. That is where the alignment step takes over. The size of the patch matters here as well: only regions that actually touch point can cause trouble, so the expensive case is rare by construction.

Dynamic programming decides where point lands

When a patch region contains point, Apheleia computes the new position with a dynamic programming algorithm for string alignment, the same family of approach as the Needleman-Wunsch algorithm. The idea is to find the alignment that keeps the cursor in the same place relative to its surroundings, so that the code you were looking at is still the code you are looking at, even though the formatter reflowed the line.

The cost of this is stated plainly in the README: the algorithm runs in quadratic time, which is why it is applied only when necessary and only to a single patch region. That is a deliberate engineering trade-off rather than an oversight. Running alignment over the whole buffer on every save would recreate the performance problem the package exists to solve. After point is placed, if its vertical position relative to the window changed, the scroll position is adjusted to keep visual continuity, and the README says this includes iterating through all windows displaying the buffer when there is more than one.

The repository layout backs this up. Alongside the main `apheleia.el` file sit `apheleia-rcs.el`, `apheleia-dp.el`, `apheleia-formatters.el`, `apheleia-formatter-context.el`, `apheleia-log.el` and `apheleia-utils.el`, with the Makefile listing that exact compile order and noting that the order is important.

Installing with straight.el and switching the mode on

Apheleia is available on MELPA, and the README says the easiest route is straight.el. Adding the package declaration to your init file pulls it in through that package manager, and other package managers work too. After installation, the mode has to be enabled in the init file as well. The README shows the autoloading has been arranged so that this line does not pull the whole package into memory until you actually save a file, which keeps startup cost low.

elisp
    (straight-use-package 'apheleia)
elisp
    (apheleia-global-mode +1)

The dependencies are modest but worth checking before you install. Emacs 27 or later is supported. Apheleia contains no formatters of its own, so Black, Prettier, Gofmt or whatever you want must be installed separately, and it is picked up automatically as long as it is on `$PATH`. The README recommends having Bash installed, since it is used to invoke certain formatters, including Node.js based ones.

Formatter commands live in the apheleia-formatters alist

By default Apheleia is configured to format with Black, Prettier and Gofmt on save in the relevant major modes. The commands and the mapping to modes are both configurable, through the `apheleia-formatters` alist and the `apheleia-mode-alist`. The alist maps formatter names, symbols such as `black` and `prettier`, to the commands used to run them, such as `("black" "-")` and `(npx "prettier" input)`. It is an ordinary alist, so standard Emacs functions work on it. Adding a command line option to Black is a matter of replacing its entry, which the README shows with `setf` and `alist-get`.

elisp
(setf (alist-get 'black apheleia-formatters)
      '("black" "--option" "..." "-"))

The alist has a further trick. Certain symbols are interpreted specially when a command is assembled, and any non string entry in a formatter that is not one of those symbols is evaluated and replaced in place. That is how a command can depend on buffer state. The README's example adds a `beautysh` entry for shell formatting where the `--tab` flag is only passed when `indent-tabs-mode` is true, and where the indent size is taken from `sh-basic-offset` only when that variable is bound. The evaluated entries are substituted into the command exactly as you would expect, whether the evaluation returns a list of flags or a single string.

Missing formatters are skipped in silence

There are some behaviours here that deserve a careful read before you trust the setup. The README states that missing formatters will be silently skipped, while errors from invoking formatters that are installed are reported on buffer save. The practical consequence is that a typo in a command name, or a formatter that is not on `$PATH`, produces no formatting and no warning. You find out by noticing that your file was not reformatted.

Windows is a second limit. The README does not guarantee support on that platform, citing a lack of support for common open standards there. Pull requests aimed at cross platform portability are welcome, but no stability guarantee is made for Windows. The buffer handling detail also matters for fileless buffers: when a buffer has no underlying file, the `file` and `filepath` values become the buffer name with file system special characters stripped, and that name determines the extension given to any temporary file. The README explains that a buffer called `*foo-bar.c*` ends up with an implicit file name of `foo-bar.c` and temporary files suffixed with `.c`, which matters for formatters that decide the language from the extension.

Apheleia against Blacken and prettier-js

The README positions Apheleia against the language specific packages it is meant to replace, naming Blacken for Python and prettier-js for JavaScript. The difference is not that Apheleia formats better. Blacken and prettier-js wrap one formatter each, and they replace the buffer contents with the formatted output, which is exactly the behaviour that moves point. Apheleia is formatter agnostic: it runs whatever command you configure, and it treats the formatter as an external process whose output becomes a patch rather than a new buffer.

The trade-off runs the other way too. Because Apheleia shells out to formatters on your `$PATH`, it inherits their startup cost, and it depends on a working external toolchain rather than bundling one. Emacs based Elisp formatters are possible, though, since the README describes implementing formatters as arbitrary Elisp functions that operate directly on a buffer, which is the stated route for integrating with something like a language server.

The project is MIT licensed, with the terms in LICENSE.md at the repository root, and the last push was on 2026-10-17. Releases are cut as versioned tags, with v4.5.0 published on 2026-05-25 and v4.4.3 on 2026-02-21.

Editorial conclusion

Apheleia fits Emacs users who format on save and lose their cursor position every time, and who already have Black, Prettier or Gofmt installed on their PATH. It does not fit a team that wants formatting guarantees enforced at commit time, since Apheleia only runs in the editor. Before adopting it, install a formatter and confirm that the file on disk is one the formatter accepts, because a missing formatter is skipped in silence rather than reported.

Frequently asked questions

Does Apheleia include Black, Prettier or Gofmt?

No. The README states that Apheleia does not include any formatters and that you must install the formatter you want separately. As long as it is on `$PATH`, Apheleia picks it up automatically.

What happens if the formatter is not installed?

Missing formatters are silently skipped, so nothing is formatted and no message appears. Errors from a formatter that is installed are reported on buffer save.

How does Apheleia keep the cursor in place while formatting?

It generates an RCS patch describing the formatter's changes and applies that patch to the buffer, so unrelated edits do not shift the text. If a patch region contains point, a quadratic time string alignment algorithm decides the new position.

Which Emacs versions does Apheleia support?

The README says Emacs 27 or later is supported. Bash is recommended as a dependency because it is used to invoke certain formatters, including Node.js based ones.

Does Apheleia work on Windows?

Support is not guaranteed, according to the README, because common open standards are missing on that platform. Pull requests improving portability are accepted but no stability guarantee is made for Windows.

Official sources

  1. Issues
  2. License: MIT
  3. radian-software/apheleia on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/radian-software-apheleia.svg)](https://hysenlabs.com/projects/radian-software-apheleia)

Was this useful?