From e7e9c55ba745e5499dc4d2370fda481b7270d74a Mon Sep 17 00:00:00 2001 From: James Cherti Date: Thu, 4 Jun 2026 16:55:10 -0400 Subject: [PATCH] Add outline-show-entry-and-parents to reveal entry hierarchy * lisp/outline.el (outline-mode-prefix-map): Rebind 'C-e' from 'outline-show-entry' to 'outline-show-entry-and-parents'. (outline-mode-menu-bar-map): Use 'outline-show-entry-and-parents' instead of 'outline-show-entry'. (outline-isearch-open-invisible): Use the new command instead of the 'outline-show-entry' primitive. This prevents unintended side effects for packages relying on the base API and avoids the 'isolated item' effect. (outline-show-entry-and-parents): New function to climb the tree, reveal parent headings, and unfold the current entry (bug#79286). --- etc/NEWS | 6 +++++ lisp/outline.el | 69 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/etc/NEWS b/etc/NEWS index 5698c577445..158081f1195 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -88,6 +88,12 @@ This means it won't get in your way even if it's slow for your repository. As such, the 'vc-dir-show-outgoing-count' option is now obsolete. +** Outline mode + +*** New command 'outline-show-entry-and-parents'. +It is bound to 'C-e' and reveals the current entry +with its parent hierarchy. + * New Modes and Packages in Emacs 32.1 diff --git a/lisp/outline.el b/lisp/outline.el index ea66ee5c8e9..250723f4b77 100644 --- a/lisp/outline.el +++ b/lisp/outline.el @@ -86,7 +86,7 @@ imitate the function `looking-at'.") "C-t" #'outline-hide-body "C-a" #'outline-show-all "C-c" #'outline-hide-entry - "C-e" #'outline-show-entry + "C-e" #'outline-show-entry-and-parents "C-l" #'outline-hide-leaves "C-k" #'outline-show-branches "C-q" #'outline-hide-sublevels @@ -130,8 +130,8 @@ imitate the function `looking-at'.") (define-key map [show outline-show-branches] '(menu-item "Show Branches" outline-show-branches :help "Show all subheadings of this heading, but not their bodies")) - (define-key map [show outline-show-entry] - '(menu-item "Show Entry" outline-show-entry + (define-key map [show outline-show-entry-and-parents] + '(menu-item "Show Entry" outline-show-entry-and-parents :help "Show the body directly following this heading")) (define-key map [show outline-show-all] '(menu-item "Show All" outline-show-all @@ -1095,7 +1095,7 @@ If FLAG is nil then text is shown, while if FLAG is t the text is hidden." ;; `outline-flag-region'). (defun outline-isearch-open-invisible (_overlay) ;; We rely on the fact that isearch places point on the matched text. - (outline-show-entry)) + (outline-show-entry-and-parents)) (defun outline-hide-entry () "Hide the body directly following this heading." @@ -1123,6 +1123,67 @@ Show the heading too, if it is currently invisible." (define-obsolete-function-alias 'show-entry #'outline-show-entry "25.1") +(defun outline-show-entry-and-parents () + "Reveal the current entry and its parent hierarchy. +This command ensures that the current entry, all of its ancestor +headings, and their immediate sibling headings are visible. + +The function iteratively unfolds the children and body of the target +entry until it is fully revealed. If invoked when the point is inside +a completely hidden subtree, it manages the visibility state to avoid +leaving the buffer in an inconsistent layout. This guarantees a safe +and predictable visual expansion." + (interactive) + ;; Wrap in `save-match-data' because outline functions use regular + ;; expressions. Without this, calling `outline-show-entry-and-parents' + ;; programmatically would clobber the caller's match data, leading to + ;; subtle, hard-to-trace bugs. + (save-match-data + ;; Repeatedly expand the outline structure at point from the outside + ;; in until the target text is fully visible. + ;; + ;; Think of this block as manually opening nested folds: + ;; - It checks whether the heading at point is folded. + ;; - If it is folded, it moves backward to that parent heading. + ;; - It opens the heading to reveal its text and subheadings. + ;; - It repeats this process layer by layer down to the target. + (let (heading-point + prior-heading-point) + (while (condition-case nil + (save-excursion + ;; Workaround: `outline-back-to-heading' throws an + ;; `outline-before-first-heading' error if the + ;; heading is on the first line (e.g., in + ;; `markdown-ts-mode') and point is deep within the + ;; hidden body of that folded first heading. + (vertical-motion 0) + ;; Navigate backward to the nearest visible heading + (outline-back-to-heading) + (setq heading-point (point)) + ;; Break the loop if we stop making progress, + ;; preventing infinite recursion + (if (eq heading-point prior-heading-point) + ;; Break out of the loop + nil + (setq prior-heading-point heading-point) + ;; Check if the heading is folded by inspecting the + ;; end of the line + (when (invisible-p (pos-eol)) + ;; Ignore errors to guarantee the target entry is + ;; still revealed via `outline-show-entry' even + ;; if a buggy third-party `outline-level' + ;; function fails during child expansion. + (ignore-errors (outline-show-children)) + + ;; Show the body directly following this heading + (outline-show-entry) + + ;; Return t to continue drilling down to the next + ;; layer of the outline hierarchy + t))) + (outline-before-first-heading + nil)))))) + (defun outline-hide-body () "Hide all body lines in buffer, leaving all headings visible. Note that this does not hide the lines preceding the first heading line."