Posts
-
super-save 0.5: Modernized and Better Than Ever
It’s been a while since the last super-save release. The last time I wrote about it was back in 2018, when I boldly proclaimed:
It seems that now super-save is beyond perfect, so don’t expect the next release any time soon!
Famous last words. There was a 0.4 release in 2023 (adding a predicate system, buffer exclusions, silent saving, and trailing whitespace cleanup), but I never got around to writing about it. The package has been rock solid for years and I just didn’t pay it much attention – it quietly did its job, which is kind of the whole point of an auto-save package.
A Bit of History
The idea behind
super-savegoes all the way back to a blog post I wrote in 2012 about auto-saving buffers on buffer and window switches. I had been using IntelliJ IDEA for Java development and loved that it would save your files automatically whenever the editor lost focus. No manualC-x C-s, no thinking about it. I wanted the same behavior in Emacs.Back then, the implementation was crude –
defadviceonswitch-to-buffer,other-window, and thewindmovecommands. That code lived in Emacs Prelude for a few years before I extracted it into a standalone package in 2015.super-savewas born.What Prompted This Release
Yesterday I stumbled upon buffer-guardian.el, a package with very similar goals to
super-save. Its README has a comparison with super-save that highlighted some valid points – mainly thatsuper-savewas still relying on advising specific commands for buffer-switch detection, while newer Emacs hooks likewindow-buffer-change-functionsandwindow-selection-change-functionscould do the job more reliably.The thing is, those hooks didn’t exist when
super-savewas created, and I didn’t rush to adopt them while Emacs 27 was still new and I wanted to support older Emacsen. But it’s 2026 now – Emacs 27.1 is ancient history. Time to modernize!What’s New in 0.5
This is the biggest
super-saverelease in years! Here are the highlights:Modern buffer/window switch detection
Buffer and window switches are now detected via
window-buffer-change-functionsandwindow-selection-change-functions, controlled by the newsuper-save-when-buffer-switchedoption (enabled by default). This catches all buffer switches – keyboard commands, mouse clicks, custom functions – unlike the old approach of advising individual (yet central) commands.Modern focus handling
Frame focus loss is now detected via
after-focus-change-functioninstead of the obsoletefocus-out-hook, controlled bysuper-save-when-focus-lost(also enabled by default).Soft-deprecated trigger system
With the new hooks in place, both
super-save-triggersandsuper-save-hook-triggersnow default tonil. You can still use them for edge cases, but for the vast majority of users, the built-in hooks cover everything.org-src and edit-indirect support
super-savenow knows how to saveorg-srcedit buffers (viaorg-edit-src-save) andedit-indirectbuffers (viaedit-indirect--commit). Both are enabled by default and controlled bysuper-save-handle-org-srcandsuper-save-handle-edit-indirect.Safer predicates
Two new default predicates prevent data loss:
verify-visited-file-modtimeavoids overwriting files modified outside Emacs, and a directory existence check prevents errors when a file’s parent directory has been removed. Predicate evaluation is also wrapped incondition-casenow, so a broken custom predicate logs a warning instead of silently disabling all auto-saving.Emacs 27.1 required
This allowed cleaning up the code and relying on modern APIs.
Upgrading
For most users, upgrading is seamless – the new defaults just work. If you had a custom
super-save-triggerslist for buffer-switching commands, you can probably remove it entirely:;; Before: manually listing every command that switches buffers (setq super-save-triggers '(switch-to-buffer other-window windmove-up windmove-down windmove-left windmove-right next-buffer previous-buffer)) ;; After: the window-system hooks catch all of these automatically ;; Just delete the above and use the defaults!If you need to add triggers for commands that don’t involve a buffer switch (like
ace-window),super-save-triggersis still available for that.A clean 0.5 setup looks something like this:
(use-package super-save :ensure t :config ;; Save buffers automatically when Emacs is idle (setq super-save-auto-save-when-idle t) ;; Don't display "Wrote file..." messages in the echo area (setq super-save-silent t) ;; Disable the built-in auto-save (backup files) since super-save handles it (setq auto-save-default nil) (super-save-mode +1))It’s also worth noting that Emacs 26.1 introduced
auto-save-visited-mode, which saves file-visiting buffers to their actual files after an idle delay. This overlaps withsuper-save-auto-save-when-idle, so if you prefer using the built-in for idle saves, you can combine the two:(use-package super-save :ensure t :config ;; Don't display "Wrote file..." messages in the echo area (setq super-save-silent t) ;; Disable the built-in auto-save (backup files) (setq auto-save-default nil) (super-save-mode +1)) ;; Let the built-in auto-save-visited-mode handle idle saves (auto-save-visited-mode +1)Burst-Driven Development Strikes Again
Most of my Emacs packages are a fine example of what I like to call burst-driven development – long periods of stability punctuated by short intense bursts of activity. I hadn’t touched
super-savein years, then spent a few hours modernizing the internals, adding a test suite, improving the documentation, and cutting a release. It was fun to revisit the package after all this time and bring it up to 2026 standards.If you’ve been using
super-save, update to 0.5 and enjoy the improvements. If you haven’t tried it yet – give it a shot. Your poor fingers might thanks for you this, as pressingC-x C-snon-stop is hard work!That’s all I have for you today. Keep hacking!
-
Tree-sitter Font-Lock and Indentation in Comint Buffers
If you maintain a tree-sitter major mode that has a REPL (comint) companion, you’ve probably wondered: can the REPL input get the same syntax highlighting and indentation as source buffers? The answer is yes – and the infrastructure has been in Emacs since 29.1. It’s just not widely known yet.
Read More -
surround.el: Vim-Style Pair Editing Comes to Emacs
In my recent article on removing paired delimiters, I mentioned that I kind of miss Vim’s surround.vim experience in Emacs. Well, it turns out someone has done something about it – surround.el brings the core ideas of
surround.vimto native Emacs, without requiring Evil mode.surround.vim(and similar plugins like mini.surround in Neovim) are some of my favorite Vim packages. The idea is so simple and so useful that it feels like it should be a built-in. So I’m happy to see someone took the time to port that beautiful idea to Emacs.The Core Ideas of surround.vim
For those who haven’t used
surround.vim, the concept is straightforward. You have a small set of operations for working with surrounding characters – the delimiters that wrap some piece of text:- Delete surrounding pair:
ds(removes the parentheses around point - Change surrounding pair:
cs("changes parens to quotes - Add surrounding pair:
ys+ motion + character wraps text with a delimiter
That’s basically it. Three operations, consistent keybindings, works everywhere regardless of file type. The beauty is in the uniformity – you don’t need different commands for different delimiters, and you don’t need to think about which mode you’re in.
surround.el in Practice
surround.elis available on MELPA and the setup is minimal:(use-package surround :ensure t :bind-keymap ("M-'" . surround-keymap))You bind a single keystroke (
M-'in this example) tosurround-keymap, and that gives you access to all the operations through a second keystroke. Here are the commands available in the keymap:Key Operation sSurround region/symbol at point dDelete surrounding pair cChange pair to another kKill text inside pair KKill text including pair iMark (select) inside pair oMark including pair There are also shortcuts for individual characters – pressing an opening delimiter (like
() after the prefix does amark-inner, while pressing the closing delimiter (like)) does amark-outer.A Short Walkthrough
Let’s see how this works in practice. Starting with the word
Hello(with point somewhere on it):Surround –
M-' s "wraps the symbol at point with quotes:Hello → "Hello"Change –
M-' c " (changes the surrounding quotes to parens:"Hello" → (Hello)Mark inner –
M-' i (selects just the text inside the parens:(|Hello|) ;; "Hello" is selectedMark outer –
M-' o ((orM-' )) selects the parens too:|( Hello)| ;; "(Hello)" is selectedDelete –
M-' d (removes the surrounding parens:(Hello) → HelloKill –
M-' k (kills the text inside the pair (leaving the delimiters gone too), whileM-' K (kills everything including the delimiters.Inner vs. Outer
Like
surround.vim,surround.eldistinguishes between “inner” (just the content between delimiters) and “outer” (content plus the delimiters themselves). Theiandkcommands operate on inner text,oandKon outer.There’s also an “auto” mode – the default for
iandk– that behaves as inner when you type an opening character and outer when you type a closing character. SoM-' (marks inner,M-' )marks outer. Handy shortcut if your fingers remember it (I’m still building the muscle memory).One caveat: auto mode can’t distinguish inner from outer for symmetric pairs like quotes (
",'), since the opening and closing character are the same. In those cases it defaults to inner.How It Differs from Vim
The biggest difference is in how you surround text. In Vim,
surround.vimuses motions –ysiw(means “surround inner word with parens.” In Emacs,surround.eloperates on the active region or the symbol at point. So the typical workflow is: select something, thenM-' s (.This actually pairs beautifully with expreg (which I wrote about recently). Use
expregto incrementally select exactly the text you want, thenM-' sto wrap it. It’s a different rhythm than Vim’s motion-based approach, but once you get used to it, it feels natural.The other operations (
d,c,k,i,o) work similarly to their Vim counterparts – you invoke the command and then specify which delimiter you’re targeting.When to Use It
surround.elfills a specific niche:-
Using
electric-pair-mode? Thensurround.elis an excellent complement.electric-pair-modehandles auto-pairing when you type delimiters, but offers nothing for removing, changing, or wrapping existing text with delimiters.surround.elfills exactly that gap. -
Using
smartparens? You probably don’t needsurround.el–smartparensalready hassp-unwrap-sexp,sp-rewrap-sexp, and friends. The overlap is significant, and adding another package on top would just be confusing. -
Using
paredit? Same story for Lisp code –paredithas you covered withparedit-splice-sexp,paredit-wrap-round, and so on. But if you want the surround experience in non-Lisp buffers,surround.elis a good pick.
My current setup is
pareditfor Lisps,electric-pair-modefor everything else, and I’m addingsurround.elto complement the latter. Early days, but it feels right.Wrapping Up
If you’ve ever wondered whether some Vim feature you miss exists in Emacs – the answer is always yes. It’s Emacs. Of course someone has written a package for it. Probably several, in fact… Admittedly, I discovered
surround.elonly when I had decided to portsurround.vimto Emacs myself. :DThat’s all I have for you today. Keep hacking!
- Delete surrounding pair:
-
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-facesorset-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-facewill 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-attributeorcustom-set-facesis 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-facedidn’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-facemight 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-relativeface-remap-add-relativehas been around since Emacs 23 (it lives inface-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.
The Magic “Cookie”
face-remap-add-relativereturns 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))))Related Functionality
A few more things worth knowing in this area:
-
face-remap-set-base– sets the base remapping for a face in the current buffer. Unlikeface-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 thedefaultface in the current buffer. This is what powersM-x buffer-face-setand 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 useface-remap-add-relativeunder the hood to remap thedefaultface. 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-relativehas 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-sin 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
anzupackage (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-countdisplays 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
anzufelt), 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 duringquery-replace(M-%) andquery-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 bylazy-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-countpairs nicely with all the features covered there.Between
use-short-answersandisearch-lazy-count, that’s two third-party packages I was able to drop from Prelude just by using built-in functionality. Keep hacking!
Subscribe via RSS | View Older Posts