radian-software / apheleia

🌷 Run code formatter on buffer contents without moving point, using RCS patches and dynamic programming.
MIT License
517 stars 73 forks source link

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:

  1. It takes some time (e.g. around 200ms for Black on an empty file), which makes the editor feel less responsive.
  2. 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:

  1. Run code formatters on 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.
  2. 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.

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:

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:

The following user options are also available:

Apheleia exposes some hooks for advanced customization:

Troubleshooting

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.

Known issues

Contributing

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.

Adding a formatter

I have done my best to make it straightforward to add a formatter. You just follow these steps:

  1. Install your formatter on your machine so you can test.
  2. Create an entry in apheleia-formatters with how to run it. (See the docstring of this variable for explanation about the available keywords.)
  3. Add entries for the relevant major modes in apheleia-mode-alist.
  4. See if it works for you!
  5. Add a file at test/formatters/installers/yourformatter.bash which explains how to install the formatter on Ubuntu. This will be used by CI.
  6. Test with 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.
  7. Add an example input (pre-formatting) and output (post-formatting) file at test/formatters/samplecode/yourformatter/in.whatever and test/formatters/samplecode/yourformatter/out.whatever.
  8. Verify that the tests are passing, using make fmt-test FORMATTERS=yourformatter from inside the fmt-docker shell.
  9. Submit a pull request, CI should now be passing!

Acknowledgements

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.