Possible bug: cursor jumps to the wrong position when undoing changes?

172 views
Skip to first unread message

Dmytro Konstantinov

unread,
Jul 30, 2015, 5:54:16 AM7/30/15
to vim_dev
Hi guys,

I think I've found a bug in the way the cursor position is handled when undoing `:s/...` command. It's a bit of a long winded story, so please bear with me:

The way my VIM is set up, saving a buffer to a file triggers a custom StripTrailingSpaces() function, which is attached at the end of the StackOverflow question (http://goo.gl/ykwJDS):

autocmd BufWritePre,FileWritePre,FileAppendPre,FilterWritePre <buffer>
\ :keepjumps call UmkaDK#StripTrailingSpaces(0)

After seeing "Restore the cursor position after undoing text change made by a script" vim tip (http://goo.gl/Gs3fqG), I got an idea to exclude the changes made by my function from the undo history by merging undo record created by the function onto the end of the previous change in the buffer.

To validate my idea I've used this simple test case:

set undolevels=10

normal ggiline one is full of aaaa
set undolevels=10 " used to break a change into separate undo blocks
normal Goline two is full of bbbb
set undolevels=10 " used to break a change into separate undo blocks
normal Goline three is full of cccc
set undolevels=10 " used to break a change into separate undo blocks

undojoin
keepjumps %s/aaaa/zzzz/
normal u

As you can see, after undoing the last change in the buffer, that is creating the third line, the cursor is correctly returned to the second line in the file.

Since my test worked, I implemented an almost identical `undojoin` in my function (see http://goo.gl/ykwJDS). However, when I undo the last change after the function has run, the cursor is returned to the top most change in the file. This is often a stripped space and is *not* the position of the change I `undojoin`-ed to.

@doliver, in one of the answers on StackOverflow (http://goo.gl/ykwJDS), has done a bit more debugging on this issue and it looks more and more like it might be a problem with the way vim records the change location when executing the `:s/...` command.

However, if at all possible, I would really like someone a bit more knowledgable in the inner workings of vim to have a look at this.

Thanks a lot,

Dmytro

Dmytro Konstantinov

unread,
Aug 3, 2015, 11:36:04 AM8/3/15
to vim_dev
After a bit of digging around we managed to confirm that it is, indeed, a bug in vim. It can be replicated with `vim +"source [filename]"` using the following two tests:

1. [OK] The following test produces the expected result: substitution change is joined to the previous change in the buffer (addition of line 3). Upon undo, the cursor position is correctly restored to the second line in the buffer.

set undolevels=10

normal ggiline one is bull of aaaa


set undolevels=10 " used to break a change into separate undo blocks
normal Goline two is full of bbbb
set undolevels=10 " used to break a change into separate undo blocks
normal Goline three is full of cccc
set undolevels=10 " used to break a change into separate undo blocks

undojoin
keepjumps %s/aaaa/zzzz/
normal u

2. [ERR] The following test produces an unexpected result: substitution change is joined to the previous change in the buffer (addition of line 4). Upon undo, the cursor position is incorrectly restored to the first line in the buffer.

set undolevels=10

normal ggiline one is bull of aaaa


set undolevels=10 " used to break a change into separate undo blocks
normal Goline two is full of bbbb
set undolevels=10 " used to break a change into separate undo blocks
normal Goline three is full of cccc
set undolevels=10 " used to break a change into separate undo blocks

normal Goline four is full of aaaa's again


set undolevels=10 " used to break a change into separate undo blocks

undojoin
keepjumps %s/aaaa/zzzz/
normal u

It appears that the bug is originating around line 2711 in undo.c Sorry, can't be more specific, C is not my strong point, so I'm just summarising what @doliver found so far.

A full description of what we've found is on StackOverflow:

http://stackoverflow.com/questions/31548025/vim-undo-why-does-the-cursor-jump-to-the-wrong-position-when-undoing-undojoin

Christian Brabandt

unread,
Aug 4, 2015, 1:35:55 PM8/4/15
to vim_dev
Hi Dmytro!
I am nut sure, I understand fully. How do you suggest to change the
cursor positioning?

Best,
Christian
--
Das Wort Gottesdienst sollte verlegt und nicht mehr vom Kirchengehen,
sondern bloß von guten Handlungen gebraucht werden.
-- Georg Christoph Lichtenberg (Moralische Bemerkungen)

Dmytro Konstantinov

unread,
Aug 4, 2015, 3:07:36 PM8/4/15
to vim_dev
Hi Christian!

Thanks for getting back to me!

On Tuesday, August 4, 2015 at 6:35:55 PM UTC+1, Christian Brabandt wrote:

> I am nut sure, I understand fully. How do you suggest to change the
> cursor positioning?

I'm afraid I know neither C nor VIM internals well enough to propose a solution. What I can do is prepare the groundwork (describe the inconsistent behaviour, provide the tests) and hope that someone would be willing to pick it up and suggest a fix.

Form everything I've read about the expected behaviour for restoring the cursor positioning (eg: http://vim.wikia.com/wiki/Restore_the_cursor_position_after_undoing_text_change_made_by_a_script), the cursor should be restored "closest to where the first change is made".

However, the inconsistency I found is that two almost identical bits of vimscript make the editor behave in different ways.

# Test 1

Imagine I have a buffer with two lines. I do `:normal Go` to add a 3rd line into that buffer, then undo it. The closest place to where the first (and in this case, the only) change is made is line 2. Thus, the cursor should be restored to line 2.

Now, let's complicate things a little. I have the same buffer with two lines. I do `:normal Go` to add a 3rd line. But this time, instead of `undo`, I do `:undojoin` and then run a substitute command (in my test I use `:keepjumps %s/aaaa/zzzz/`). Only then do I do `:undo`.

As far as positioning of the cursor goes, the above two examples should be absolutely identical. This is because we've `:undojoined` our substitution change onto the back of an existing change, which was the addition of the 3rd line. Thus, the closest place to where the *first* change is made is still line 2, which is where the cursor should be positioned after the `:undo`.

This is exactly how my first test behaves, the one marked with [OK].

# Test 2

My second test, marked with [ERR], displays a different set of behaviour. It demonstrates that under certain conditions, "closest to where the first change is made" rule for the cursor positioning is disregarded and instead the cursor is restored nearest to the first substitution made by the `:s///` command. This appears to be inconsistent with how the cursor should be positioned.

Would anyone be willing to patch this?

Regards,

Dmytro

Christian Brabandt

unread,
Aug 5, 2015, 2:37:51 PM8/5/15
to vim_dev
Hi Dmytro!
Well, I can see that this might be a problem, but I currently don't
understand the code, that set's the cursor position and therefore am not
sure, how to fix the problem.


Best,
Christian
--
Theorie und Erfahrung (Phänomen) stehen gegeneinander in
beständigem Konflikt. Alle Vereinigung in der Reflexion ist eine
Täuschung; nur durch Handeln können sie vereinigt werden.
-- Goethe, Maximen und Reflektionen, Nr. 1099

Dmytro Konstantinov

unread,
Aug 10, 2015, 6:19:10 AM8/10/15
to vim_dev
Hi Christian!

Sorry for the late reply,

On Wednesday, August 5, 2015 at 7:37:51 PM UTC+1, Christian Brabandt wrote:
> > Form everything I've read about the expected behaviour for restoring the cursor positioning (eg: http://vim.wikia.com/wiki/Restore_the_cursor_position_after_undoing_text_change_made_by_a_script), the cursor should be restored "closest to where the first change is made".
> >
> > However, the inconsistency I found is that two almost identical bits of vimscript make the editor behave in different ways.
> >
> > # Test 1
> >
> > Imagine I have a buffer with two lines. I do `:normal Go` to add a 3rd line into that buffer, then undo it. The closest place to where the first (and in this case, the only) change is made is line 2. Thus, the cursor should be restored to line 2.
> >
> > Now, let's complicate things a little. I have the same buffer with two lines. I do `:normal Go` to add a 3rd line. But this time, instead of `undo`, I do `:undojoin` and then run a substitute command (in my test I use `:keepjumps %s/aaaa/zzzz/`). Only then do I do `:undo`.
> >
> > As far as positioning of the cursor goes, the above two examples should be absolutely identical. This is because we've `:undojoined` our substitution change onto the back of an existing change, which was the addition of the 3rd line. Thus, the closest place to where the *first* change is made is still line 2, which is where the cursor should be positioned after the `:undo`.
> >
> > This is exactly how my first test behaves, the one marked with [OK].
> >
> > # Test 2
> >
> > My second test, marked with [ERR], displays a different set of behaviour. It demonstrates that under certain conditions, "closest to where the first change is made" rule for the cursor positioning is disregarded and instead the cursor is restored nearest to the first substitution made by the `:s///` command. This appears to be inconsistent with how the cursor should be positioned.
> >
> > Would anyone be willing to patch this?
>
> Well, I can see that this might be a problem, but I currently don't
> understand the code, that set's the cursor position and therefore am not
> sure, how to fix the problem.

Not a problem at all. As I said before, I don't understand the code enough either. But I think we've established two very important things:

1. We confirmed that described behaviour is a problem

2. We confirmed that the problem lies within VIM

I'm hoping that this might be enough for someone who is a bit more used to VIM's codebase to have a look at this issue and propose a solution…

Thanks for all the help Christian,

Dmytro
Reply all
Reply to author
Forward
0 new messages