Subject: Re: bug#51809: 29.0.50; [PATCH] Support for outline default state in Diff buffers

From: Matthias Meulien <[email-masked]>
Date: Sun, 26 Dec 2021 17:05:25 +0100

Matthias Meulien <[email-masked]> writes:

> (...) Thanks for reading the patch! I'll send another one, hopefully
> taking your remarks into account, when support of local variables is
> fixed.

Here is an updated patch implementing a default state for Outline mode
and Outline minor mode:

>From db0cf942950c7e997d2701742ce16c8385f452e0 Mon Sep 17 00:00:00 2001
From: Matthias Meulien <[email-masked]>
Date: Wed, 8 Dec 2021 22:35:42 +0100
Subject: [PATCH] Extend Outline mode with default visibility state

* lisp/outline.el (outline-mode, outline-minor-mode): Ensure default
visibility state is applied
(outline-hide-sublevels): Add optional argument for function to call
on each heading
(outline-default-state): Define the default visibility state
(outline-apply-default-state): Apply default visibility state
---
 lisp/outline.el | 183 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 180 insertions(+), 3 deletions(-)

diff --git a/lisp/outline.el b/lisp/outline.el
index 2ede4e23ea..c52b9cd4e7 100644
--- a/lisp/outline.el
+++ b/lisp/outline.el
@@ -353,7 +353,9 @@ outline-mode
               '(outline-font-lock-keywords t nil nil backward-paragraph))
   (setq-local imenu-generic-expression
 	      (list (list nil (concat "^\\(?:" outline-regexp "\\).*$") 0)))
-  (add-hook 'change-major-mode-hook #'outline-show-all nil t))
+  (add-hook 'change-major-mode-hook #'outline-show-all nil t)
+  (add-hook 'hack-local-variables-hook
+	    #'outline-apply-default-state))
 
 (defvar outline-minor-mode-map)
 
@@ -436,7 +438,9 @@ outline-minor-mode
 		  nil t)
         (setq-local line-move-ignore-invisible t)
 	;; Cause use of ellipses for invisible text.
-	(add-to-invisibility-spec '(outline . t)))
+	(add-to-invisibility-spec '(outline . t))
+        (add-hook 'hack-local-variables-hook
+	          #'outline-apply-default-state))
     (when (or outline-minor-mode-cycle outline-minor-mode-highlight)
       (if font-lock-fontified
           (font-lock-remove-keywords nil outline-font-lock-keywords))
@@ -1093,7 +1097,7 @@ outline-hide-sublevels
       (outline-map-region
        (lambda ()
 	 (if (<= (funcall outline-level) levels)
-	     (outline-show-heading)))
+             (outline-show-heading)))
        beg end)
       ;; Finally unhide any trailing newline.
       (goto-char (point-max))
@@ -1307,6 +1311,179 @@ outline-headers-as-kill
                     (insert "\n\n"))))))
           (kill-new (buffer-string)))))))
 
+(defcustom outline-default-state nil
+  "If non-nil, some headings are initially outlined.
+
+If equal to `outline-show-all', all text of buffer is shown.
+
+If equal to `outline-show-only-headings', only headings are shown.
+
+If equal to a number, show only headings up to the corresponding
+level. See `outline-default-state-subtree-visibility' to
+customize visibility of the subtree at the choosen level.
+
+If equal to a lambda function or function name, this function is
+expected to toggle headings visibility, and will be called after
+the mode is enabled."
+  :version "29.1"
+  :type '(choice (const :tag "Disabled" nil)
+                 (const :tag "Show all" outline-show-all)
+                 (const :tag "Only headings" outline-show-only-headings)
+                 (natnum :tag "Show headings up to level" :value 1)
+                 (function :tag "Custom function")))
+
+(defcustom outline-default-state-subtree-visibility nil
+  "Defines visibility of subtree starting at level defined by `outline-default-state'.
+
+When nil, the subtree is hidden unconditionally.
+
+When equal to a list, each element is expected to equal one of:
+
+- A cons cell with CAR `match-regexp' and CDR a regexp, the
+  subtree will be hidden when the outline heading match the
+  regexp.
+
+- `subtree-has-long-lines' to only show the heading branches when
+   long lines are detected in its subtree (see
+   `outline-long-line-threshold' for the definition of long
+   lines).
+
+- `subtree-is-long' to only show the heading branches when its
+  subtree contains more than `outline-line-count-threshold'
+  lines.
+
+- A lambda function or function name which will be evaluated with
+  point at the beginningg of the heading and the match data set
+  appropriately, the function being expected to toggle the
+  heading visibility."
+  :version "29.1"
+  :type '(choice (const :tag "Hide subtree" nil)
+                 (set :tag "Show subtree unless"
+                      (cons :tag "Heading match regexp"
+                            (const match-regexp)  string)
+                      (const :tag "Body has long lines"
+                             subtree-has-long-lines)
+                      (const :tag "Body is long"
+                             subtree-is-long)
+                      (cons :tag "Custom function"
+                            (const custom-function) function))))
+
+(defcustom outline-long-line-threshold 1000
+  "Minimal number of characters in a line for a heading to be outlined."
+  :version "29.1"
+  :type '(natnum :tag "Number of lines"))
+
+(defcustom outline-line-count-threshold 50
+  "Minimal number of lines for a heading to be outlined."
+  :version "29.1"
+  :type '(natnum :tag "Number of lines"))
+
+(defun outline-apply-default-state ()
+  "Apply the outline state defined by `outline-default-state'."
+  (interactive)
+  (cond
+   ((integerp outline-default-state)
+    (outline--show-headings-up-to-level outline-default-state))
+   ((when (functionp outline-default-state)
+      (funcall outline-default-state)))))
+
+(defun outline-show-only-headings ()
+  "Show only headings."
+  (interactive)
+  (outline-show-all)
+  (outline-hide-region-body (point-min) (point-max)))
+
+(eval-when-compile (require 'so-long))
+(autoload 'so-long-detected-long-line-p "so-long")
+(defvar so-long-skip-leading-comments)
+(defvar so-long-threshold)
+(defvar so-long-max-lines)
+
+(defun outline--show-headings-up-to-level (level)
+  "Show only headings up to a LEVEL level and call FUN on the leaves.
+
+Like `outline-hide-sublevels' but but call
+`outline-default-state-subtree-visibility' for each heading at
+level equal to LEVEL."
+  (if (not outline-default-state-subtree-visibility)
+      (outline-hide-sublevels level)
+    (if (< level 1)
+        (error "Must keep at least one level of headers"))
+    (save-excursion
+      (let* (outline-view-change-hook
+             (beg (progn
+                    (goto-char (point-min))
+                    ;; Skip the prelude, if any.
+                    (unless (outline-on-heading-p t) (outline-next-heading))
+                    (point)))
+             (end (progn
+                    (goto-char (point-max))
+                    ;; Keep empty last line, if available.
+                    (if (bolp) (1- (point)) (point))))
+             (heading-regexp
+              (cdr-safe
+               (assoc 'match-regexp
+                      outline-default-state-subtree-visibility)))
+             (check-line-count
+              (memq 'subtree-is-long
+                    outline-default-state-subtree-visibility))
+             (check-long-lines
+              (memq 'subtree-has-long-lines
+                    outline-default-state-subtree-visibility))
+             (custom-function
+              (cdr-safe
+               (assoc 'custom-function
+                      outline-default-state-subtree-visibility))))
+        (if (< end beg)
+	    (setq beg (prog1 end (setq end beg))))
+        ;; First hide everything.
+        (outline-hide-sublevels level)
+        ;; Then unhide the top level headers.
+        (outline-map-region
+         (lambda ()
+             (let ((current-level (outline-level)))
+	       (when (< current-level level)
+                 (outline-show-heading)
+                 (outline-show-entry))
+               (when (= current-level level)
+                 (cond
+                  ((and heading-regexp
+                        (let ((beg (point))
+                              (end (progn (outline-end-of-heading) (point))))
+                          (string-match-p heading-regexp (buffer-substring beg end))))
+                   ;; hide entry when heading match regexp
+                   (outline-hide-entry))
+                  ((and check-line-count
+                        (save-excursion
+                          (let* ((beg (point))
+                                 (end (progn (outline-end-of-subtree) (point)))
+                                 (line-count (count-lines beg end)))
+                            (< outline-line-count-threshold line-count))))
+                   ;; show only branches when line count of subtree >
+                   ;; threshold
+                   (outline-show-branches))
+                  ((and check-long-lines
+                        (save-excursion
+                          (let ((beg (point))
+                                (end (progn (outline-end-of-subtree) (point))))
+                            (save-restriction
+                              (narrow-to-region beg end)
+                              (let ((so-long-skip-leading-comments nil)
+                                    (so-long-threshold outline-long-line-threshold)
+                                    (so-long-max-lines nil))
+                                (so-long-detected-long-line-p))))))
+                   ;; show only branches when long lines are detected
+                   ;; in subtree
+                   (outline-show-branches))
+                  (custom-function
+                   ;; call custom function if defined
+                   (funcall custom-function))
+                  (t
+                   ;; if no previous clause succeeds, show subtree
+                   (outline-show-subtree))))))
+         beg end)))
+    (run-hooks 'outline-view-change-hook)))
+
 (defun outline--cycle-state ()
   "Return the cycle state of current heading.
 Return either 'hide-all, 'headings-only, or 'show-all."
-- 
2.30.2

Here is a file used to test this feature:

# -*- mode: outline; -*-

Help to test implementation of outline default state.

* Heading 1

Preambule

** Heading with long lines 1.1

With looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong line

** Heading 1.2

Some text

** Heading 1.3

A first paragraph followed by a second paragraph but with less
interesting text.

To be discussed.

** Heading with not so long line 1.4

aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa	

* Heading 2

Preamble to a heading with many lines.

** Heading with many lines 2.1

Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines
Many lines

** Heading 2.2
Many lines
* Heading 3

Preamble

** Heading matching regex 3.1 TOHIDE

Hidden body

*** Heading 3.1.1

Body of hidden parent

**** Heading 3.1.1.1

*** Heading 3.1.2

Still in a hidden parent

* Heading 4

Last body
and nothing
else but those three lines

# Local Variables:
# outline-default-state: 2
# outline-default-state-subtree-visibility: ((match-regexp . "TOHIDE") subtree-has-long-lines subtree-is-long)
# outline-long-line-threshold: 200
# outline-line-count-threshold: 100
# End:

There's a bug when used with diff-mode (where `outline-level' returns
unexpected values), the starting point of that thread! I'll try to study
this in the forthcoming days.