Emacs, scripting and anything text oriented.

Org: Show only Post subtree headings

Kaushal Modi

How to define a custom org-global-cycle-like command that collapses only the Org subtrees with specific properties.

I start this post by introducing what the Org mode global cycling command does, what kind of subtree folding I actually need, and then share the solution with code snippets.

Org Global Cycle #

Org mode has a built-in org-global-cycle command that you might be familiar with. It’s bound by default to the S-TAB key. Each time this command is called, the Org buffer will cycle through these states:

  1. Overview: Show only the Level 1 headings and collapse everything underneath.
  2. Contents: Show only the Org headings and collapse all the content.
  3. Show All: Expand all the headings and show their contents too.

If a numeric prefix N is used with this command, it will show only the Org headings up to Level N. For example, C-2 S-TAB will show only the headings up to Level 2.

This is a really helpful command, but I needed something different ..

Skeleton of only Post headings #

I maintain most of this website’s content in a single Org file. I have dozens of blog posts organized in Org subtrees, which I further organize under “category” headings .. It kind of looks like the below mock-up:  It’s amazing how many features PlantUML has. If you are interested in creating diagrams like these, check out the PlantUML Salt syntax.

Figure 1: Post Subtrees at arbitrary heading levels

Figure 1: Post Subtrees at arbitrary heading levels

As we can see,

  • All the post subtrees are not at Level 1 headings.
  • They are also not at a fixed Level N.
  • The heading level of the post depends on how many parent categories that post has (and that will also change over time).

I needed to basically show everything leading up to a post subtree heading, and then collapse all the content under that post; even the sub-headings.

The “Collapse All Posts” function #

The modi/org-hugo-collapse-all-posts function defined below meets the above requirement:

  1. It first widens the whole buffer and expands all the headings.
  2. Then it loops through all the headings and collapses all the post subtrees i.e. all the subtrees that have the EXPORT_FILE_NAME property set. This is where I use the org-map-entries magic.
  3. Finally it looks for Org headings that begin with “Footnotes” or “COMMENT” and collapses them as well.

I am using the development version of Org mode (version 9.6, yet to be released as of <2022-06-15 Wed>) which has the new org-fold library. This library obsoletes the use of outline.el library and other code-folding related functions in Org mode. So cl-flet is used to create function symbol aliases that use the org-fold-* functions if available, otherwise they fall back to the legacy functions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(defun modi/org-hugo-collapse-all-posts ()
  "Collapse all post subtrees in the current buffer.
Also collapse the Footnotes subtree and COMMENT subtrees if
present.

A post subtree is one that has the EXPORT_FILE_NAME property
set."
  (interactive)
  (cl-flet ((show-all (if (fboundp 'org-fold-show-all)
                          #'org-fold-show-all
                        #'org-show-all))
            (hide-subtree (if (fboundp 'org-fold-hide-subtree)
                              #'org-fold-hide-subtree
                            #'outline-hide-subtree)))
    (widen)
    (show-all '(headings))
    ;; Collapse all the post subtrees (ones with EXPORT_FILE_NAME
    ;; property set).
    (org-map-entries #'hide-subtree "EXPORT_FILE_NAME<>\"\"" 'file)
    ;; Also hide Footnotes and comments.
    (save-excursion
      (goto-char (point-min))
      (while (re-search-forward "^\\(\\* Footnotes\\|\\*+ COMMENT\\)"
                                nil :noerror)
        (hide-subtree)))))
Code Snippet 1: Function that collapses all the post subtrees in the current buffer

Binding with C-u C-c TAB #

The function is ready, but let’s now add a bit of convenience to it.

If a point is under a subtree, C-c TAB will collapse that subtree while showing only Level 1 headings, and if a numeric prefix is used, it will show only those many levels of headings. I decided to bind the above function to C-u C-c TAB because,

  1. The behavior of modi/org-hugo-collapse-all-posts falls in the same category as that of C-c TAB.
  2. The C-u C-c .. binding rolls off the fingers pretty nicely 😃.

This binding is achieved using one of my favorite Emacs features .. the advice system. The :before-until Advice Combinator is used here, which means that if the advising function (below) returns a nil, the advised or the original function org-ctrl-c-tab is not called.

The advising function below detects if the C-u prefix argument is used. If it is, the modi/org-hugo-collapse-all-posts function is called, otherwise the original org-ctrl-c-tab function is called.

1
2
3
4
5
6
7
8
9
(defun modi/org-ctrl-c-tab-advice (&rest args)
  "Run `modi/org-hugo-collapse-all-posts' when
doing \\[universal-argument] \\[org-ctrl-c-tab]."
  (let ((do-not-run-orig-fn (equal '(4) current-prefix-arg)))
    (when do-not-run-orig-fn
      (modi/org-hugo-collapse-all-posts))
    do-not-run-orig-fn))

(advice-add 'org-ctrl-c-tab :before-until #'modi/org-ctrl-c-tab-advice)
Code Snippet 2: Bind C-u C-c TAB to call modi/org-hugo-collapse-all-posts

Result #

After evaluating the above two snippets, when I do C-u C-c TAB in my “blog posts” Org buffer, I see this:

Figure 2: My “blog posts” Org buffer showing only the Post subtree headings

Figure 2: My “blog posts” Org buffer showing only the Post subtree headings

It matches that earlier mockup — Mission accomplished! 💯


Versions used: emacs 28.1.50 , org release_9.5.4-549-gaa789b
This is Day 30 of #100DaysToOffload.
Webmentions
Comment by Kaushal on Thu Jun 16, 2022 09:49 EDT

@yantan92

Have you seen org-sparse-tree bound to C-c /?

I had tried it earlier and I tried it again when you suggested, but I don’t seem to get the subtrees filtered exactly as above.

  • I tried the [r]egexp filter to match EXPORT_FILE_NAME, but that shows the property drawers expands as the regexp matches happen in those.

  • I tried [p]roperty match, but couldn’t figure out how to enter an equivalent of EXPORT_FILE_NAME &gt; "".

I looked at (org) Sparse Trees, but I failed to get what I want. Maybe I need to invest more time in understanding how exactly the Org Sparse Tree feature works.

Can you suggest what filter I can use in org-sparse-tree so that I get the view you see in the buffer screenshot above?

Thanks!

Comment by yantar92 on Thu Jun 16, 2022 09:19 EDT
Have you seen org-sparse-tree bound to C-c /?