Subject: bug#78665: 31.0.50; Very slow saves

From: Stefan Monnier <[email-masked]>
Date: Mon, 02 Jun 2025 18:18:13 -0400

> AFAICT the problem is that sometimes this heading is already hidden
> (because of a preceding subtree covering the current one), so we end up
> going back to the preceding/larger subtree and re-hiding it.  So for
> a subtree like that of `;;; Code:` covering N smaller subtrees that were
> previously hidden, we end hiding the `;;; Code:` subtree N times (and
> each time, this requires looping through all the covered headings),
> which introduces an O(N^2) complexity.

The patch below seems to fix it (and bug#78673 as well).
Juri, comments?

        Stefan
diff --git a/lisp/outline.el b/lisp/outline.el
index 9d453881b7e..5e8a3360c63 100644
--- a/lisp/outline.el
+++ b/lisp/outline.el
@@ -685,6 +685,7 @@ outline-next-preface
     (goto-char (match-beginning 0))
     ;; Compensate "\n" from the beginning of regexp
     (when (and outline-search-function (not (bobp))) (forward-char -1)))
+  ;; FIXME: Use `outline--end-of-previous'.
   (when (and (bolp) (or outline-blank-line (eobp)) (not (bobp)))
     (forward-char -1)))
 
@@ -1287,6 +1288,16 @@ outline-flag-subtree
 			  (progn (outline-end-of-subtree) (point))
 			  flag)))
 
+(defun outline--end-of-previous ()
+  "Go back from BOH (or EOB) to end of previous element."
+  (if (eobp)
+      (if (bolp) (forward-char -1))
+    ;; Go to end of line before heading
+    (forward-char -1)
+    (if (and outline-blank-line (bolp))
+        ;; leave blank line before heading
+        (forward-char -1))))
+
 (defun outline-end-of-subtree ()
   "Move to the end of the current subtree."
   (outline-back-to-heading)
@@ -1298,12 +1309,7 @@ outline-end-of-subtree
       (outline-next-heading))
     (if (and (bolp) (not (eolp)))
 	;; We stopped at a nonempty line (the next heading).
-	(progn
-	  ;; Go to end of line before heading
-	  (forward-char -1)
-          (if (and outline-blank-line (bolp))
- 	      ;; leave blank line before heading
- 	      (forward-char -1))))))
+	(outline--end-of-previous))))
 
 (defun outline-show-branches ()
   "Show all subheadings of this heading, but not their bodies."
@@ -1717,8 +1723,8 @@ outline-hide-by-heading-regexp
   (run-hooks 'outline-view-change-hook))
 
 (defun outline--hidden-headings-paths ()
-  "Return a hash with headings of currently hidden outlines.
-Every hash key is a list whose elements compose a complete path
+  "Return a hash-table with headings of currently hidden outlines.
+Every key is a list whose elements compose a complete path
 of headings descending from the top level down to the bottom level.
 This is useful to save the hidden outlines and restore them later
 after reverting the buffer.  Also return the outline where point
@@ -1730,40 +1736,60 @@ outline--hidden-headings-paths
          (current-end (when current-heading-p (pos-eol))))
     (outline-map-region
      (lambda ()
-       (let* ((level (funcall outline-level))
-              (heading (buffer-substring-no-properties (pos-bol) (pos-eol))))
-         (while (and path (>= (cdar path) level))
-           (pop path))
-         (push (cons heading level) path)
-         (when (save-excursion
-                 (outline-end-of-heading)
-                 (seq-some (lambda (o) (eq (overlay-get o 'invisible)
-                                           'outline))
-                           (overlays-at (point))))
-           (setf (gethash (mapcar #'car path) paths) t))
+       (let ((level (funcall outline-level)))
+         (if (outline-invisible-p)
+             ;; Covered by "the" previous heading.
+             (cl-callf (lambda (l) (if (numberp l) (min l level) level))
+                 (gethash (mapcar #'car path) paths))
+           (let ((heading (buffer-substring-no-properties (pos-bol) (pos-eol))))
+             (while (and path (>= (cdar path) level))
+               (pop path))
+             (push (cons heading level) path)
+             (when (save-excursion
+                     (outline-end-of-heading)
+                     (outline-invisible-p))
+               (setf (gethash (mapcar #'car path) paths) t))))
          (when (and current-heading-p (<= current-beg (point) current-end))
            (setq current-path (mapcar #'car path)))))
      (point-min) (point-max))
     (list paths current-path)))
 
 (defun outline--hidden-headings-restore-paths (paths current-path)
-  "Restore hidden outlines from a hash of hidden headings.
+  "Restore hidden outlines from a hash-table of hidden headings.
 This is useful after reverting the buffer to restore the outlines
 hidden by `outline--hidden-headings-paths'.  Also restore point
 on the same outline where point was before reverting the buffer."
-  (let (path current-point outline-view-change-hook)
+  (let ((hidelevel nil) (hidestart nil)
+        path current-point outline-view-change-hook)
     (outline-map-region
      (lambda ()
-       (let* ((level (funcall outline-level))
-              (heading (buffer-substring (pos-bol) (pos-eol))))
-         (while (and path (>= (cdar path) level))
-           (pop path))
-         (push (cons heading level) path)
-         (when (gethash (mapcar #'car path) paths)
-           (outline-hide-subtree))
+       (let ((level (funcall outline-level)))
+         (if (and (numberp hidelevel) (<= hidelevel level))
+             nil
+           (when hidestart
+             (outline-flag-region hidestart
+                                  (save-excursion (outline--end-of-previous)
+                                                  (point))
+                                  t)
+             (setq hidestart nil))
+           (let* ((heading (buffer-substring-no-properties
+                            (pos-bol) (pos-eol))))
+             (while (and path (>= (cdar path) level))
+               (pop path))
+             (push (cons heading level) path)
+             (when (setq hidelevel (gethash (mapcar #'car path) paths))
+               (setq hidestart (save-excursion (outline-end-of-heading)
+                                               (point))))))
          (when (and current-path (equal current-path (mapcar #'car path)))
            (setq current-point (point)))))
      (point-min) (point-max))
+    (when hidestart
+      (outline-flag-region hidestart
+                           (save-excursion
+                             (goto-char (point-max))
+                             (outline--end-of-previous)
+                             (point))
+                           t))
     (when current-point (goto-char current-point))))
 
 (defun outline-revert-buffer-restore-visibility ()