Subject: bug#79286: outline-mode: attempting to open folded sections does not work as expected inside a deeply nested header

From: James Cherti <[email-masked]>
Date: Thu, 21 Aug 2025 15:09:19 -0400

On 2025-08-21 14:34, Rahguzar wrote:
> Hi James,

Hi Rahguzar,

> James Cherti <[email-masked]> writes:
> 
>> When using outline-mode or outline-minor-mode, attempting to
>> open folded sections (outline-show-entry) does not work as
>> expected when the cursor is positioned inside a deeply
>> nested header.
>>
>> Steps to Reproduce:
>> -------------------
>> 1. Open an outline-mode buffer with multiple
>>      nested headers, for example:
>>
>> Level 1
>> ** Level 2
>> *** Level 3
>> **** Level 4: A
>> Some content here
>> Some content here
>> Some content here
>> Some content here
>> **** Level 4: B
>> Some content here
>> Some content here
>> Some content here
>> Some content here
>> **** Level 4: C
>> Some content here
>> Some content here< PUT THE CURSOR HERE
>> Some content here
>> Some content here
>> **** Level 4: D
>> Some content here
>> Some content here
>> Some content here
>> Some content here
>> Some content here
>> **** Level 4: E
>> Some content here
>> Some content here
>> Some content here
>>
>> 2. Put the cursor before `< PUT THE CURSOR HERE`
>>
>> 3. Collapse all folds with: (hide-sublevels 1)
>>
>> 6. Show entry: (outline-show-entry)
>>
>> The folds fail to expand fully in nested hierarchies, leaving all
>> content hidden except for `level 4: C`.
>>
>> Screenshot:
>> https://github.com/user-attachments/assets/2ea6c3bd-0284-40a7-90e5-1619bdf3acc5
>>
>> Expected Behavior:
>> ------------------
>> The expected behavior is that only the heading at point (and
>> its associated content) should be expanded, while all
>> ancestor headings remain collapsed. (Similar to other editors.)
>>
>> Actual Behavior:
>> ----------------
>> Only the heading at point expands, while other headings
>> remain invisible, which is confusing. In some cases, these
>> hidden headings cannot be revealed until the parent heading
>> is first collapsed and then expanded again.
> 
> I have encountered this with isearch and was able to fix this
> with in my init file.
> 
> ```emacs-lisp
>    (defun +outline-reveal (pos)
>      (let ((heading-pos nil))
>        (while (not (eq heading-pos (progn (outline-back-to-heading) (point))))
>          (setq heading-pos (point))
>          (outline-show-children)
>          (goto-char pos))
>        (outline-show-entry)
>        (goto-char pos)))
> 
>    (defun +outline-invsible-open (&optional _)
>      (when (and outline-minor-mode
>                 (outline-invisible-p))
>        (+outline-reveal (point))))
> 
>    (setq outline-isearch-open-invisible-function #'+outline-invsible-open)
> ```
> With an interactive spec the function +outline-reveal can be used as an
> alternative to outline-show-entry without this problem but I can't
> imagine a scenario where that might be needed.
> 

Unfortunately, neither `+outline-reveal` nor `+outline-invisible-open` 
resolves this issue.

I wrote the following Elisp code to fix this issue:
--8<---------------cut here---------------start------------->8---
;; Author James Cherti
;; GitHub: https://github.com/jamescherti
(defun my-outline-show-entry ()
   "Show the body directly following this heading.
Show the heading too, if it is currently invisible."
   (interactive)

   ;; Show previous headers
   (unless (derived-mode-p 'outline-indent-minor-mode)
     (save-excursion
       (let ((func-invisible-p
              (cond
               ((and (derived-mode-p 'org-mode)
                     (fboundp 'org-invisible-p))
                'org-invisible-p)

               ((and (or (derived-mode-p 'outline-mode)
                         (derived-mode-p 'outline-minor-mode))
                     (fboundp 'outline-invisible-p))
                'outline-invisible-p)

               (t
                'invisible-p)))
             (initial-point (point))
             (visual-point nil)
             (visual-init-point nil))
         ;; Unfolding loop
         (while (and
                 ;; Folded?
                 (save-excursion
                   (end-of-line)
                   (funcall func-invisible-p (point)))
                 ;; When the real point differs from the visual point, 
it indicates
                 ;; that the cursor is still located within a hidden header.
                 ;;
                 ;; This prevents infinite loops if the unfolding stops 
changing
                 ;; the cursor position.
                 (or
                  (not visual-init-point)
                  (not visual-point)
                  (/= visual-init-point visual-point)))

           (save-excursion
             (outline-back-to-heading)
             (beginning-of-line)
             (setq visual-point (point))
             (with-no-warnings
               (show-children))

             ;; Go back to point
             (goto-char initial-point)
             (outline-back-to-heading)
             (beginning-of-line)
             (setq visual-init-point (point))))

         ;; Previous version of `outline-show-entry'
         (save-excursion
           (outline-back-to-heading t)
           (outline-flag-region (1- (point))
                                (progn
                                  (outline-next-preface)
                                  (if (= 1 (- (point-max) (point)))
                                      (point-max)
                                    (point)))
                                nil))))))

(advice-add 'outline-show-entry :override #'my-outline-show-entry)
--8<---------------cut here---------------start------------->8---

This could likely be simplified using built-in functions.
I welcome any suggestions for improvement.

>> Environment:
>> ------------
>> * Emacs version: 30.2 and master branch
>> (ade6608e2587452c8ea565ce3057879379ebd0b5)
>>
>> --
>> James Cherti
>> GitHub: https://github.com/jamescherti
>> Website: https://www.jamescherti.com/
> 
> Rahguzar

--
James Cherti
GitHub: https://github.com/jamescherti
Website: https://www.jamescherti.com/