• Stars
    star
    212
  • Rank 179,741 (Top 4 %)
  • Language
    Emacs Lisp
  • Created almost 5 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

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

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:

  • 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.

    • 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 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.
  • 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.

Known issues

  • 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.

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.

More Repositories

1

straight.el

🍀 Next-generation, purely functional package manager for the Emacs hacker.
Emacs Lisp
2,018
star
2

riju

⚡ Extremely fast online playground for every programming language.
JavaScript
588
star
3

radian

🍉 Dotfiles that marry elegance and practicality.
Emacs Lisp
370
star
4

ctrlf

⌨️ Emacs finally learns how to ctrl+F.
Emacs Lisp
276
star
5

el-patch

✨ Future-proof your Emacs Lisp customizations!
Emacs Lisp
224
star
6

riju-replit

⚡ Extremely fast online playground for every programming language.
112
star
7

blackout

💡 The easy way to clean up your Emacs mode lighters.
Emacs Lisp
42
star
8

intuitive-explanations

📚 Understand, don't memorize.
JavaScript
14
star
9

emtas

Emacs Lisp
14
star
10

mercury

💀 ABANDONED: Emacs interface to Facebook Messenger
Python
10
star
11

minimal-webapp

💀 DEPRECATED: Minimal webapp using ClojureScript, Compojure, and Reagent
Clojure
7
star
12

ishikk

💀 ABANDONED: Calendar for the weary fisher.
Emacs Lisp
5
star
13

with-feature

💀 DEPRECATED: Better version of with-eval-after-load.
Emacs Lisp
5
star
14

Watching-Paint-Dry

Java applet where you can add paint and then watch it dry. Yes, really.
Java
4
star
15

elint

💀 DEPRECATED: Small module to deduplicate Elisp build tooling.
Shell
4
star
16

dotman

💀 DEPRECATED: One package manager to rule them all
Python
4
star
17

example-website

Example static site, like my personal website but much simpler
HTML
4
star
18

cs121-hello

Demo for CS 121 "Software Development", Harvey Mudd College Spring 2019.
HTML
4
star
19

straight.el-support

💀 DEPRECATED: examples and benchmarks for https://github.com/raxod502/straight.el
Emacs Lisp
3
star
20

lazy-map

Lazy maps for Clojure.
Clojure
3
star
21

funwithframes

Game where there are a lot of rectangles at the same time.
Processing
3
star
22

contributor-guide

Deduplicate contributor guides.
3
star
23

space-grid

Abandoned plan for a clone of the old Flash game Star Relic
Python
3
star
24

empty

Absolute bare minimum Leiningen template
Clojure
3
star
25

org-emacs

Simple configuration of Emacs optimized for Org mode
Emacs Lisp
3
star
26

mothers-day-2013

Java applet for Mother's Day from 2013.
Java
3
star
27

cs121-whales

CS 121 - Spring 2019
JavaScript
3
star
28

puzzles

Solvers for KenKen and Sudoku puzzles in Clojure
Clojure
2
star
29

dfa

Generate DFAs using genetic algorithm.
Clojure
2
star
30

TI84

Old programs that I wrote for the TI-84 in middle and high school.
2
star
31

MathViewers

Programming projects from high school math classes.
Java
2
star
32

CAS

💀 DEPRECATED: Failed attempt at creating a computer algebra system
Java
2
star
33

ScienceFair

Middle school science fair project: Boolean satisfiability solver applied to Sudoku puzzles.
Python
2
star
34

mothers-day-2021

JavaScript
2
star
35

bug48170-repro

https://debbugs.gnu.org/cgi/bugreport.cgi?bug=48170
Emacs Lisp
1
star
36

JFLAP-Autograder

Grades JFLAP student submissions automatically.
Python
1
star
37

juniper-tools

Tools and scripts for Juniper Networks '19-20 HMC Clinic project.
Python
1
star
38

tabcrush

💀 ABANDONED: Crushes table-editing problems.
Emacs Lisp
1
star
39

juniper-linux

Linux kernel for Juniper Networks '19-20 HMC Clinic project
C
1
star
40

pset

💀 ABANDONED: The simplest LaTeX problem-set templating engine that could possibly work.
Python
1
star
41

acc

💀 DEPRECATED: Command-line accounting tool.
Python
1
star
42

VotingLib

Library for studying the comparative effectiveness of different voting systems
Java
1
star
43

tidier-legacy

💀 DEPRECATED: crusty, broken Scala code for cleaning GitHub issues
Shell
1
star
44

mood-tracker

Capture, aggregate, and analyze moods
Shell
1
star
45

fathers-day-2021

JavaScript
1
star
46

profile-dotemacs

My mirror of the Emacs package, with some fixes
Emacs Lisp
1
star