1. Frank Fischer
  2. evil
Issue #210 resolved

Kmacros and undo cause buffer content wipeout

epich
created an issue

Using kmacros with undo while Evil is active can cause erasure of buffer contents.

Reproduction steps: Open Emacs, switch to scratch buffer, type some text

blah
foo
bar

Begin kmacro definition C-x (, edit text: kix<ESC>, then C-x ). The kmacro goes up a line and inserts x. Execute the kmacro a few times with C-x e. Enter the u command. The entire buffer contents disappear! This bug causes me to shy away from kmacros much more than I would otherwise, and it makes me avoid undo after I do use it. A fix is very desirable.

Comments (15)

  1. epich reporter

    I'm using Emacs 24.3, and witness the bug with minimal .emacs config.

    I'm trying to create an Evil ERT test to demonstrate. When I use the message function inside a evil-test-buffer form, it doesn't print to the Messages buffer. Do you have any ideas about that?

  2. Frank Fischer repo owner

    No, sorry, I usually have the same problem (maybe ert-info or so can help). But note that usual evil tests use keyboard macros for the test itself, so it may be problematic to test the use of keyboard macros.

  3. epich reporter

    Here's some data about what's happening. I put print statements in relevant Evil functions and executed: C-x ( ix<ESC> C-x ) C-x e .

    Defining kbd macro...
    Inside evil-start-undo-step , start of function, buffer-undo-list=(nil (1 . 192) (t . 0)) evil-undo-list-pointer=nil 
    Inside evil-start-undo-step , end of function, buffer-undo-list=(nil (1 . 192) (t . 0)) evil-undo-list-pointer=(nil (1 . 192) (t . 0)) 
    Inside evil-end-undo-step , start of function, buffer-undo-list=(nil (192 . 193) (t . 0) nil (1 . 192) (t . 0)) evil-undo-list-pointer=(nil (1 . 192) (t . 0)) 
    Inside evil-filter-list , end of while loop, tail=((192 . 193) (t . 0) nil (1 . 192) (t . 0)) pointer=(nil (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((t . 0) nil (1 . 192) (t . 0)) pointer=(nil (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=(nil (1 . 192) (t . 0)) pointer=(nil (1 . 192) (t . 0))
    Inside evil-end-undo-step , end of function, buffer-undo-list=((192 . 193) (t . 0) nil (1 . 192) (t . 0)) evil-undo-list-pointer=nil 
    Keyboard macro defined
    Inside evil-start-undo-step , start of function, buffer-undo-list=(nil (192 . 193) (t . 0) nil (1 . 192) (t . 0)) evil-undo-list-pointer=nil 
    Inside evil-start-undo-step , end of function, buffer-undo-list=(nil (192 . 193) (t . 0) nil (1 . 192) (t . 0)) evil-undo-list-pointer=(nil (192 . 193) (t . 0) nil (1 . 192) (t . 0)) 
    Inside evil-end-undo-step , start of function, buffer-undo-list=(nil (192 . 194) (t . 0) nil (1 . 192) (t . 0)) evil-undo-list-pointer=(nil (192 . 194) (t . 0) nil (1 . 192) (t . 0)) 
    Inside evil-filter-list , end of while loop, tail=((192 . 194) (t . 0) nil (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) nil (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((t . 0) nil (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) nil (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=(nil (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) nil (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=nil pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-end-undo-step , end of function, buffer-undo-list=((192 . 194) (t . 0) (1 . 192) (t . 0)) evil-undo-list-pointer=nil
    

    This one seems to show that both insertions of character "x" are consolidated into one entry (192 . 194). That two separate insertions were consolidated into one might be a bug. But we see another more clearly when I do kk and C-x e .

    Inside evil-start-undo-step , start of function, buffer-undo-list=(nil (192 . 194) (t . 0) (1 . 192) (t . 0)) evil-undo-list-pointer=nil 
    Inside evil-start-undo-step , end of function, buffer-undo-list=(nil (192 . 194) (t . 0) (1 . 192) (t . 0)) evil-undo-list-pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0)) 
    Inside evil-end-undo-step , start of function, buffer-undo-list=(nil (143 . 144) nil (192 . 194) (t . 0) (1 . 192) (t . 0)) evil-undo-list-pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0)) 
    Inside evil-filter-list , end of while loop, tail=((143 . 144) nil (192 . 194) (t . 0) (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=(nil (192 . 194) (t . 0) (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((192 . 194) (t . 0) (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((t . 0) (1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((1 . 192) (t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=((t . 0)) pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-filter-list , end of while loop, tail=nil pointer=(nil (192 . 194) (t . 0) (1 . 192) (t . 0))
    Inside evil-end-undo-step , end of function, buffer-undo-list=((143 . 144) (192 . 194) (t . 0) (1 . 192) (t . 0)) evil-undo-list-pointer=nil
    

    The end of the second iteration of the while loop clearly shows that tail and pointer have equal contents. But apparently since the while loop continues they are not eq. Perhaps Emacs copied the buffer-undo-list at some point?

    Anyway, the end result of this is that the evil-refresh-undo-step scrubs all the undo boundaries from the undo history, so then when I undo after executing the kbd macro, all the buffer contents are expunged.

  4. Frank Fischer repo owner

    Hm, I still have problems to reproduce the error. Can you please

    • check whether the problem occurs with other Emacs versions (besides 24.3)
    • attach the patch that adds your debug messages
    • check if you use undo-tree and if yes, which version
    • you said you use a "minimal" .emacs -> please attach this file with as less customizations as possible (as reasonable)

    Thanks.

  5. epich reporter

    Oops, actually I was using 24.2.3 .

    I don't use undo-tree.

    Contents of minimal .emacs:

    ;; Initialize evil
    (add-to-list 'load-path "~/.emacs.d/evil")
    (require 'evil)
    (evil-mode 1)
    

    Patch with debug statements is:

    diff --git a/evil-common.el b/evil-common.el
    index 47b0760..145dc4c 100644
    --- a/evil-common.el
    +++ b/evil-common.el
    @@ -129,7 +129,8 @@ changing the value of `foo'."
               (setq list tail)))
            (t
             (setq head tail
    -              tail (cdr tail)))))
    +              tail (cdr tail))))
    +      (message "Inside evil-filter-list , end of while loop, tail=%s pointer=%s" tail pointer))
         list))
    
     (defun evil-member-if (predicate list &optional pointer)
    @@ -2819,22 +2820,26 @@ If no description is available, return the empty string."
     All following buffer modifications are grouped together as a
     single action. If CONTINUE is non-nil, preceding modifications
     are included. The step is terminated with `evil-end-undo-step'."
    +  (message "Inside evil-start-undo-step , start of function, buffer-undo-list=%s evil-undo-list-pointer=%s " buffer-undo-list evil-undo-list-pointer)
       (when (listp buffer-undo-list)
         (if evil-undo-list-pointer
             (evil-refresh-undo-step)
           (unless (or continue (null (car-safe buffer-undo-list)))
             (undo-boundary))
    -      (setq evil-undo-list-pointer (or buffer-undo-list t)))))
    +      (setq evil-undo-list-pointer (or buffer-undo-list t))))
    +  (message "Inside evil-start-undo-step , end of function, buffer-undo-list=%s evil-undo-list-pointer=%s " buffer-undo-list evil-undo-list-pointer))
    
     (defun evil-end-undo-step (&optional continue)
       "End a undo step started with `evil-start-undo-step'.
     Adds an undo boundary unless CONTINUE is specified."
    +  (message "Inside evil-end-undo-step , start of function, buffer-undo-list=%s evil-undo-list-pointer=%s " buffer-undo-list evil-undo-list-pointer)  
       (when evil-undo-list-pointer
         (evil-refresh-undo-step)
         (unless continue
           (undo-boundary))
         (remove-hook 'post-command-hook #'evil-refresh-undo-step t)
    -    (setq evil-undo-list-pointer nil)))
    +    (setq evil-undo-list-pointer nil))
    +  (message "Inside evil-end-undo-step , end of function, buffer-undo-list=%s evil-undo-list-pointer=%s " buffer-undo-list evil-undo-list-pointer))
    
     (defun evil-refresh-undo-step ()
       "Refresh `buffer-undo-list' entries for current undo step.
    
  6. epich reporter

    As mentioned I tried Emacs 24.2.3. That's on RHEL 5.

    I tried 24.2.1 on Windows and witnessed the bug.

    I tried the latest Emacs on bzr trunk in Mint 12.04 and did not witness the error.

  7. epich reporter

    I bisected the Git mirror of Emacs to determine the commit which caused this bug to disappear:

    603632c493d4e58de6484781f4d5a4bf0f89a4e4 is the first bad commit commit 603632c493d4e58de6484781f4d5a4bf0f89a4e4 Author: Stefan Monnier monnier@iro.umontreal.ca Date: Wed Jul 18 09:20:59 2012 -0400

    * src/lisp.h (last_undo_boundary): Declare new var.
    * src/keyboard.c (command_loop_1): Set it.
    * src/cmds.c (Fself_insert_command): Use it to only remove boundaries that
    were auto-added by the command loop.
    

    :040000 040000 8965edf2697fc3c032b4d285dbd0d566b44a0f14 6b50ccd6ffca3e84b64526d621f00e085e668500 M src

    I'm not certain whether this solved the issue or concealed it.

    I editted the commit so as it patches the 24.2 release without failures. I attached it to this bug report for the benefit of those who want the fix without using Emacs trunk.

  8. Log in to comment