{"article":{"slug":"emacs-finally-folds-to-my-liking","title":"Emacs finally folds to my liking","subtitle":null,"summary":"Josep Bigorra describes unifying code folding across every Emacs mode on Emacs 32 master using kirigami as a dispatcher over treesit-fold, Hideshow, Outline and Org, with one consistent keymap, Tree-sitter fold guides, Org-specific handling and tests that verify folds and key bindings.","content_type":"blog_post","language":"en","canonical_url":"https://jointhefreeworld.org/blog/articles/emacs/emacs-folding-kirigami/","author":{"name":"Josep Bigorra","url":"https://jointhefreeworld.org/","person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"jointhefreeworld","url":"https://jointhefreeworld.org/","listing_slug":null,"listing":null},"topics":[{"name":"Developer Tools","slug":"developer-tools","url":"https://listedarticles.com/topics/developer-tools"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"},{"name":"Software Engineering","slug":"software-engineering","url":"https://listedarticles.com/topics/software-engineering"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":1304,"reading_minutes":6,"published_at":"2026-10-23T00:00:00.000Z","added_at":"2026-10-24T11:10:29.248Z","updated_at":"2026-10-24T11:10:29.248Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/emacs-finally-folds-to-my-liking","markdown_url":"https://listedarticles.com/articles/emacs-finally-folds-to-my-liking.md","example":false,"citation":"Josep Bigorra, jointhefreeworld. \"Emacs finally folds to my liking.\" 23 Oct 2026. https://jointhefreeworld.org/blog/articles/emacs/emacs-folding-kirigami/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://jointhefreeworld.org/blog/articles/emacs/emacs-folding-kirigami/"},"body_markdown":"# Emacs finally folds to my liking\n\n23/10/2026 · 11 minutes\n\nI’ve been slowly turning [my Emacs config](https://codeberg.org/jjba23/heks-emacs) into something I actually *enjoy* using, not just something that works, and folding was the last place where that wasn’t true.\n\nEvery mode had its own idea of what folding is: `treesit-fold` in Tree-sitter modes, Hideshow in the Lisp-ish ones, Outline in everything that resembles text, Org with its own invisible text that no one else’s commands can undo.\n\nSo every buffer had different keys, a different look, and worst of all different *performance* characteristics. Folding a big file sometimes felt like holding my breath.\n\nI’m running Emacs 32.0.50 from `master` (built from source, as one does), and it’s finally one system.\n\n![](https://jointhefreeworld.org/static-assets/heks/screen-2026-10-23-16-17-32.png)\n\n## kirigami is the piece I didn’t know I needed\n\n[kirigami](https://github.com/jamescherti/kirigami.el) doesn’t fold anything itself. It *dispatches*: it keeps a list of folding engines, checks which one is actually active in the buffer, and forwards to it. Tree-sitter, Hideshow, Outline, Org, Vdiff — all behind one interface.\n\nSo on top of it I put one set of keys, the same in a Kotlin buffer, YAML, a Scheme or ELisp buffer and an Org file:\n\n| Key | Does |\n| --- | --- |\n| `C-c v c` | close this fold |\n| `C-c v C` | close every fold |\n| `C-c v o` | open this fold |\n| `C-c v O` | open this fold and everything under it |\n| `C-c v r` | reveal everything |\n| `C-c v t` | toggle the fold at point |\n\nThat sounds small. It is not small. The number of times a day I used to think “wait, what does TAB do in this mode again” is now zero, and the muscle memory transfers across an entire polyglot config.\n\nOne wrinkle worth mentioning: kirigami answers with the *first* engine in its list whose mode is on, and `outline-minor-mode` is on in nearly every buffer I have. In the buffers where I deliberately turned on Hideshow — Scheme, for instance — that choice has to win, otherwise `C-c v c` folds “headings” a Scheme file doesn’t have. So I reorder `kirigami-fold-list` to put Hideshow first instead of rewriting it; engines kirigami adds later still get used wherever Hideshow is off.\n\n## Tree-sitter folding, with guides that are actually lines\n\nMost of my programming buffers are `*-ts-mode`, so `treesit-fold` does the work there — bash, C, C++, CMake, CSS, Dockerfile, Elixir, Go, HTML, Java, JS/TS/TSX, JSON, Kotlin, Lua, Nix, PHP, Python, Ruby, Rust, Scala, TOML, YAML. Line counts are shown inline, which I find genuinely useful when deciding whether to open a fold.\n\nThe indicators took the most fiddling. `treesit-fold-indicators` draws a block’s guide with a bitmap 23 rows tall, centered in the line beside it. A line in my config is 29 pixels — the font’s 23 plus six of `line-spacing` — so every row of the guide was a 23-pixel dash with three pixels of nothing above and below it. A folded block looked like a *dotted* line running down the fringe. Not subtle, just wrong.\n\nThere’s no setting for that, so I replaced the bitmaps: a single-row bitmap that `define-fringe-bitmap` repeats down however tall the line is (no spacing can introduce a gap), plus a `+=/`-= box and an end-elbow cut to the exact height of a line, so the guide runs from the marker into the row below and turns toward the text where the block ends. It’s an advice on the function that builds the string for each line. It looks like a bracket now, not a perforation.\n\nAnd the colour: the indicators inherited `shadow`, which most themes make as bright as a comment. Fine for one marker on one line, wrong for a marker on *every* line of a block — the grey became a rule drawn through the code. They’re now `shadow` blended 50% toward the theme’s background, recomputed on every theme and scale change, so it fades correctly on light themes too and no theme needs a special entry.\n\n## Hideshow earns its keep\n\nNot everything has a grammar, and I’m not going to pretend otherwise:\n\n- **Scheme** — my Emacs build has no Scheme grammar, so Scheme buffers use Hideshow with `hs-block-start-regexp` set to `\"^(\"` and `hs-forward-sexp-function` set to `forward-sexp`. A Scheme file is a sequence of top-level forms, so each definition is a fold, and collapsing the one you’re inside works from anywhere within it. That’s the behaviour I wanted from the start and Hideshow doesn’t guess it by default.\n- **Emacs Lisp** — the `prog-mode` fallback, same keys.\n- **XML** — folds from start tag to end tag using `nxml-scan-element-forward`, skipping `<?xml ?>`, comments, DOCTYPE and self-closing elements.\n- **YAML** (the non-Tree-sitter mode) — indentation-based blocks with a small `forward-sexp` of my own that walks to the end of the indented block.\n\nHideshow’s own fringe indicators (`hs-show-indicators` + `hs-indicator-type 'fringe`) are on in all of these, so it doesn’t feel like a downgrade from the Tree-sitter buffers.\n\nThe rule I enforce per buffer is **exactly one set of indicators**: Tree-sitter buffers get `treesit-fold` and Hideshow is turned off; Hideshow buffers get Hideshow and Outline’s clickable buttons are turned off so the two don’t double up; outline-only buffers (Markdown, diff) keep the buttons. Nobody ends up with two markers per block.\n\n## Org, which needed its own everything\n\nOrg folds with text properties, not overlays, so neither Tree-sitter nor Hideshow nor Outline’s buttons can draw a marker for it. So I wrote its own indicators: a fringe triangle pointing down for an open heading, right for a folded one, clickable with `mouse-1` to toggle exactly like `TAB`. Folding itself is untouched — `TAB`, `S-TAB` and the `C-c v` keys all still work.\n\nThe performance part is where the real win is. A naive “redraw indicators after every command” implementation is a disaster in a large file, because it re-searches the buffer and rebuilds every overlay on every pause. Mine is driven by the text instead:\n\n- if `buffer-chars-modified-tick` didn’t change, only the indicators that are **on screen** are redrawn — off-screen headings can wait until they’re scrolled to;\n- if it changed and the change was recorded, only the **dirty region** is rebuilt;\n- if it changed with no recorded change, only then is the whole buffer rebuilt.\n\nFolding changes text properties rather than characters, so folding alone takes the cheap path. Fold state is read back from Org on each redraw rather than remembered, so external folding can never leave a stale marker.\n\nTwo config choices matter just as much: `org-fold-core-style` is `text-properties` (so a visibility cycle doesn’t re-parse the file), `org-element-use-cache` is on, and Org starts with `showeverything` — which is both what I want (a note is read, not consulted) and the cheaper state, because it skips the visibility passes that walk every heading on open. I also removed a `org-restart-font-lock` that was re-fontifying the whole buffer a second time on open; JIT-lock handles what’s on screen.\n\n## It feels smooth, and that’s the whole point\n\nThe end result is that folding is *boring* now. `C-c v c` in Kotlin, Scheme, Elisp, Org, XML, YAML, Markdown. Guides that look like brackets. Indicators quiet enough to ignore until you need them. No pause on a 3000-line Org file. No two competing markers.\n\nIt honestly changes how much I collapse and expand while reading code. I used to avoid folding because it felt fiddly; now it’s free and mouse friendly.\n\nIf you want to read the actual code: it’s in [dev-folding.el](https://codeberg.org/jjba23/heks-emacs/src/branch/trunk/src/modules/dev-folding.el) for the engines and indicators, the `heks/fold-*` wrappers in `heks-emacs.el` for the keys, and the Org indicators live in `org.el`. Everything is GPL-3.0, and it’s tested: `maak verify` asserts a fold actually covers a whole definition and that the documented keys are bound, and `maak keys` fails on a key that does two jobs or names a command that doesn’t exist.\n\nHappy to answer questions about any of it, especially if you’ve fought the `treesit-fold-indicators` bitmaps yourself, because that one cost me an evening and I’d love to know if there’s a cleaner way.\n","body_html":"<h1 id=\"emacs-finally-folds-to-my-liking\">Emacs finally folds to my liking</h1>\n<p>23/10/2026 · 11 minutes</p>\n<p>I’ve been slowly turning <a href=\"https://codeberg.org/jjba23/heks-emacs\" rel=\"nofollow ugc noopener\">my Emacs config</a> into something I actually <em>enjoy</em> using, not just something that works, and folding was the last place where that wasn’t true.</p>\n<p>Every mode had its own idea of what folding is: <code>treesit-fold</code> in Tree-sitter modes, Hideshow in the Lisp-ish ones, Outline in everything that resembles text, Org with its own invisible text that no one else’s commands can undo.</p>\n<p>So every buffer had different keys, a different look, and worst of all different <em>performance</em> characteristics. Folding a big file sometimes felt like holding my breath.</p>\n<p>I’m running Emacs 32.0.50 from <code>master</code> (built from source, as one does), and it’s finally one system.</p>\n<figure><img src=\"https://jointhefreeworld.org/static-assets/heks/screen-2026-10-23-16-17-32.png\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<h2 id=\"kirigami-is-the-piece-i-didn-t-know-i-needed\">kirigami is the piece I didn’t know I needed</h2>\n<p><a href=\"https://github.com/jamescherti/kirigami.el\" rel=\"nofollow ugc noopener\">kirigami</a> doesn’t fold anything itself. It <em>dispatches</em>: it keeps a list of folding engines, checks which one is actually active in the buffer, and forwards to it. Tree-sitter, Hideshow, Outline, Org, Vdiff — all behind one interface.</p>\n<p>So on top of it I put one set of keys, the same in a Kotlin buffer, YAML, a Scheme or ELisp buffer and an Org file:</p>\n<div class=\"table-wrap\"><table><thead><tr><th>Key</th><th>Does</th></tr></thead><tbody><tr><td><code>C-c v c</code></td><td>close this fold</td></tr><tr><td><code>C-c v C</code></td><td>close every fold</td></tr><tr><td><code>C-c v o</code></td><td>open this fold</td></tr><tr><td><code>C-c v O</code></td><td>open this fold and everything under it</td></tr><tr><td><code>C-c v r</code></td><td>reveal everything</td></tr><tr><td><code>C-c v t</code></td><td>toggle the fold at point</td></tr></tbody></table></div>\n<p>That sounds small. It is not small. The number of times a day I used to think “wait, what does TAB do in this mode again” is now zero, and the muscle memory transfers across an entire polyglot config.</p>\n<p>One wrinkle worth mentioning: kirigami answers with the <em>first</em> engine in its list whose mode is on, and <code>outline-minor-mode</code> is on in nearly every buffer I have. In the buffers where I deliberately turned on Hideshow — Scheme, for instance — that choice has to win, otherwise <code>C-c v c</code> folds “headings” a Scheme file doesn’t have. So I reorder <code>kirigami-fold-list</code> to put Hideshow first instead of rewriting it; engines kirigami adds later still get used wherever Hideshow is off.</p>\n<h2 id=\"tree-sitter-folding-with-guides-that-are-actually-lines\">Tree-sitter folding, with guides that are actually lines</h2>\n<p>Most of my programming buffers are <code>*-ts-mode</code>, so <code>treesit-fold</code> does the work there — bash, C, C++, CMake, CSS, Dockerfile, Elixir, Go, HTML, Java, JS/TS/TSX, JSON, Kotlin, Lua, Nix, PHP, Python, Ruby, Rust, Scala, TOML, YAML. Line counts are shown inline, which I find genuinely useful when deciding whether to open a fold.</p>\n<p>The indicators took the most fiddling. <code>treesit-fold-indicators</code> draws a block’s guide with a bitmap 23 rows tall, centered in the line beside it. A line in my config is 29 pixels — the font’s 23 plus six of <code>line-spacing</code> — so every row of the guide was a 23-pixel dash with three pixels of nothing above and below it. A folded block looked like a <em>dotted</em> line running down the fringe. Not subtle, just wrong.</p>\n<p>There’s no setting for that, so I replaced the bitmaps: a single-row bitmap that <code>define-fringe-bitmap</code> repeats down however tall the line is (no spacing can introduce a gap), plus a <code>+=/</code>-= box and an end-elbow cut to the exact height of a line, so the guide runs from the marker into the row below and turns toward the text where the block ends. It’s an advice on the function that builds the string for each line. It looks like a bracket now, not a perforation.</p>\n<p>And the colour: the indicators inherited <code>shadow</code>, which most themes make as bright as a comment. Fine for one marker on one line, wrong for a marker on <em>every</em> line of a block — the grey became a rule drawn through the code. They’re now <code>shadow</code> blended 50% toward the theme’s background, recomputed on every theme and scale change, so it fades correctly on light themes too and no theme needs a special entry.</p>\n<h2 id=\"hideshow-earns-its-keep\">Hideshow earns its keep</h2>\n<p>Not everything has a grammar, and I’m not going to pretend otherwise:</p>\n<ul><li><strong>Scheme</strong> — my Emacs build has no Scheme grammar, so Scheme buffers use Hideshow with <code>hs-block-start-regexp</code> set to <code>&quot;^(&quot;</code> and <code>hs-forward-sexp-function</code> set to <code>forward-sexp</code>. A Scheme file is a sequence of top-level forms, so each definition is a fold, and collapsing the one you’re inside works from anywhere within it. That’s the behaviour I wanted from the start and Hideshow doesn’t guess it by default.</li><li><strong>Emacs Lisp</strong> — the <code>prog-mode</code> fallback, same keys.</li><li><strong>XML</strong> — folds from start tag to end tag using <code>nxml-scan-element-forward</code>, skipping <code>&lt;?xml ?&gt;</code>, comments, DOCTYPE and self-closing elements.</li><li><strong>YAML</strong> (the non-Tree-sitter mode) — indentation-based blocks with a small <code>forward-sexp</code> of my own that walks to the end of the indented block.</li></ul>\n<p>Hideshow’s own fringe indicators (<code>hs-show-indicators</code> + <code>hs-indicator-type &#39;fringe</code>) are on in all of these, so it doesn’t feel like a downgrade from the Tree-sitter buffers.</p>\n<p>The rule I enforce per buffer is <strong>exactly one set of indicators</strong>: Tree-sitter buffers get <code>treesit-fold</code> and Hideshow is turned off; Hideshow buffers get Hideshow and Outline’s clickable buttons are turned off so the two don’t double up; outline-only buffers (Markdown, diff) keep the buttons. Nobody ends up with two markers per block.</p>\n<h2 id=\"org-which-needed-its-own-everything\">Org, which needed its own everything</h2>\n<p>Org folds with text properties, not overlays, so neither Tree-sitter nor Hideshow nor Outline’s buttons can draw a marker for it. So I wrote its own indicators: a fringe triangle pointing down for an open heading, right for a folded one, clickable with <code>mouse-1</code> to toggle exactly like <code>TAB</code>. Folding itself is untouched — <code>TAB</code>, <code>S-TAB</code> and the <code>C-c v</code> keys all still work.</p>\n<p>The performance part is where the real win is. A naive “redraw indicators after every command” implementation is a disaster in a large file, because it re-searches the buffer and rebuilds every overlay on every pause. Mine is driven by the text instead:</p>\n<ul><li>if <code>buffer-chars-modified-tick</code> didn’t change, only the indicators that are <strong>on screen</strong> are redrawn — off-screen headings can wait until they’re scrolled to;</li><li>if it changed and the change was recorded, only the <strong>dirty region</strong> is rebuilt;</li><li>if it changed with no recorded change, only then is the whole buffer rebuilt.</li></ul>\n<p>Folding changes text properties rather than characters, so folding alone takes the cheap path. Fold state is read back from Org on each redraw rather than remembered, so external folding can never leave a stale marker.</p>\n<p>Two config choices matter just as much: <code>org-fold-core-style</code> is <code>text-properties</code> (so a visibility cycle doesn’t re-parse the file), <code>org-element-use-cache</code> is on, and Org starts with <code>showeverything</code> — which is both what I want (a note is read, not consulted) and the cheaper state, because it skips the visibility passes that walk every heading on open. I also removed a <code>org-restart-font-lock</code> that was re-fontifying the whole buffer a second time on open; JIT-lock handles what’s on screen.</p>\n<h2 id=\"it-feels-smooth-and-that-s-the-whole-point\">It feels smooth, and that’s the whole point</h2>\n<p>The end result is that folding is <em>boring</em> now. <code>C-c v c</code> in Kotlin, Scheme, Elisp, Org, XML, YAML, Markdown. Guides that look like brackets. Indicators quiet enough to ignore until you need them. No pause on a 3000-line Org file. No two competing markers.</p>\n<p>It honestly changes how much I collapse and expand while reading code. I used to avoid folding because it felt fiddly; now it’s free and mouse friendly.</p>\n<p>If you want to read the actual code: it’s in <a href=\"https://codeberg.org/jjba23/heks-emacs/src/branch/trunk/src/modules/dev-folding.el\" rel=\"nofollow ugc noopener\">dev-folding.el</a> for the engines and indicators, the <code>heks/fold-*</code> wrappers in <code>heks-emacs.el</code> for the keys, and the Org indicators live in <code>org.el</code>. Everything is GPL-3.0, and it’s tested: <code>maak verify</code> asserts a fold actually covers a whole definition and that the documented keys are bound, and <code>maak keys</code> fails on a key that does two jobs or names a command that doesn’t exist.</p>\n<p>Happy to answer questions about any of it, especially if you’ve fought the <code>treesit-fold-indicators</code> bitmaps yourself, because that one cost me an evening and I’d love to know if there’s a cleaner way.</p>","headings":[{"level":1,"text":"Emacs finally folds to my liking","id":"emacs-finally-folds-to-my-liking"},{"level":2,"text":"kirigami is the piece I didn’t know I needed","id":"kirigami-is-the-piece-i-didn-t-know-i-needed"},{"level":2,"text":"Tree-sitter folding, with guides that are actually lines","id":"tree-sitter-folding-with-guides-that-are-actually-lines"},{"level":2,"text":"Hideshow earns its keep","id":"hideshow-earns-its-keep"},{"level":2,"text":"Org, which needed its own everything","id":"org-which-needed-its-own-everything"},{"level":2,"text":"It feels smooth, and that’s the whole point","id":"it-feels-smooth-and-that-s-the-whole-point"}]}}