1

As I understand it, undo operations (namely undo and undo-only) are regular Emacs operations which add to the undo history.

An issue with this is undoing adds to the size of the undo data, which could cause some undo data to be freed.

Is there a way to reliably pause this while the user runs undo & undo-only, restoring the normal freeing behavior once a regular editing operation runs?


Recipe that exemplifies the issue

Run the following command:

emacs -Q --eval "(setq undo-limit 1 undo-outer-limit 10 undo-strong-limit 10)" newfile.txt

Type in 8 characters over 6 lines, so the document reads.

AAAAAAAA
BBBBBBBB
CCCCCCCC
DDDDDDDD
EEEEEEEE
FFFFFFFF

Undo using C-_ until only the first line is present.

AAAAAAAA

Now try redo by pressing C-g, C-_ (keep pressing C-_).

The redo does not complete, it reaches 4 lines, then gives an error.

AAAAAAAA
BBBBBBBB
CCCCCCCC
DDDDDDDD

With the warning

Warning (undo): Buffer ‘newfile.txt’ undo info was 32 bytes long.
The undo info was discarded because it exceeded `undo-outer-limit'.

This is just to show that the undo history is freed while undoing.

  • Please provide a recipe, to clarify what you're asking. – Drew Jan 18 at 22:26
  • @Drew, added exact steps to redo the problem. – ideasman42 Jan 18 at 23:07
  • 2
    This sounds related to a recent suggestion on the Emacs devel mailing list wherein someone proposed (let ((gc-cons-threshold most-positive-fixnum)) ...) around a function that operates on the undo-list to temporarily prevent GC from truncating the list: lists.gnu.org/archive/html/emacs-devel/2020-01/msg00327.html Eli Z. opines that GC should not be paused by the user .... My take on the comments by Eli Z. is that GC should be permitted to do its thing ... as such, adjusting the undo-limits upwards may be about as good as it gets. – lawlist Jan 19 at 0:42
  • Thanks, that makes things much clearer. – Drew Jan 19 at 2:27
  • However, it sounds like you answer your own question in the recipe. You set undo-outer-limit to a value that caused the behavior you lament. The doctor would say, I think, "If it hurts then don't do that." What is the real question, here? If you want to propose an enhancement to Emacs or report a bug, then M-x report-emacs--bug. – Drew Jan 19 at 2:28
0

This is an example implementation of a temporary lock which increases the limits while undoing.

This uses the after-change-functions to detect non-undo changes, releasing the lock.

Tested with emacs-27.

Note that I'm not all that happy with my own solution, I think the way it relies on hooks is fragile, although I didn't see a better way to do it either.

;; Undo Locking: don't free undo state while undoing.

;; These functions don't clear the lock even when they modify the buffer.
;; Include 'save-buffer' so hooks to format on save don't release the lock.
(defvar-local myundo--lock-dont-unlock-fn (list 'save-buffer 'undo 'undo-only))

;; When non-nil we have a lock, store undo limits here while locked.
(defvar-local myundo--lock-alist nil)

;; Override with 'let' so undo functions don't release the lock.
(defconst myundo--lock-dont-unlock nil)

(defun myundo--lock-disable-after-change (_beg _end _len)
  (unless myundo--lock-dont-unlock
    ;; Can be null since removing the 'after-change' doesn't happen immediately.
    (when myundo--lock-alist
      (myundo--lock-release))))

(defun myundo--without-lock-fn (old-function &rest args)
  (let ((myundo--lock-dont-unlock t))
    (apply old-function args)))

(defun myundo--lock-aquire ()
  ;; In case the lock was already enabled, re-enable.
  (when myundo--lock-alist
    (myundo--lock-release))

  ;; Prevent loss of undo data.
  (setq myundo--lock-alist
    (list
      (cons 'undo-outer-limit undo-outer-limit)
      (cons 'undo-limit undo-limit)
      (cons 'undo-strong-limit undo-strong-limit)))

  ;; Don't truncate any undo data in the middle of this.
  (setq undo-outer-limit nil)
  (setq undo-limit most-positive-fixnum)
  (setq undo-strong-limit most-positive-fixnum)

  (dolist (elem myundo--lock-dont-unlock-fn)
    (advice-add elem :around #'myundo--without-lock-fn '((name . "myundo--advice"))))

  (add-hook 'after-change-functions #'myundo--lock-disable-after-change :local)

  (message "Undo Lock Aquired"))

(defun myundo--lock-release ()

  (dolist (elem myundo--lock-dont-unlock-fn) (advice-remove elem "myundo--advice"))

  (remove-hook 'after-change-functions #'myundo--lock-disable-after-change :local)

  (setq undo-outer-limit (alist-get 'undo-outer-limit myundo--lock-alist))
  (setq undo-limit (alist-get 'undo-limit myundo--lock-alist))
  (setq undo-strong-limit (alist-get 'undo-strong-limit myundo--lock-alist))

  ;; Clear to show we're not locked.
  (setq myundo--lock-alist nil)

  ;; Account for reduced undo limits.
  (garbage-collect)

  (message "Undo Lock Released"))

(defun myundo--lock-aquire-advice (&rest _)
  (unless myundo--lock-alist
    (myundo--lock-aquire)))

(defun myundo--lock-install ()
  (interactive)
  (advice-add 'undo :after #'myundo--lock-aquire-advice '((name . "myundo--advice"))))

(defun myundo--lock-uninstall ()
  (interactive)
  (advice-remove 'undo "myundo--advice"))

;; Enable this now!
(myundo--lock-install)

| improve this answer | |
  • I recommend you express your frustration via M-x report-emacs-bug, phrasing it as a request for feature. E.g. the undo-truncation performed during GC could be tweaked such that the data is optionally moved somewhere where a package like "persistent undo" could make use of it. – Stefan Jan 19 at 13:49
  • 1
    I wonder why you don't do something like (advice-add 'undo :around (lambda (origfun &rest args) (let ((undo-outer-limit nil) (undo-limit most-positive-fixnum) (undo-strong-limit most-positive-fixnum)) (apply origfun args)))) – Stefan Jan 19 at 13:52
  • I did this because I don't think I can guarantee the GC wont run. Wouldn't any edits to other buffers that run the GC also impact this buffer? – ideasman42 Jan 19 at 13:56
  • Those settings don't prevent running the GC, they control the effects of the GC instead, and they'd be active only during the execution of undo, which AFAICT is what you're talking about here. I don't understand your last question (buffers don't run the GC). But in any case, I recommend moving this elsewhere because the SX format is ill-suited for discussions. – Stefan Jan 19 at 14:13
  • Another case to increase these values is tools that format on save, I use clang-format on save for example. This would make saving loose undo history too. OTOH, what you suggest probably works well for many cases. I just wasn't sure when these values are used to limit undo. – ideasman42 Jan 19 at 14:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.