6

How can I test in elisp whether an org-mode tree node (either a heading or a list item) is folded?

Looking at the code it seems the logic is a little complicated and only in org-cycle-internal-local, with no public API?

I've sometimes wished for this when writing small functions to navigate the tree and change its folding state - e.g. today when attempting to answer this question.

Note that if the node has no children, for my purposes I'd either need that to be regarded as folded, or to be able to find out explicitly whether there are children. Otherwise I can't keep org-cycleing until the node is folded.

Croad Langshan
  • 3,192
  • 14
  • 42
  • Yes, you should utilize the same system of `org-cycle-internal-local`. You'll need to spend more time examining/understanding how that function works. As you can see, part of what happens is that the `'invisible` character property (e.g., an overlay) is either present or not present in the area of the task/event that is folded/unfolded. For example, place your cursor immediately before the folded region and type: `M-x eval-expression RET (get-char-property (point) 'invisible) RET` Then try typing at the same location: `C-u C-x =` (aka **what-cursor-position**) to see the overlay details. – lawlist Sep 04 '16 at 16:23

2 Answers2

5

There's a simple way to test whether a heading or list item is folded: the text within it will be invisible. As such, you can use:

(defun org-folded-p ()
  "Returns non-nil if point is on a folded headline or plain list
item."
  (and (or (org-at-heading-p)
           (org-at-item-p))
       (invisible-p (point-at-eol))))

Note that, if you'd like to use it in non-org modes derived from outline-mode, you should swap in outline-on-heading-p instead.

Dan
  • 32,584
  • 6
  • 98
  • 168
  • Nice -- but I don't think this meets the "no children" condition (final paragraph of question) – Croad Langshan Sep 05 '16 at 00:18
  • @CroadLangshan: This post was to answer the question in the first sentence ("How can I test in elisp whether an `org-mode` tree node (either a heading or a list item) is folded?") The rest of the post is actually a second question ("how to change a folding state") -- it might be best to split the original post into two discrete questions. – Dan Sep 05 '16 at 00:24
  • Sure. But to be honest I think it's common (in any context, not just SE) to lie a little up front for clarity when explaining anything, so it's necessary to read the whole of a question to correctly answer it. Perhaps I should have opened the question with "can it be folded", which is more precisely what the question is about: that is a reasonable single question, which my (ugly!) answer does in fact answer. But I judged it a little less obvious in meaning than "is it folded", therefore perhaps not the best first sentence... – Croad Langshan Sep 05 '16 at 00:38
  • @CroadLangshan: "Lying a little up front" is a bad idea and adds to the babel, which makes it harder for people to find information. Readers should not need to intuit what you actually want. Both the subject line and the first sentence ask about folding, and the latter is the only real question in the post. Think about what a different user will think a year from now when s/he searches for information about folding, and comes up with a post that *looks* like it's exactly the question, and then turns out to be something different. – Dan Sep 05 '16 at 13:17
  • Readers don't need to intuit what I want because I explained that explicitly in the question. In response to your pragmatic point: despite my hyperbole about 'lying', I don't think it's possible to reach SE title nirvana, and my best guess is such a reader looking later is likely to face the same problem as me, and to think about it the same way. – Croad Langshan Sep 11 '16 at 16:20
  • there is a problem. if the current tree was just closed with `evil-close-fold`, then `(org-at-heading-p)` will return nil. – Toothrot Jan 04 '20 at 22:29
0

I took a crack at it, based on that function (no tests, sorry for weird indentation, likely some redundancy and I'm sure poor style):

(defun my/org-get-folded-state ()
    (cond
        ((not (or (org-at-item-p) (org-at-heading-p)))
            (message "not at node (neither heading nor list item)")
            'not-at-node)
        ((org-before-first-heading-p)
            (message "not at node (neither heading nor list item)")
            'not-at-node)
        (t
            (let (eoh eol eos has-children children-skipped struct)
                ;; First, determine end of headline (EOH), end of subtree or item
                ;; (EOS), and if item or heading has children (HAS-CHILDREN).
                (save-excursion
                    (if (org-at-item-p)
                        (progn
                            (beginning-of-line)
                            (setq struct (org-list-struct))
                            (setq eoh (point-at-eol))
                            (setq eos (org-list-get-item-end-before-blank (point) struct))
                            (setq has-children (org-list-has-child-p (point) struct)))
                        (org-back-to-heading)
                        (setq eoh (save-excursion (outline-end-of-heading) (point)))
                        (setq eos (save-excursion (org-end-of-subtree t t)
                                    (when (bolp) (backward-char)) (point)))
                        (setq has-children
                            (or (save-excursion
                                    (let ((level (funcall outline-level)))
                                        (outline-next-heading)
                                        (and (org-at-heading-p t)
                                            (> (funcall outline-level) level))))
                                (save-excursion
                                    (org-list-search-forward (org-item-beginning-re) eos t)))))
                    ;; Determine end invisible part of buffer (EOL)
                    (beginning-of-line 2)
                    (while (and (not (eobp)) ;; this is like `next-line'
                            (get-char-property (1- (point)) 'invisible))
                        (goto-char (next-single-char-property-change (point) 'invisible))
                        (and (eolp) (beginning-of-line 2)))
                    (setq eol (point)))
                (cond
                    ((= eos eoh)
                        (message "empty node")
                        'empty-node)
                    ((or (>= eol eos)
                        (not (string-match "\\S-" (buffer-substring eol eos))))
                        (message "folded")
                        'folded)
                    (t
                        (message "not folded")
                        'not-folded))))))
Croad Langshan
  • 3,192
  • 14
  • 42