(outline--hidden-headings-paths): Fix slow saves (bug#78665)

* lisp/outline.el: Prefer #' to quote function names.
(outline--end-of-previous): New function, extracted from
`outline-end-of-subtree`.
(outline-end-of-subtree): Use it.
(outline--hidden-headings-paths): Distinguish headings where just the
entry is hidden from those where a whole subtree is hidden (bug#78673).
(outline--hidden-headings-restore-paths): Adjust accordingly and don't
delegate to functions like `outline-hide-subtree` so as to avoid
an O(N²) behavior.
This commit is contained in:
Stefan Monnier 2025-06-04 16:36:27 -04:00
parent 6f24725323
commit 77a4c63fda
2 changed files with 90 additions and 59 deletions

View file

@ -235,10 +235,10 @@ The argument MAP is optional and defaults to `outline-minor-mode-cycle-map'."
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
(outline-minor-mode-cycle--bind map (kbd "TAB") #'outline-cycle) (outline-minor-mode-cycle--bind map (kbd "TAB") #'outline-cycle)
(outline-minor-mode-cycle--bind map (kbd "<backtab>") #'outline-cycle-buffer) (outline-minor-mode-cycle--bind map (kbd "<backtab>") #'outline-cycle-buffer)
(keymap-set map "<left-margin> <mouse-1>" 'outline-cycle) (keymap-set map "<left-margin> <mouse-1>" #'outline-cycle)
(keymap-set map "<right-margin> <mouse-1>" 'outline-cycle) (keymap-set map "<right-margin> <mouse-1>" #'outline-cycle)
(keymap-set map "<left-margin> S-<mouse-1>" 'outline-cycle-buffer) (keymap-set map "<left-margin> S-<mouse-1>" #'outline-cycle-buffer)
(keymap-set map "<right-margin> S-<mouse-1>" 'outline-cycle-buffer) (keymap-set map "<right-margin> S-<mouse-1>" #'outline-cycle-buffer)
map) map)
"Keymap used as a parent of the `outline-minor-mode' keymap. "Keymap used as a parent of the `outline-minor-mode' keymap.
It contains key bindings that can be used to cycle visibility. It contains key bindings that can be used to cycle visibility.
@ -483,7 +483,7 @@ Turning on outline mode calls the value of `text-mode-hook' and then of
The value of this variable is checked as part of loading Outline mode. The value of this variable is checked as part of loading Outline mode.
After that, changing the prefix key requires manipulating keymaps." After that, changing the prefix key requires manipulating keymaps."
:type 'key-sequence :type 'key-sequence
:initialize 'custom-initialize-default :initialize #'custom-initialize-default
:set (lambda (sym val) :set (lambda (sym val)
(define-key outline-minor-mode-map outline-minor-mode-prefix nil) (define-key outline-minor-mode-map outline-minor-mode-prefix nil)
(define-key outline-minor-mode-map val outline-mode-prefix-map) (define-key outline-minor-mode-map val outline-mode-prefix-map)
@ -685,6 +685,7 @@ at the end of the buffer."
(goto-char (match-beginning 0)) (goto-char (match-beginning 0))
;; Compensate "\n" from the beginning of regexp ;; Compensate "\n" from the beginning of regexp
(when (and outline-search-function (not (bobp))) (forward-char -1))) (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))) (when (and (bolp) (or outline-blank-line (eobp)) (not (bobp)))
(forward-char -1))) (forward-char -1)))
@ -1287,6 +1288,16 @@ This also unhides the top heading-less body, if any."
(progn (outline-end-of-subtree) (point)) (progn (outline-end-of-subtree) (point))
flag))) 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 () (defun outline-end-of-subtree ()
"Move to the end of the current subtree." "Move to the end of the current subtree."
(outline-back-to-heading) (outline-back-to-heading)
@ -1298,12 +1309,7 @@ This also unhides the top heading-less body, if any."
(outline-next-heading)) (outline-next-heading))
(if (and (bolp) (not (eolp))) (if (and (bolp) (not (eolp)))
;; We stopped at a nonempty line (the next heading). ;; We stopped at a nonempty line (the next heading).
(progn (outline--end-of-previous))))
;; 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-show-branches () (defun outline-show-branches ()
"Show all subheadings of this heading, but not their bodies." "Show all subheadings of this heading, but not their bodies."
@ -1717,12 +1723,17 @@ LEVEL, decides of subtree visibility according to
(run-hooks 'outline-view-change-hook)) (run-hooks 'outline-view-change-hook))
(defun outline--hidden-headings-paths () (defun outline--hidden-headings-paths ()
"Return a hash with headings of currently hidden outlines. "Return (HASH-TABLE CURRENT-HEADING).
Every hash key is a list whose elements compose a complete path HASH-TABLE holds the 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. of headings descending from the top level down to the bottom level.
Every entry's value is non-nil if that entry should be hidden.
The specific non-nil vale can be t to hide just the entry, or a number
LEVEL to mean that not just the entry should be hidden but also all the
subsequent elements of level higher or equal to LEVEL.
This is useful to save the hidden outlines and restore them later This is useful to save the hidden outlines and restore them later
after reverting the buffer. Also return the outline where point after reverting the buffer.
was located before reverting the buffer." CURRENT-HEADING is the heading where point is located."
(let* ((paths (make-hash-table :test #'equal)) (let* ((paths (make-hash-table :test #'equal))
path current-path path current-path
(current-heading-p (outline-on-heading-p)) (current-heading-p (outline-on-heading-p))
@ -1730,40 +1741,60 @@ was located before reverting the buffer."
(current-end (when current-heading-p (pos-eol)))) (current-end (when current-heading-p (pos-eol))))
(outline-map-region (outline-map-region
(lambda () (lambda ()
(let* ((level (funcall outline-level)) (let ((level (funcall outline-level)))
(heading (buffer-substring-no-properties (pos-bol) (pos-eol)))) (if (outline-invisible-p)
(while (and path (>= (cdar path) level)) ;; Covered by "the" previous heading.
(pop path)) (cl-callf (lambda (l) (if (numberp l) (min l level) level))
(push (cons heading level) path) (gethash (mapcar #'car path) paths))
(when (save-excursion (let ((heading (buffer-substring-no-properties (pos-bol) (pos-eol))))
(outline-end-of-heading) (while (and path (>= (cdar path) level))
(seq-some (lambda (o) (eq (overlay-get o 'invisible) (pop path))
'outline)) (push (cons heading level) path)
(overlays-at (point)))) (when (save-excursion
(setf (gethash (mapcar #'car path) paths) t)) (outline-end-of-heading)
(outline-invisible-p))
(setf (gethash (mapcar #'car path) paths) t))))
(when (and current-heading-p (<= current-beg (point) current-end)) (when (and current-heading-p (<= current-beg (point) current-end))
(setq current-path (mapcar #'car path))))) (setq current-path (mapcar #'car path)))))
(point-min) (point-max)) (point-min) (point-max))
(list paths current-path))) (list paths current-path)))
(defun outline--hidden-headings-restore-paths (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 This is useful after reverting the buffer to restore the outlines
hidden by `outline--hidden-headings-paths'. Also restore point hidden by `outline--hidden-headings-paths'. Also restore point
on the same outline where point was before reverting the buffer." 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 (outline-map-region
(lambda () (lambda ()
(let* ((level (funcall outline-level)) (let ((level (funcall outline-level)))
(heading (buffer-substring (pos-bol) (pos-eol)))) (if (and (numberp hidelevel) (<= hidelevel level))
(while (and path (>= (cdar path) level)) nil
(pop path)) (when hidestart
(push (cons heading level) path) (outline-flag-region hidestart
(when (gethash (mapcar #'car path) paths) (save-excursion (outline--end-of-previous)
(outline-hide-subtree)) (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))) (when (and current-path (equal current-path (mapcar #'car path)))
(setq current-point (point))))) (setq current-point (point)))))
(point-min) (point-max)) (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)))) (when current-point (goto-char current-point))))
(defun outline-revert-buffer-restore-visibility () (defun outline-revert-buffer-restore-visibility ()

View file

@ -72,7 +72,7 @@
(transient--emergency-exit :debugger) (transient--emergency-exit :debugger)
(apply #'debug args)) (apply #'debug args))
;;; Options ;;;; Options
(defgroup transient nil (defgroup transient nil
"Transient commands." "Transient commands."
@ -507,7 +507,7 @@ give you as many additional suffixes as you hoped.)"
:group 'transient :group 'transient
:type 'boolean) :type 'boolean)
;;; Faces ;;;; Faces
(defgroup transient-faces nil (defgroup transient-faces nil
"Faces used by Transient." "Faces used by Transient."
@ -655,7 +655,7 @@ See also option `transient-highlight-mismatched-keys'."
See also option `transient-highlight-mismatched-keys'." See also option `transient-highlight-mismatched-keys'."
:group 'transient-faces) :group 'transient-faces)
;;; Persistence ;;;; Persistence
(defun transient--read-file-contents (file) (defun transient--read-file-contents (file)
(with-demoted-errors "Transient error: %S" (with-demoted-errors "Transient error: %S"
@ -718,7 +718,7 @@ If `transient-save-history' is nil, then do nothing."
(unless noninteractive (unless noninteractive
(add-hook 'kill-emacs-hook #'transient-maybe-save-history)) (add-hook 'kill-emacs-hook #'transient-maybe-save-history))
;;; Classes ;;;; Classes
;;;; Prefix ;;;; Prefix
(defclass transient-prefix () (defclass transient-prefix ()
@ -965,7 +965,7 @@ commands or strings. This group inserts an empty line between
subgroups. The subgroups are responsible for displaying their subgroups. The subgroups are responsible for displaying their
elements themselves.") elements themselves.")
;;; Define ;;;; Define
(defmacro transient-define-prefix (name arglist &rest args) (defmacro transient-define-prefix (name arglist &rest args)
"Define NAME as a transient prefix command. "Define NAME as a transient prefix command.
@ -1482,7 +1482,7 @@ Intended for use in a group's `:setup-children' function."
(setq prefix (oref prefix command))) (setq prefix (oref prefix command)))
(mapcar (apply-partially #'transient-parse-suffix prefix) suffixes)) (mapcar (apply-partially #'transient-parse-suffix prefix) suffixes))
;;; Edit ;;;; Edit
(defun transient--insert-suffix (prefix loc suffix action &optional keep-other) (defun transient--insert-suffix (prefix loc suffix action &optional keep-other)
(pcase-let* ((suf (cl-etypecase suffix (pcase-let* ((suf (cl-etypecase suffix
@ -1699,7 +1699,7 @@ using `transient-define-suffix', `transient-define-infix' or
(user-error "Cannot set level for `%s'; no prototype object exists" (user-error "Cannot set level for `%s'; no prototype object exists"
command))) command)))
;;; Variables ;;;; Variables
(defvar transient-current-prefix nil (defvar transient-current-prefix nil
"The transient from which this suffix command was invoked. "The transient from which this suffix command was invoked.
@ -1806,7 +1806,7 @@ This is bound while the suffixes are drawn in the transient buffer.")
mwheel-scroll mwheel-scroll
scroll-bar-toolkit-scroll)) scroll-bar-toolkit-scroll))
;;; Identities ;;;; Identities
(defun transient-active-prefix (&optional prefixes) (defun transient-active-prefix (&optional prefixes)
"Return the active transient object. "Return the active transient object.
@ -1944,7 +1944,7 @@ probably use this instead:
(seq-some (lambda (cmd) (get cmd 'transient--suffix)) (seq-some (lambda (cmd) (get cmd 'transient--suffix))
(function-alias-p command)))) (function-alias-p command))))
;;; Keymaps ;;;; Keymaps
(defvar-keymap transient-base-map (defvar-keymap transient-base-map
:doc "Parent of other keymaps used by Transient. :doc "Parent of other keymaps used by Transient.
@ -2306,7 +2306,7 @@ of the corresponding object."
transient--transient-map)) transient--transient-map))
topmap)) topmap))
;;; Setup ;;;; Setup
(defun transient-setup (&optional name layout edit &rest params) (defun transient-setup (&optional name layout edit &rest params)
"Setup the transient specified by NAME. "Setup the transient specified by NAME.
@ -2587,7 +2587,7 @@ value. Otherwise return CHILDREN as is.")
(transient--debug " autoload %s" cmd) (transient--debug " autoload %s" cmd)
(autoload-do-load fn))) (autoload-do-load fn)))
;;; Flow-Control ;;;; Flow-Control
(defun transient--setup-transient () (defun transient--setup-transient ()
(transient--debug 'setup-transient) (transient--debug 'setup-transient)
@ -2981,7 +2981,7 @@ identifying the exit."
(transient--pre-exit) (transient--pre-exit)
(transient--post-exit this-command))) (transient--post-exit this-command)))
;;; Pre-Commands ;;;; Pre-Commands
(defun transient--call-pre-command () (defun transient--call-pre-command ()
(if-let* ((fn (transient--get-pre-command this-command (if-let* ((fn (transient--get-pre-command this-command
@ -3164,7 +3164,7 @@ prefix argument and pivot to `transient-update'."
(put 'transient--do-move 'transient-face 'transient-key-stay) (put 'transient--do-move 'transient-face 'transient-key-stay)
(put 'transient--do-minus 'transient-face 'transient-key-stay) (put 'transient--do-minus 'transient-face 'transient-key-stay)
;;; Commands ;;;; Commands
;;;; Noop ;;;; Noop
(defun transient-noop () (defun transient-noop ()
@ -3487,7 +3487,7 @@ such as when suggesting a new feature or reporting an issue."
arguments " ")) arguments " "))
(message "%s: %S" (key-description (this-command-keys)) arguments))) (message "%s: %S" (key-description (this-command-keys)) arguments)))
;;; Value ;;;; Value
;;;; Init ;;;; Init
(cl-defgeneric transient-init-value (obj) (cl-defgeneric transient-init-value (obj)
@ -4000,7 +4000,7 @@ Append \"=\ to ARG to indicate that it is an option."
(or (match-string 1 match) ""))) (or (match-string 1 match) "")))
(and (member arg args) t))) (and (member arg args) t)))
;;; Return ;;;; Return
(defun transient-init-return (obj) (defun transient-init-return (obj)
(when-let* ((transient--stack) (when-let* ((transient--stack)
@ -4012,7 +4012,7 @@ Append \"=\ to ARG to indicate that it is an option."
(list t 'recurse #'transient--do-recurse)))) (list t 'recurse #'transient--do-recurse))))
(oset obj return t))) (oset obj return t)))
;;; Scope ;;;; Scope
;;;; Init ;;;; Init
(cl-defgeneric transient-init-scope (obj) (cl-defgeneric transient-init-scope (obj)
@ -4084,7 +4084,7 @@ If no prefix matches, return nil."
(and-let* ((obj (transient-prefix-object))) (and-let* ((obj (transient-prefix-object)))
(oref obj scope)))) (oref obj scope))))
;;; History ;;;; History
(cl-defgeneric transient--history-key (obj) (cl-defgeneric transient--history-key (obj)
"Return OBJ's history key.") "Return OBJ's history key.")
@ -4116,7 +4116,7 @@ have a history of their own.")
(cons val (delete val (alist-get (transient--history-key obj) (cons val (delete val (alist-get (transient--history-key obj)
transient-history)))))) transient-history))))))
;;; Display ;;;; Display
(defun transient--show-hint () (defun transient--show-hint ()
(let ((message-log-max nil)) (let ((message-log-max nil))
@ -4190,7 +4190,7 @@ have a history of their own.")
(window-body-width window t) (window-body-width window t)
(window-body-height window t)))) (window-body-height window t))))
;;; Delete ;;;; Delete
(defun transient--delete-window () (defun transient--delete-window ()
(when (window-live-p transient--window) (when (window-live-p transient--window)
@ -4224,7 +4224,7 @@ have a history of their own.")
(setq show (natnump show))) (setq show (natnump show)))
show)) show))
;;; Format ;;;; Format
(defun transient--format-hint () (defun transient--format-hint ()
(if (and transient-show-popup (<= transient-show-popup 0)) (if (and transient-show-popup (<= transient-show-popup 0))
@ -4728,7 +4728,7 @@ a prefix command, while porting a regular keymap to a transient."
(propertize (car (split-string doc "\n")) 'face 'font-lock-doc-face) (propertize (car (split-string doc "\n")) 'face 'font-lock-doc-face)
(propertize (symbol-name command) 'face 'font-lock-function-name-face)))) (propertize (symbol-name command) 'face 'font-lock-function-name-face))))
;;; Help ;;;; Help
(cl-defgeneric transient-show-help (obj) (cl-defgeneric transient-show-help (obj)
"Show documentation for the command represented by OBJ.") "Show documentation for the command represented by OBJ.")
@ -5169,7 +5169,7 @@ as stand-in for elements of exhausted lists."
(setq lists (mapcar #'cdr lists))) (setq lists (mapcar #'cdr lists)))
(nreverse result))) (nreverse result)))
;;; Font-Lock ;;;; Font-Lock
(defconst transient-font-lock-keywords (defconst transient-font-lock-keywords
(eval-when-compile (eval-when-compile
@ -5187,7 +5187,7 @@ as stand-in for elements of exhausted lists."
(font-lock-add-keywords 'emacs-lisp-mode transient-font-lock-keywords) (font-lock-add-keywords 'emacs-lisp-mode transient-font-lock-keywords)
;;; Auxiliary Classes ;;;; Auxiliary Classes
;;;; `transient-lisp-variable' ;;;; `transient-lisp-variable'
(defclass transient-lisp-variable (transient-variable) (defclass transient-lisp-variable (transient-variable)
@ -5250,4 +5250,4 @@ as stand-in for elements of exhausted lists."
;; indent-tabs-mode: nil ;; indent-tabs-mode: nil
;; checkdoc-symbol-words: ("command-line" "edit-mode" "help-mode") ;; checkdoc-symbol-words: ("command-line" "edit-mode" "help-mode")
;; End: ;; End:
;;; transient.el ends here ;;;; transient.el ends here