bug#81512: [PATCH] Add command `outline-occur'

Roi Martin <[email protected]> Mon, 03 Aug 2026 21:47:45 +0200
Newsgroups gmane.emacs.bugs
Message-ID <[email protected]>
--=-=-=
Content-Type: text/plain

Roi Martin <[email protected]> writes:

> Juri Linkov <[email protected]> writes:
>
>>>> It's fine for packages to add own rules to the existing value of
>>>> 'display-buffer-default-alist'.  Then the last package wins
>>>> when the first rule will match, e.g.:
>>>>
>>>>    (((category . bookmark-jump)
>>>>       display-buffer-for-package-2
>>>>       (package . 2))
>>>>     ((category . bookmark-jump)
>>>>       display-buffer-for-package-1
>>>>       (package . 1))
>>>
>>> This means that once the user has loaded package-2, the behavior
>>> prescribed by the previously loaded package-1 is lost.  This cannot
>>> happen if package designers are told to let-bind that variable.
>>
>> We can't tell authors what they should do.  But it's fine to add
>> a suggestion in the docstring that it's intended to be used with let-bind.
>
> If we all agree in the changes below, I'll send the v4 of the patch.
>
> - Rename the variable to `display-buffer-default-alist'.
> - Add a note in the docstring of `display-buffer-default-alist' as well
>   as the corresponding section of the Elisp manual saying that this
>   variable is intended to be let-bound (rather than saying that "it
>   should never be assigned buffer-locally or globally" as in the v2 of
>   patch).
>
> Juri, Martin are you OK?
>
>         Roi

Please, find attached the v4 of the patch.  Besides the discussed
changes, I also documented `display-buffer-default-alist' under the Info
node `(elisp) Precedence of Action Functions'.

        Roi


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=v4-0001-Add-outline-occur-command-and-display-buffer-defa.patch

From 3e9b47afc5f37e3876c324a603f37424634d7fc4 Mon Sep 17 00:00:00 2001
From: Roi Martin <[email protected]>
Date: Tue, 28 Jul 2026 20:19:03 +0200
Subject: [PATCH v4] Add `outline-occur' command and
 `display-buffer-default-alist' variable

Add the `outline-occur' command that allows navigating the current
buffer's outline using Occur.  Also add the
`display-buffer-default-alist' variable, which Lisp programs may
let-bind to specify conditional actions for nested `display-buffer'
calls.

* doc/lispref/windows.texi (Choosing Window)
(Precedence of Action Functions): Document
`display-buffer-default-alist' variable.
* etc/NEWS: Add `outline-occur' and `display-buffer-default-alist'
entries.
* lisp/outline.el (outline-mode-prefix-map): Bind `outline-occur'.
(outline-mode-menu-bar-map): Add `outline-occur' menu item.
(outline-occur-regexp): New buffer-local variable.
(outline-occur): New command.
* lisp/replace.el (occur-1): Call `display-buffer' with category.
* lisp/window.el (display-buffer-alist): Update docstring.
(display-buffer-default-alist): New variable.
(display-buffer): Handle `display-buffer-default-alist' variable.
* test/lisp/outline-resources/outline.txt: New test file.
* test/lisp/outline-tests.el: New test suite.  (Bug#81512)

Co-authored-by: Martin Rudalics <[email protected]>
Co-authored-by: Juri Linkov <[email protected]>
---
 doc/lispref/windows.texi                | 37 +++++++++++--
 etc/NEWS                                | 12 +++++
 lisp/outline.el                         | 31 ++++++++++-
 lisp/replace.el                         |  2 +-
 lisp/window.el                          | 30 ++++++++---
 test/lisp/outline-resources/outline.txt |  2 +
 test/lisp/outline-tests.el              | 69 +++++++++++++++++++++++++
 7 files changed, 172 insertions(+), 11 deletions(-)
 create mode 100644 test/lisp/outline-resources/outline.txt
 create mode 100644 test/lisp/outline-tests.el

diff --git a/doc/lispref/windows.texi b/doc/lispref/windows.texi
index ddc8524ddb6b..3189179c4e5a 100644
--- a/doc/lispref/windows.texi
+++ b/doc/lispref/windows.texi
@@ -3119,6 +3119,9 @@ Choosing Window
 @item
 The user option @code{display-buffer-alist}.
 
+@item
+The variable @code{display-buffer-default-alist}.
+
 @item
 The @var{action} argument.
 
@@ -3181,6 +3184,13 @@ Choosing Window
 default value is an empty display action, i.e., @w{@code{(nil . nil)}}.
 @end defvar
 
+@defvar display-buffer-default-alist
+The value of this variable is an alist mapping conditions to display
+actions (@pxref{display-buffer-alist}).  Lisp programs may let-bind it
+to specify conditional actions for nested @code{display-buffer} calls.
+@end defvar
+
+@anchor{display-buffer-alist}
 @defopt display-buffer-alist
 The value of this option is an alist mapping conditions to display
 actions.  Each condition is passed to @code{buffer-match-p}
@@ -4475,10 +4485,31 @@ Precedence of Action Functions
 the window already showing @file{*foo*} since both functions supplied
 by the @var{action} argument try to reuse such a window first.
 
-   By setting the @var{action} argument, an application effectively
+   There are cases, however, where Lisp programs want to modify the
+default behavior of a @code{display-buffer} call hidden behind another
+function.  In those cases, setting the @var{action} argument is not an
+option.  Fortunately, the @code{display-buffer-default-alist} variable
+can be used to specify a list of conditional default actions.  For
+instance, consider the case where an application wants to customize the
+behavior of the @code{display-buffer} call invoked internally by the
+@code{occur} function.
+
+@example
+@group
+(let ((display-buffer-default-alist
+       (append '(((category . occur)
+                  (display-buffer-pop-up-window)
+                  (post-command-select-window . t)))
+               display-buffer-default-alist)))
+  (occur "baz"))
+@end group
+@end example
+
+   By setting the @var{action} argument and the
+@code{display-buffer-default-alist} variable, an application effectively
 overrules any customization of @code{display-buffer-base-action}.  Our
-user can now either accept the choice of the application, or redouble
-by customizing the option @code{display-buffer-alist} as follows:
+user can now either accept the choice of the application, or redouble by
+customizing the option @code{display-buffer-alist} as follows:
 
 @example
 @group
diff --git a/etc/NEWS b/etc/NEWS
index ead3f7fb72cb..56d87c8e4e36 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -206,6 +206,13 @@ with named choices for the predefined search functions
 ('outline-search-from-regexp' and 'outline-search-level')
 as well as the default nil and arbitrary user functions.
 
+---
+*** New command 'outline-occur'.
+It allows users to navigate the current buffer's outline using Occur.
+The 'outline-regexp' variable is used to find the beginning of the
+outline headings.  The 'outline-occur-regexp' buffer-local variable
+allows overriding this regexp.  It is bound to 'M-o'.
+
 ** Newsticker
 
 ---
@@ -327,6 +334,11 @@ how to analyze the arguments of a function by declaring the
 specification of each argument, rather than implementing an analyzer
 function as you would with 'elisp-scope-define-function-analyzer'.
 
++++
+** New variable 'display-buffer-default-alist'
+Lisp programs may let-bind this variable to specify conditional actions
+for nested 'display-buffer' calls.
+
 
 * Changes in Emacs 32.1 on Non-Free Operating Systems
 
diff --git a/lisp/outline.el b/lisp/outline.el
index 4b4f3c2d520e..1ee86ae871dc 100644
--- a/lisp/outline.el
+++ b/lisp/outline.el
@@ -107,7 +107,8 @@ outline-mode-prefix-map
   "/ h" #'outline-hide-by-heading-regexp
   "C-<" #'outline-promote
   "C->" #'outline-demote
-  "RET" #'outline-insert-heading)
+  "RET" #'outline-insert-heading
+  "M-o" #'outline-occur)
 
 (defvar outline-mode-menu-bar-map
   (let ((map (make-sparse-keymap)))
@@ -148,6 +149,9 @@ outline-mode-menu-bar-map
 		  :help "Show all of the text in the buffer"))
     (define-key map [headings]
       (cons "Headings" (make-sparse-keymap "Headings")))
+    (define-key map [headings outline-occur]
+      '(menu-item "Show in Occur" outline-occur
+		  :help "Navigate the buffer's outline using Occur"))
     (define-key map [headings demote-subtree]
       '(menu-item "Demote Subtree" outline-demote
 		  :help "Demote headings lower down the tree"))
@@ -2179,6 +2183,31 @@ outline-editing-repeat-map
   "C-<" #'outline-promote
   "<"   #'outline-promote)
 
+
+;;; Occur outline navigation
+
+(defvar-local outline-occur-regexp nil
+  "Regexp used by `outline-occur' to override `outline-regexp'.
+Matches the beginning of the outline headings.  Any line whose beginning
+matches this regexp is considered to start a heading.  As Outline mode
+does with `outline-regexp', `outline-occur' only checks this regexp at
+the start of a line, so the regexp need not start with `^'.")
+
+(defun outline-occur ()
+  "Navigate the current buffer's outline using `occur'.
+The `outline-regexp' variable is used to find the beginning of the
+outline headings.  The `outline-occur-regexp' buffer-local variable
+allows overriding this regexp."
+  (interactive)
+  (if-let* ((regexp (or outline-occur-regexp outline-regexp)))
+      (let ((display-buffer-default-alist
+	     (append '(((category . occur)
+		        (display-buffer-pop-up-window)
+		        (post-command-select-window . t)))
+		     display-buffer-default-alist)))
+        (occur (concat "^\\(?:" regexp "\\)")))
+    (user-error "No outline regexp defined")))
+
 
 (provide 'outline)
 (provide 'noutline)
diff --git a/lisp/replace.el b/lisp/replace.el
index e7407869ca27..be8b4d1baffb 100644
--- a/lisp/replace.el
+++ b/lisp/replace.el
@@ -2062,7 +2062,7 @@ occur-1
 	  (setq occur-revert-arguments (list regexp nlines bufs))
           (if (= count 0)
               (kill-buffer occur-buf)
-            (display-buffer occur-buf)
+            (display-buffer occur-buf '(nil (category . occur)))
             (when occur--final-pos
               (set-window-point
                (get-buffer-window occur-buf 'all-frames)
diff --git a/lisp/window.el b/lisp/window.el
index 06128cf0483c..dbd72e7bf6c4 100644
--- a/lisp/window.el
+++ b/lisp/window.el
@@ -8084,9 +8084,9 @@ display-buffer-overriding-action
 
 (defcustom display-buffer-alist nil
   "Alist of user-defined conditional actions for `display-buffer'.
-Its value takes effect before processing the ACTION argument of
-`display-buffer' and before `display-buffer-base-action' and
-`display-buffer-fallback-action', but after
+Its value takes effect before processing `display-buffer-default-alist',
+the ACTION argument of `display-buffer', `display-buffer-base-action'
+and `display-buffer-fallback-action', but after
 `display-buffer-overriding-action', which see.
 
 If non-nil, this is an alist of elements (CONDITION . ACTION),
@@ -8110,6 +8110,21 @@ display-buffer-alist
   :version "24.1"
   :group 'windows)
 
+(defvar display-buffer-default-alist nil
+  "Alist of conditional default actions for `display-buffer'.
+Its value takes effect before processing the ACTION argument of
+`display-buffer' and before `display-buffer-base-action' and
+`display-buffer-fallback-action', but after
+`display-buffer-overriding-action' and `display-buffer-alist', which
+see.
+
+Lisp programs may let-bind this variable to specify conditional actions
+for nested `display-buffer' calls.
+
+If non-nil, this is an alist of elements (CONDITION . ACTION) like
+`display-buffer-alist'.")
+(put 'display-buffer-default-alist 'risky-local-variable t)
+
 (defcustom display-buffer-base-action '(nil . nil)
   "User-specified default action for `display-buffer'.
 This is the default action used by `display-buffer' if no other
@@ -8398,6 +8413,9 @@ display-buffer
             (display-buffer-assq-regexp
              buf-name display-buffer-alist action))
            (special-action (display-buffer--special-action buffer))
+           (default-action
+            (display-buffer-assq-regexp
+             buf-name display-buffer-default-alist action))
            ;; Extra actions from the arguments to this function:
            (extra-action
             (cons nil (append (if inhibit-same-window
@@ -8405,9 +8423,9 @@ display-buffer
                               (if frame
                                   `((reusable-frames . ,frame))))))
            ;; Construct action function list and action alist.
-           (actions (list display-buffer-overriding-action
-                          user-action special-action action extra-action
-                          display-buffer-base-action
+           (actions (list display-buffer-overriding-action user-action
+                          special-action default-action action
+                          extra-action display-buffer-base-action
                           display-buffer-fallback-action))
            (functions (apply #'append
                              (mapcar (lambda (x)
diff --git a/test/lisp/outline-resources/outline.txt b/test/lisp/outline-resources/outline.txt
new file mode 100644
index 000000000000..f3704f9438a0
--- /dev/null
+++ b/test/lisp/outline-resources/outline.txt
@@ -0,0 +1,2 @@
+* Star heading
+- Dash heading
diff --git a/test/lisp/outline-tests.el b/test/lisp/outline-tests.el
new file mode 100644
index 000000000000..da87a0d1171f
--- /dev/null
+++ b/test/lisp/outline-tests.el
@@ -0,0 +1,69 @@
+;;; outline-tests.el --- ERT tests for outline.el -*- lexical-binding: t -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Tests for the `outline' feature.
+
+;;; Code:
+
+(require 'ert)
+(require 'ert-x)
+(require 'outline)
+
+(ert-deftest outline-tests--outline-occur ()
+  "Test the `outline-occur' function with `outline-regexp'."
+  (let ((test-file (ert-resource-file "outline.txt")))
+    (with-temp-buffer
+      (insert-file-contents test-file)
+      (setq-local outline-regexp "\\*"
+		  outline-occur-regexp nil)
+      (outline-occur))
+    (with-current-buffer (get-buffer "*Occur*")
+      (goto-char (point-min))
+      (should (search-forward "* Star heading"))
+      (goto-char (point-min))
+      (should-error (search-forward "- Dash heading")))))
+
+(ert-deftest outline-tests--outline-occur-override ()
+  "Test the `outline-occur' function with `outline-occur-regexp'."
+  (let ((test-file (ert-resource-file "outline.txt")))
+    (with-temp-buffer
+      (insert-file-contents test-file)
+      (setq-local outline-regexp "\\*"
+		  outline-occur-regexp "-")
+      (outline-occur))
+    (with-current-buffer (get-buffer "*Occur*")
+      (goto-char (point-min))
+      (should-error (search-forward "* Star heading"))
+      (goto-char (point-min))
+      (should (search-forward "- Dash heading")))))
+
+(ert-deftest outline-tests--outline-occur-undefined-regexp ()
+  "Test the `outline-occur' function with undefined regexp."
+  (let ((test-file (ert-resource-file "outline.txt")))
+    (with-temp-buffer
+      (insert-file-contents test-file)
+      (setq-local outline-regexp nil
+		  outline-occur-regexp nil)
+      (should-error (outline-occur)))))
+
+(provide 'outline-tests)
+
+;;; outline-tests.el ends here
-- 
2.55.0


--=-=-=--