Posts

  • Buffer-Local Face Remapping with face-remap-add-relative

    Yet another neocaml issue taught me something I didn’t know about Emacs. Someone requested that the docs show how to customize font-lock faces per mode rather than globally. The suggested approach used face-remap-add-relative – a function I’d never heard of, despite 20+ years of daily Emacs use. Time to dig in.

    The Problem

    Let’s say you want type names in OCaml buffers to be displayed in a different color than in, say, Python buffers. The naive approach is to use custom-set-faces or set-face-attribute:

    (set-face-attribute 'font-lock-type-face nil :foreground "DarkGreen")
    

    This works, but it’s a global change – every buffer that uses font-lock-type-face will now show types in dark green.

    When Global is Fine

    To be fair, global face customization is perfectly valid in several scenarios:

    • Your theme doesn’t cover certain faces. Many themes only style a subset of the faces that various packages define. If a face looks wrong or unstyled, a global set-face-attribute or custom-set-faces is the quickest fix.

    • Your theme makes choices you disagree with. I recently ran into this with the Catppuccin theme – the styling of font-lock-variable-name-face didn’t match how I expected it to look, so I filed a PR. It got closed because different modes interpret that face inconsistently (definitions in Elisp, references in C), making a universal fix impractical for the theme. That’s exactly the kind of situation where you’d want to override the face yourself.

    • You want consistent styling everywhere. If you just want all comments to be italic, or all strings to use a specific color regardless of mode, global is the way to go. No need to complicate things with per-buffer remapping.

    When Global Isn’t Enough

    The problem comes when you bounce between several languages (and let’s be honest, most of us do) and you want different visual treatment depending on the mode. Not all modes use the built-in font-lock faces consistently, and for some – especially markup languages – there’s a lot of room for improvisation in how faces get applied. A global change to font-lock-keyword-face might look great in your Python buffers but terrible in your Org files.

    That’s where buffer-local face remapping comes in.

    Enter face-remap-add-relative

    face-remap-add-relative has been around since Emacs 23 (it lives in face-remap.el), and it does exactly what the name suggests – it remaps a face relative to its current definition, and only in the current buffer. The change is buffer-local, so it won’t leak into other buffers.

    Here’s the basic usage:

    (face-remap-add-relative 'font-lock-type-face :foreground "DarkGreen")
    

    To apply this automatically in a specific mode, hook it up:

    (defun my-ocaml-faces ()
      "Customize faces for OCaml buffers."
      (face-remap-add-relative 'font-lock-type-face :foreground "DarkGreen")
      (face-remap-add-relative 'font-lock-function-name-face :weight 'bold))
    
    (add-hook 'neocaml-mode-hook #'my-ocaml-faces)
    

    Now OCaml buffers get their own face tweaks while everything else stays untouched. You can do the same for any mode – just swap the hook and adjust the faces to taste.

    face-remap-add-relative returns a cookie – a token you’ll need if you want to undo the remapping later. If you’re just setting things up in a mode hook and leaving them, you can ignore the cookie. But if you want to toggle the remapping on and off, you’ll need to hold onto it:

    (defvar-local my-type-face-cookie nil
      "Cookie for type face remapping.")
    
    (defun my-toggle-type-face ()
      "Toggle custom type face in current buffer."
      (interactive)
      (if my-type-face-cookie
          (progn
            (face-remap-remove-relative my-type-face-cookie)
            (setq my-type-face-cookie nil)
            (message "Type face remapping removed"))
        (setq my-type-face-cookie
              (face-remap-add-relative 'font-lock-type-face
                                       :foreground "DarkGreen"))
        (message "Type face remapping applied")))
    

    Note the use of defvar-local – since face remapping is buffer-local, your cookie variable should be too.

    A cleaner approach is to wrap this in a minor mode:

    (defvar-local my-type-remap-cookie nil)
    
    (define-minor-mode my-type-remap-mode
      "Minor mode to remap type face in current buffer."
      :lighter " TypeRemap"
      (if my-type-remap-mode
          (setq my-type-remap-cookie
                (face-remap-add-relative 'font-lock-type-face
                                         :foreground "DarkGreen"))
        (when my-type-remap-cookie
          (face-remap-remove-relative my-type-remap-cookie)
          (setq my-type-remap-cookie nil))))
    

    A few more things worth knowing in this area:

    • face-remap-set-base – sets the base remapping for a face in the current buffer. Unlike face-remap-add-relative (which layers on top of the existing face), this replaces the face definition entirely for that buffer. Use this when you want to completely override a face rather than tweak it.

    • buffer-face-mode / buffer-face-set – a built-in minor mode that remaps the default face in the current buffer. This is what powers M-x buffer-face-set and is handy if you want a different base font in specific buffers (say, a proportional font for prose and a monospace font for code).

    • text-scale-adjust (C-x C-= / C-x C--) – the familiar text scaling commands actually use face-remap-add-relative under the hood to remap the default face. So if you’ve ever zoomed text in a single buffer, you’ve been using face remapping without knowing it.

    • face-remapping-alist – the buffer-local variable where all of this state is stored. You generally shouldn’t manipulate it directly (that’s what the functions above are for), but it’s useful for debugging – check its value in a buffer to see what remappings are active.

    Wrapping Up

    I have to admit – I’m a bit embarrassed that face-remap-add-relative has been sitting in Emacs since version 23 and I’d never once used it. Probably because I never felt the need for per-mode face customizations – but I can certainly see why others would, especially when working across languages with very different syntax highlighting conventions.

    Working on neocaml has been a gold mine of learning (and relearning). I’m happy to keep sharing the things I discover along the way. Keep hacking!

  • isearch-lazy-count: Built-in Search Match Counting

    Continuing my Prelude modernization effort (see the previous post for context), another long-standing third-party dependency I was happy to drop was anzu.

    The Problem

    When you’re searching with C-s in Emacs, you can see the current match highlighted, but you have no idea how many total matches exist in the buffer or which one you’re currently on. Are there 3 matches or 300? You just don’t know.

    The Old Way

    For years, I used the anzu package (an Emacs port of anzu.vim) to display match counts in the mode-line. It worked well, but it was yet another third-party dependency to maintain – and one that eventually ended up in the Emacs orphanage, which is never a great sign for long-term maintenance.

    The New Way

    Emacs 27 introduced isearch-lazy-count:

    (setopt isearch-lazy-count t)
    

    With this enabled, your search prompt shows something like (3/47) – meaning you’re on the 3rd match out of 47 total. Simple, built-in, and requires no external packages.

    Unlike anzu, which showed counts in the mode-line, isearch-lazy-count displays them right in the minibuffer alongside the search string, which is arguably a better location since your eyes are already there while searching.

    Customizing the Format

    Two variables control how the count is displayed:

    ;; Prefix format (default: "%s/%s ")
    ;; Shows before the search string in the minibuffer
    (setopt lazy-count-prefix-format "(%s/%s) ")
    
    ;; Suffix format (default: nil)
    ;; Shows after the search string
    (setopt lazy-count-suffix-format nil)
    

    If you prefer the count at the end of the prompt (closer to how anzu felt), you can swap them:

    (setopt lazy-count-prefix-format nil)
    (setopt lazy-count-suffix-format " [%s/%s]")
    

    Good to Know

    The count works with all isearch variants – regular search, regex search (C-M-s), and word search. It also shows counts during query-replace (M-%) and query-replace-regexp (C-M-%), which is very handy for knowing how many replacements you’re about to make.

    The counting is “lazy” in the sense that it piggybacks on the lazy highlighting mechanism (lazy-highlight-mode), so it doesn’t add significant overhead. In very large buffers, you might notice a brief delay before the count appears, controlled by lazy-highlight-initial-delay. One thing to keep in mind – if you’ve disabled lazy highlighting for performance reasons, you’ll need to re-enable it, as the count depends on it.

    If you haven’t read it already, check out my earlier article You Have No Idea How Powerful Isearch Is for a deep dive into what isearch can do. isearch-lazy-count pairs nicely with all the features covered there.

    Between use-short-answers and isearch-lazy-count, that’s two third-party packages I was able to drop from Prelude just by using built-in functionality. Keep hacking!

  • use-short-answers: The Modern Way to Tame yes-or-no Prompts

    I recently started a long overdue update of Emacs Prelude, rebasing it on Emacs 29 as the minimum supported version. This has been a great excuse to revisit a bunch of old configuration patterns and replace them with their modern built-in equivalents. One of the first things I updated was the classic yes-or-no-p hack.

    The Problem

    By default, Emacs asks you to type out the full word yes or no for certain prompts – things like killing a modified buffer or deleting a file. The idea is that this extra friction prevents you from accidentally confirming something destructive, but in practice most people find it annoying and want to just hit y or n.

    The Old Way

    For decades, the standard solution was one of these:

    (fset 'yes-or-no-p 'y-or-n-p)
    
    ;; or equivalently:
    (defalias 'yes-or-no-p 'y-or-n-p)
    

    This worked by literally replacing the yes-or-no-p function with y-or-n-p at runtime. Hacky, but effective – until native compilation came along in Emacs 28 and broke it. Native compilation can hardcode calls to C primitives, which means fset/defalias sometimes has no effect on yes-or-no-p calls that were already compiled. You’d set it up, and some prompts would still ask for yes or no. Not fun.

    The New Way

    Emacs 28 introduced the use-short-answers variable:

    (setopt use-short-answers t)
    

    That’s it. Clean, discoverable, native-compilation-safe, and officially supported. It makes yes-or-no-p delegate to y-or-n-p internally, so it works correctly regardless of compilation strategy.

    If you’re maintaining a config that needs to support older Emacs versions as well, you can do:

    (if (boundp 'use-short-answers)
        (setopt use-short-answers t)
      (fset 'yes-or-no-p 'y-or-n-p))
    

    A Word of Caution

    The Emacs maintainers intentionally designed yes-or-no-p to slow you down for destructive operations. Enabling use-short-answers removes that friction entirely. In practice, I’ve never accidentally confirmed something I shouldn’t have with a quick y, but it’s worth knowing the tradeoff you’re making.

    A Few More Things

    If you’re using GUI Emacs, you might also want to disable dialog boxes for a consistent experience:

    (setopt use-dialog-box nil)
    

    It’s also worth knowing that the related variable read-answer-short controls the same behavior for multi-choice prompts (the ones using read-answer internally). Setting use-short-answers affects both yes-or-no-p and read-answer.

    This is one of those small quality-of-life improvements that Emacs has been accumulating in recent versions. Updating Prelude has been a nice reminder of how many rough edges have been smoothed over. Keep hacking!

  • Removing Paired Delimiters in Emacs

    The other day someone filed an issue against my neocaml package, reporting surprising behavior with delete-pair. My first reaction was – wait, delete-pair? I’ve been using Emacs for over 20 years and I wasn’t sure I had ever used this command. Time for some investigation!

    What is delete-pair?

    delete-pair is a built-in Emacs command (defined in lisp/emacs-lisp/lisp.el) that deletes a pair of matching characters – typically parentheses, brackets, braces, or quotes. You place point on an opening delimiter, invoke delete-pair, and it removes both the opening and closing delimiter.

    Given that it lives in lisp.el, it was clearly designed with Lisp editing in mind originally. And it was probably quite handy back in the day – before paredit came along and made delete-pair largely redundant for Lisp hackers.

    Here’s a simple example. Given the following code (with point on the opening parenthesis):

    (print_endline "hello")
    

    Running M-x delete-pair gives you:

    print_endline "hello"
    

    Simple and useful! Yet delete-pair has no default keybinding, which probably explains why so few people know about it. If you want to use it regularly, you’ll need to bind it yourself:

    (global-set-key (kbd "M-s-d") #'delete-pair)
    

    Pick whatever keybinding works for you, of course. There’s no universally agreed upon binding for this one.

    The Gotcha

    The issue that was reported boiled down to delete-pair not always finding the correct matching delimiter. It uses forward-sexp under the hood to find the matching closer, which means its accuracy depends entirely on the buffer’s syntax table and the major mode’s parsing capabilities. For languages with complex or unusual syntax, this can sometimes lead to the wrong delimiter being removed – not great when you’re trying to be surgical about your edits.

    Alternatives for Pair Management

    If you work with paired delimiters frequently, delete-pair is just one tool in a rich ecosystem. Here’s a quick overview of the alternatives:

    paredit

    paredit is the gold standard for structured editing of Lisp code. I’ve been a heavy paredit user for as long as I can remember – if you write any Lisp-family language, it’s indispensable. paredit gives you paredit-splice-sexp (bound to M-s by default), which removes the surrounding delimiters while keeping the contents intact. There’s also paredit-raise-sexp (M-r), which replaces the enclosing sexp with the sexp at point – another way to get rid of delimiters. And of course, paredit prevents you from creating unbalanced expressions in the first place, which is a huge win.

    Once you’ve got paredit in your muscle memory, you’ll never think about delete-pair again (as I clearly haven’t).

    Let’s see these commands in action. In the examples below, | marks the position of point.

    paredit-splice-sexp (M-s) – removes the surrounding delimiters:

    ;; Before (point anywhere inside the inner parens):
    (foo (bar| baz) quux)
    
    ;; After M-s:
    (foo bar| baz quux)
    

    paredit-raise-sexp (M-r) – replaces the enclosing sexp with the sexp at point:

    ;; Before:
    (foo (bar| baz) quux)
    
    ;; After M-r:
    (foo bar| quux)
    

    Notice the difference: splice keeps all the siblings, raise keeps only the sexp at point and discards everything else inside the enclosing delimiters.

    smartparens

    smartparens is the most feature-rich option and works across all languages, not just Lisps. For unwrapping pairs, it offers a whole family of commands:

    • sp-unwrap-sexp – removes the enclosing pair delimiters, keeping the content
    • sp-backward-unwrap-sexp – same, but operating backward
    • sp-splice-sexp (M-D) – removes delimiters and integrates content into the parent expression
    • sp-splice-sexp-killing-backward / sp-splice-sexp-killing-forward – splice while killing content in one direction

    Here’s how the key ones look in practice:

    sp-unwrap-sexp – removes the next pair’s delimiters:

    # Before (point on the opening bracket):
    result = calculate(|[x, y, z])
    
    # After sp-unwrap-sexp:
    result = calculate(|x, y, z)
    

    sp-splice-sexp (M-D) – works like paredit’s splice, removes the innermost enclosing pair:

    # Before (point anywhere inside the parens):
    result = calculate(x + |y)
    
    # After M-D:
    result = calculate x + |y
    

    sp-splice-sexp-killing-backward – splices, but also kills everything before point:

    # Before:
    result = [first, second, |third, fourth]
    
    # After sp-splice-sexp-killing-backward:
    result = |third, fourth
    

    I used smartparens for a while for non-Lisp languages, but eventually found it a bit heavy for my needs.

    electric-pair-mode

    electric-pair-mode is the built-in option (since Emacs 24.1) that automatically inserts matching delimiters when you type an opening one. It’s lightweight, requires zero configuration, and works surprisingly well for most use cases. I’ve been using it as my go-to solution for non-Lisp languages for a while now.

    The one thing electric-pair-mode doesn’t offer is any way to unwrap/remove paired delimiters. The closest it gets is deleting both delimiters when you backspace between an adjacent empty pair (e.g., (|) – pressing backspace removes both parens). But that’s it – there’s no unwrap command. That’s where delete-pair comes in handy as a complement.

    A Note on Vim’s surround.vim

    Having played with Vim and its various surround.vim-like plugins over the years, I have to admit – I kind of miss that experience in Emacs, at least for removing paired delimiters. surround.vim makes it dead simple: ds( deletes surrounding parens, ds" deletes surrounding quotes. It works uniformly across all file types and feels very natural.

    In Emacs, the story is more fragmented – paredit handles it beautifully for Lisps, smartparens does it for everything but is a heavyweight dependency, and electric-pair-mode just… doesn’t do it at all. delete-pair is the closest thing to a universal built-in solution, but its lack of a default keybinding and its reliance on forward-sexp make it a bit rough around the edges.

    If you’re using electric-pair-mode and want a simple surround.vim-style “delete surrounding pair” command without pulling in a big package, here’s a little hack that does the trick:

    (defun delete-surrounding-pair (char)
      "Delete the nearest surrounding pair of CHAR.
    CHAR should be an opening delimiter like (, [, {, or \".
    Works by searching backward for the opener and forward for the closer."
      (interactive "cDelete surrounding pair: ")
      (let* ((pairs '((?\( . ?\))
                      (?\[ . ?\])
                      (?{ . ?})
                      (?\" . ?\")
                      (?\' . ?\')
                      (?\` . ?\`)))
             (closer (or (alist-get char pairs)
                         (error "Unknown pair character: %c" char))))
        (save-excursion
          (let ((orig (point)))
            ;; Find and delete the opener
            (when (search-backward (char-to-string char) nil t)
              (delete-char 1)
              ;; Find and delete the closer (adjust for removed char)
              (goto-char (1- orig))
              (when (search-forward (char-to-string closer) nil t)
                (delete-char -1)))))))
    
    (global-set-key (kbd "M-s-d") #'delete-surrounding-pair)
    

    Now you can hit M-s-d ( to delete surrounding parens, M-s-d " for quotes, etc. It’s deliberately naive – no syntax awareness, no nesting support – so it won’t play well with delimiters inside strings or comments (it’ll happily match a paren in a comment if that’s what it finds first). But for quick, straightforward edits it gets the job done.

    TIP: If you’re looking for something closer to the surround.vim experience in Emacs (without going full smartparens), check out surround.el. It’s a lightweight package (available on MELPA) that provides surround.vim-style operations – deleting, changing, and adding surrounding pairs – all through a single keymap. It supports both “inner” and “outer” text selection modes and works uniformly across file types, which makes it a nice middle ground between delete-pair and smartparens.

    When to Use What

    My current setup is:

    • Lisp languages (Emacs Lisp, Clojure, Common Lisp, etc.): paredit, no contest.
    • Everything else: electric-pair-mode for auto-pairing (I rarely need to unwrap something outside of Lisps)

    I think surround.el will pair well with electric-pair-mode, but I discovered it only recently and I’ve yet to try it out in practice.

    If you want a more powerful structural editing experience across all languages, smartparens is hard to beat. It’s just more than I personally need outside of Lisp.

    Wrapping Up

    One of the greatest aspects of Emacs is that we get to learn (or relearn) something about it every other day. Even after decades of daily use, there are always more commands lurking in the corners, patiently waiting to be discovered.

    Now, if you’ll excuse me, I’m going to immediately forget about delete-pair again. Keep hacking!

    Update: A reader pointed me to this Reddit thread with more tips on using delete-pair (including using it to change surrounding delimiters, surround.vim-style). Worth a read if you want to get more mileage out of it.

  • Code Formatting in Emacs

    I got inspired to look into this topic after receiving the following obscure bug report for neocaml:

    I had your package installed. First impressions were good, but then I had to uninstall it. The code formatting on save stopped working for some reason, and the quickest solution was to revert to the previous setup.

    I found this somewhat entertaining – neocaml is a major mode, it has nothing to do with code formatting. But I still started to wonder what kind of setup that user might have. So here we are!

    Code formatting is one of those things that shouldn’t require much thought – you pick a formatter, you run it, and your code looks consistent. In practice, Emacs gives you a surprising number of ways to get there, from built-in indentation commands to external formatters to LSP-powered solutions. This article covers the landscape and helps you pick the right approach.

    One thing to note upfront: most formatting solutions hook into saving the buffer, but there are two distinct patterns. The more common one is format-then-save (via before-save-hook) – the buffer is formatted before it’s written to disk, so the file is always in a formatted state. The alternative is save-then-format (via after-save-hook) – the file is saved first, then formatted and saved again. The second approach can be done asynchronously (the editor doesn’t block), but it means the file is briefly unformatted on disk. Keep this distinction in mind as we go through the options.

    Built-in: Indentation, Not Formatting

    Let’s get the terminology straight. Emacs has excellent built-in indentation support, but indentation is not the same as formatting. indent-region (C-M-\) adjusts leading whitespace according to the major mode’s rules. It won’t reformat long lines, reorganize imports, add or remove blank lines, or apply any of the opinionated style choices that modern formatters handle.

    That said, for many languages (especially Lisps), indent-region on the whole buffer is all the formatting you’ll ever need:

    ;; A simple indent-buffer command (Emacs doesn't ship one)
    (defun indent-buffer ()
      "Indent the entire buffer."
      (interactive)
      (indent-region (point-min) (point-max)))
    

    Tip: whitespace-cleanup is a nice complement – it handles trailing whitespace, mixed tabs/spaces, and empty lines at the beginning and end of the buffer. Adding it to before-save-hook keeps things tidy:

    (add-hook 'before-save-hook #'whitespace-cleanup)
    

    Shelling Out: The DIY Approach

    The simplest way to run an external formatter is shell-command-on-region (M-|). With a prefix argument (C-u M-|), it replaces the region with the command’s output:

    C-u M-| prettier --stdin-filepath foo.js RET
    

    (The --stdin-filepath flag doesn’t read from a file – it just tells Prettier which parser to use based on the filename extension.)

    You can wrap this in a command for repeated use:

    (defun format-with-prettier ()
      "Format the current buffer with Prettier."
      (interactive)
      (let ((point (point)))
        (shell-command-on-region
         (point-min) (point-max)
         "prettier --stdin-filepath foo.js"
         (current-buffer) t)
        (goto-char point)))
    

    This works, but it’s fragile – no error handling, no automatic file type detection, and cursor position is only approximately preserved. For anything beyond a quick one-off, you’ll want a proper package.

    reformatter.el: Define Your Own (Re)Formatters

    reformatter.el is a small library that generates formatter commands from a simple declaration. You define the formatter once, and it creates everything you need:

    (reformatter-define black-format
      :program "black"
      :args '("-q" "-")
      :lighter " Black")
    

    This single form generates three things:

    • black-format-buffer – format the entire buffer
    • black-format-region – format the selected region
    • black-format-on-save-mode – a minor mode that formats on save

    Enabling format-on-save is then just:

    (add-hook 'python-mode-hook #'black-format-on-save-mode)
    

    reformatter.el handles temp files, error reporting, and stdin/stdout piping. It also supports formatters that work on files instead of stdin (via :stdin nil and :input-file), and you can use buffer-local variables in :program and :args for per-project configuration via .dir-locals.el.

    I love the approach taken by this package! It’s explicit, you see exactly what’s being called, and the generated on-save mode plays nicely with the rest of your config.

    format-all: Zero Configuration

    format-all takes a different approach – it auto-detects the right formatter for 70+ languages based on the major mode. You don’t define anything; it just works:

    (add-hook 'prog-mode-hook #'format-all-mode)
    (add-hook 'prog-mode-hook #'format-all-ensure-formatter)
    

    The main command is format-all-region-or-buffer. The format-all-mode minor mode handles format-on-save. If you need to override the auto-detected formatter, set format-all-formatters (works well in .dir-locals.el):

    ;; In .dir-locals.el -- use black instead of autopep8 for Python
    ((python-mode . ((format-all-formatters . (("Python" black))))))
    
    ;; Or in your init file
    (setq-default format-all-formatters '(("Python" black)))
    

    The trade-off is less control – you’re trusting the package’s formatter database, and debugging issues is harder when you don’t see the underlying command.

    apheleia: Async and Cursor-Aware

    apheleia is the most sophisticated option. It solves two problems the other packages don’t:

    1. Asynchronous formatting – it runs the formatter after save, so the editor never blocks. If you modify the buffer before formatting completes, the result is discarded.
    2. Cursor preservation – instead of replacing the entire buffer, it applies changes as RCS patches (a classic diff format from one of the earliest version control systems), so your cursor position and scroll state are maintained.
    ;; Enable globally
    (apheleia-global-mode +1)
    

    apheleia auto-detects formatters like format-all, but you can configure things explicitly:

    ;; Chain multiple formatters (e.g., sort imports, then format)
    (setf (alist-get 'python-mode apheleia-mode-alist)
          '(isort black))
    

    Formatter chaining is a killer feature – isort then black, eslint then prettier, etc. No other package handles this as cleanly.

    Caveat: Because apheleia formats after save, the file on disk is briefly in an unformatted state. This is usually fine, but it can confuse tools that watch files for changes. It also doesn’t support TRAMP/remote files.

    LSP: eglot and lsp-mode

    If you’re already using a language server, formatting is built in. The language server handles the formatting logic, and Emacs just sends the request.

    eglot (built-in since Emacs 29)

    The main commands are eglot-format (formats the active region, or the entire buffer if no region is active) and eglot-format-buffer (always formats the entire buffer).

    Format-on-save requires a hook – eglot doesn’t provide a toggle for it:

    (add-hook 'eglot-managed-mode-hook
              (lambda ()
                (add-hook 'before-save-hook #'eglot-format-buffer nil t)))
    

    The nil t makes the hook buffer-local, so it only fires in eglot-managed buffers.

    lsp-mode

    The equivalents here are lsp-format-buffer and lsp-format-region.

    lsp-mode has a built-in option for format-on-save:

    (setq lsp-format-buffer-on-save t)
    

    It also supports on-type formatting (formatting as you type closing braces, semicolons, etc.) via lsp-enable-on-type-formatting, which is enabled by default.

    LSP Caveats

    • Formatting capabilities depend entirely on the language server. Some servers (like gopls or rust-analyzer) have excellent formatters; others may not support formatting at all.
    • The formatter’s configuration lives outside Emacs – in .clang-format, pyproject.toml, .prettierrc, etc. This is actually a feature if you’re working on a team, since the config is shared.
    • LSP formatting can be slow for large files since it’s a round-trip to the server process.

    Which Approach Should You Use?

    There’s no single right answer, but here’s a rough guide:

    • Lisps and simple indentation needs: Built-in indent-region is probably all you need.
    • Specific formatter, full control: reformatter.el – explicit, simple, and predictable.
    • Many languages, minimal config: format-all or apheleia. Pick apheleia if you want async formatting and cursor stability.
    • Already using LSP: Just use eglot-format / lsp-format-buffer. One less package to maintain.
    • Mixed setup: Nothing stops you from using LSP formatting for some languages and reformatter.el for others. Just be careful not to have two things fighting over format-on-save for the same mode.

    Tip: Whichever approach you choose, consider enabling format-on-save per project via .dir-locals.el rather than globally. Not every project uses the same formatter (or any formatter at all), and formatting someone else’s unformatted codebase on save is a recipe for noisy diffs.

    ;; .dir-locals.el
    ((python-mode . ((eval . (black-format-on-save-mode)))))
    

    Epilogue

    So many options, right? That’s so Emacs!

    I’ll admit that I don’t actually use any of the packages mentioned in this article – I learned about all of them while doing a bit of research for alternatives to the DIY and LSP approaches. That said, I have a very high opinion of everything done by Steve Purcell (author of reformatter.el, many other Emacs packages, a popular Emacs Prelude-like config, and co-maintainer of MELPA) and Radon Rosborough (author of apheleia, straight.el, and the Radian Emacs config), so I have no issue endorsing packages created by them.

    I’m in camp LSP most of the time these days, and I’d guess most people are too. But if I weren’t, I’d probably take apheleia for a spin. Either way, it’s never bad to have options, right?

    There are languages where LSP isn’t as prevalent – all sorts of Lisp dialects, for instance – where something like apheleia or reformatter.el might come in handy. But then again, in Lisps indent-region works so well that you rarely need anything else. I’m a huge fan of indent-region myself – for any good Emacs mode, it’s all the formatting you need.

Subscribe via RSS | View Older Posts