Files
neovim/test/old/testdir/test_undo.vim
zeertzjq a0dc3f0067 vim-patch:9.2.0935: reading an undo file is slow with many undo headers (#41285)
Problem:  Reading an undo file resolves every stored sequence number
          with a linear scan over all headers, making loading
          quadratic in the number of undo states.
Solution: Sort uhp_table on uh_seq once and resolve each reference
          with a binary search; the duplicate uh_seq check becomes a
          single pass over the sorted table (Samuel Schlesinger).

At the default 'undolevels' of 1000 the quadratic cost is not
measurable; it takes 'undolevels' in the tens of thousands to matter.
Loading an undo file with 20000 states and 50 alternate branches with
:rundo goes from 1.49s to 0.11s (min of 3, macOS arm64), with the
same undotree().

Also make old_idx/new_idx/cur_idx and the loop index "i" long instead
of short/int: they index uhp_table, whose length num_head is a long
read from the file.  A short index truncated above 32767 headers,
making the restored b_u_oldhead/b_u_newhead/b_u_curhead pointers
wrong in exactly the many-headers case this change is about.

Add tests: a round-trip test with alternate branches that compares
the entries of the tree and the text at every sequence number, a
corruption test with a duplicated uh_seq, and a test for reading an
undo file with zero headers, which is written when only the line for
the "U" command is saved.

closes: vim/vim#20942

fccf613c8f

Co-authored-by: Samuel Schlesinger <sgschlesinger@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-08-12 10:00:09 +08:00

1148 lines
30 KiB
VimL

" Tests for the undo tree.
" Since this script is sourced we need to explicitly break changes up in
" undo-able pieces. Do that by setting 'undolevels'.
" Also tests :earlier and :later.
source check.vim
source screendump.vim
func Test_undotree()
new
normal! Aabc
set ul=100
let d = undotree()
call assert_equal(1, d.seq_last)
call assert_equal(1, d.seq_cur)
call assert_equal(0, d.save_last)
call assert_equal(0, d.save_cur)
call assert_equal(1, len(d.entries))
call assert_equal(1, d.entries[0].newhead)
call assert_equal(1, d.entries[0].seq)
call assert_true(d.entries[0].time <= d.time_cur)
normal! Adef
set ul=100
let d = undotree()
call assert_equal(2, d.seq_last)
call assert_equal(2, d.seq_cur)
call assert_equal(0, d.save_last)
call assert_equal(0, d.save_cur)
call assert_equal(2, len(d.entries))
call assert_equal(1, d.entries[0].seq)
call assert_equal(1, d.entries[1].newhead)
call assert_equal(2, d.entries[1].seq)
call assert_true(d.entries[1].time <= d.time_cur)
undo
set ul=100
let d = undotree()
call assert_equal(2, d.seq_last)
call assert_equal(1, d.seq_cur)
call assert_equal(0, d.save_last)
call assert_equal(0, d.save_cur)
call assert_equal(2, len(d.entries))
call assert_equal(1, d.entries[0].seq)
call assert_equal(1, d.entries[1].curhead)
call assert_equal(1, d.entries[1].newhead)
call assert_equal(2, d.entries[1].seq)
call assert_true(d.entries[1].time == d.time_cur)
normal! Aghi
set ul=100
let d = undotree()
call assert_equal(3, d.seq_last)
call assert_equal(3, d.seq_cur)
call assert_equal(0, d.save_last)
call assert_equal(0, d.save_cur)
call assert_equal(2, len(d.entries))
call assert_equal(1, d.entries[0].seq)
call assert_equal(2, d.entries[1].alt[0].seq)
call assert_equal(1, d.entries[1].newhead)
call assert_equal(3, d.entries[1].seq)
call assert_true(d.entries[1].time <= d.time_cur)
undo
set ul=100
let d = undotree()
call assert_equal(3, d.seq_last)
call assert_equal(1, d.seq_cur)
call assert_equal(0, d.save_last)
call assert_equal(0, d.save_cur)
call assert_equal(2, len(d.entries))
call assert_equal(1, d.entries[0].seq)
call assert_equal(2, d.entries[1].alt[0].seq)
call assert_equal(1, d.entries[1].curhead)
call assert_equal(1, d.entries[1].newhead)
call assert_equal(3, d.entries[1].seq)
call assert_true(d.entries[1].time == d.time_cur)
w! Xtest
let d = undotree()
call assert_equal(1, d.save_cur)
call assert_equal(1, d.save_last)
call delete('Xtest')
bwipe! Xtest
endfunc
func FillBuffer()
for i in range(1,13)
put=i
" Set 'undolevels' to split undo.
exe "setg ul=" . &g:ul
endfor
endfunc
func Test_undotree_bufnr()
new
let buf1 = bufnr()
normal! Aabc
set ul=100
" Save undo tree without bufnr as ground truth for buffer 1
let d1 = undotree()
new
let buf2 = bufnr()
normal! Adef
set ul=100
normal! Aghi
set ul=100
" Save undo tree without bufnr as ground truth for buffer 2
let d2 = undotree()
" Check undotree() with bufnr argument
let d = undotree(buf1)
call assert_equal(d1, d)
call assert_notequal(d2, d)
let d = undotree(buf2)
call assert_notequal(d1, d)
call assert_equal(d2, d)
" Switch buffers and check again
wincmd p
let d = undotree(buf1)
call assert_equal(d1, d)
let d = undotree(buf2)
call assert_notequal(d1, d)
call assert_equal(d2, d)
" error cases
call assert_fails('call undotree(-1)', 'E158:')
call assert_fails('call undotree("nosuchbuf")', 'E158:')
" after creating a buffer nosuchbuf, undotree('nosuchbuf') should
" not error out
new nosuchbuf
let d = {'seq_last': 0, 'entries': [], 'time_cur': 0, 'save_last': 0, 'synced': 1, 'save_cur': 0, 'seq_cur': 0}
call assert_equal(d, undotree("nosuchbuf"))
" clean up
bw nosuchbuf
" Drop created windows
set ul&
new
only!
endfunc
func Test_global_local_undolevels()
new one
set undolevels=5
call FillBuffer()
" will only undo the last 5 changes, end up with 13 - (5 + 1) = 7 lines
earlier 10
call assert_equal(5, &g:undolevels)
call assert_equal(-123456, &l:undolevels)
call assert_equal('7', getline('$'))
new two
setlocal undolevels=2
call FillBuffer()
" will only undo the last 2 changes, end up with 13 - (2 + 1) = 10 lines
earlier 10
call assert_equal(5, &g:undolevels)
call assert_equal(2, &l:undolevels)
call assert_equal('10', getline('$'))
setlocal ul=10
call assert_equal(5, &g:undolevels)
call assert_equal(10, &l:undolevels)
" Setting local value in "two" must not change local value in "one"
wincmd p
call assert_equal(5, &g:undolevels)
call assert_equal(-123456, &l:undolevels)
new three
setglobal ul=50
call assert_equal(50, &g:undolevels)
call assert_equal(-123456, &l:undolevels)
" Resetting the local 'undolevels' value to use the global value
setlocal undolevels=5
"setlocal undolevels<
set undolevels<
call assert_equal(-123456, &l:undolevels)
" Drop created windows
set ul&
new
only!
endfunc
func BackOne(expected)
call feedkeys('g-', 'xt')
call assert_equal(a:expected, getline(1))
endfunc
func Test_undo_del_chars()
throw 'Skipped: Nvim does not support test_settime()'
" Setup a buffer without creating undo entries
new
set ul=-1
call setline(1, ['123-456'])
set ul=100
1
call test_settime(100)
" Delete three characters and undo with g-
call feedkeys('x', 'xt')
call feedkeys('x', 'xt')
call feedkeys('x', 'xt')
call assert_equal('-456', getline(1))
call BackOne('3-456')
call BackOne('23-456')
call BackOne('123-456')
call assert_fails("BackOne('123-456')", "E492: Not an editor command: BackOne('123-456')")
:" Delete three other characters and go back in time with g-
call feedkeys('$x', 'xt')
call feedkeys('x', 'xt')
call feedkeys('x', 'xt')
call assert_equal('123-', getline(1))
call test_settime(101)
call BackOne('123-4')
call BackOne('123-45')
" skips '123-456' because it's older
call BackOne('-456')
call BackOne('3-456')
call BackOne('23-456')
call BackOne('123-456')
call assert_fails("BackOne('123-456')", "E492: Not an editor command: BackOne('123-456')")
normal 10g+
call assert_equal('123-', getline(1))
:" Jump two seconds and go some seconds forward and backward
call test_settime(103)
call feedkeys("Aa\<Esc>", 'xt')
call feedkeys("Ab\<Esc>", 'xt')
call feedkeys("Ac\<Esc>", 'xt')
call assert_equal('123-abc', getline(1))
earlier 1s
call assert_equal('123-', getline(1))
earlier 3s
call assert_equal('123-456', getline(1))
later 1s
call assert_equal('123-', getline(1))
later 1h
call assert_equal('123-abc', getline(1))
close!
endfunc
func Test_undolist()
new
set ul=100
let a = execute('undolist')
call assert_equal("\nNothing to undo", a)
" 1 leaf (2 changes).
call feedkeys('achange1', 'xt')
call feedkeys('achange2', 'xt')
let a = execute('undolist')
call assert_match("^\nnumber changes when *saved\n *2 *2 .*$", a)
" 2 leaves.
call feedkeys('u', 'xt')
call feedkeys('achange3\<Esc>', 'xt')
let a = execute('undolist')
call assert_match("^\nnumber changes when *saved\n *2 *2 *.*\n *3 *2 .*$", a)
close!
endfunc
func Test_U_command()
new
set ul=100
call feedkeys("achange1\<Esc>", 'xt')
call feedkeys("achange2\<Esc>", 'xt')
norm! U
call assert_equal('', getline(1))
norm! U
call assert_equal('change1change2', getline(1))
close!
endfunc
func Test_undojoin()
new
call feedkeys("Goaaaa\<Esc>", 'xt')
call feedkeys("obbbb\<Esc>", 'xt')
call assert_equal(['aaaa', 'bbbb'], getline(2, '$'))
call feedkeys("u", 'xt')
call assert_equal(['aaaa'], getline(2, '$'))
call feedkeys("obbbb\<Esc>", 'xt')
undojoin
" Note: next change must not be as if typed
call feedkeys("occcc\<Esc>", 'x')
call assert_equal(['aaaa', 'bbbb', 'cccc'], getline(2, '$'))
call feedkeys("u", 'xt')
call assert_equal(['aaaa'], getline(2, '$'))
bwipe!
endfunc
func Test_undojoin_redo()
new
call setline(1, ['first line', 'second line'])
call feedkeys("ixx\<Esc>", 'xt')
call feedkeys(":undojoin | redo\<CR>", 'xt')
call assert_equal('xxfirst line', getline(1))
call assert_equal('second line', getline(2))
bwipe!
endfunc
" undojoin not allowed after undo
func Test_undojoin_after_undo()
new
call feedkeys("ixx\<Esc>u", 'xt')
call assert_fails(':undojoin', 'E790:')
bwipe!
endfunc
" undojoin is a noop when no change yet, or when 'undolevels' is negative
func Test_undojoin_noop()
new
call feedkeys(":undojoin\<CR>", 'xt')
call assert_equal([''], getline(1, '$'))
setlocal undolevels=-1
call feedkeys("ixx\<Esc>u", 'xt')
call feedkeys(":undojoin\<CR>", 'xt')
call assert_equal(['xx'], getline(1, '$'))
bwipe!
endfunc
func Test_undo_write()
call delete('Xtest')
split Xtest
call feedkeys("ione one one\<Esc>", 'xt')
w!
call feedkeys("otwo\<Esc>", 'xt')
call feedkeys("otwo\<Esc>", 'xt')
w
call feedkeys("othree\<Esc>", 'xt')
call assert_equal(['one one one', 'two', 'two', 'three'], getline(1, '$'))
earlier 1f
call assert_equal(['one one one', 'two', 'two'], getline(1, '$'))
earlier 1f
call assert_equal(['one one one'], getline(1, '$'))
earlier 1f
call assert_equal([''], getline(1, '$'))
later 1f
call assert_equal(['one one one'], getline(1, '$'))
later 1f
call assert_equal(['one one one', 'two', 'two'], getline(1, '$'))
later 1f
call assert_equal(['one one one', 'two', 'two', 'three'], getline(1, '$'))
close!
call delete('Xtest')
bwipe! Xtest
call assert_fails('earlier xyz', 'E475:')
endfunc
func Test_insert_expr()
new
" calling setline() triggers undo sync
call feedkeys("oa\<Esc>", 'xt')
call feedkeys("ob\<Esc>", 'xt')
set ul=100
call feedkeys("o1\<Esc>a2\<C-R>=setline('.','1234')\<CR>\<CR>\<Esc>", 'x')
call assert_equal(['a', 'b', '120', '34'], getline(2, '$'))
call feedkeys("u", 'x')
call assert_equal(['a', 'b', '12'], getline(2, '$'))
call feedkeys("u", 'x')
call assert_equal(['a', 'b'], getline(2, '$'))
call feedkeys("oc\<Esc>", 'xt')
set ul=100
call feedkeys("o1\<Esc>a2\<C-R>=setline('.','1234')\<CR>\<CR>\<Esc>", 'x')
call assert_equal(['a', 'b', 'c', '120', '34'], getline(2, '$'))
call feedkeys("u", 'x')
call assert_equal(['a', 'b', 'c', '12'], getline(2, '$'))
call feedkeys("od\<Esc>", 'xt')
set ul=100
call feedkeys("o1\<Esc>a2\<C-R>=string(123)\<CR>\<Esc>", 'x')
call assert_equal(['a', 'b', 'c', '12', 'd', '12123'], getline(2, '$'))
call feedkeys("u", 'x')
call assert_equal(['a', 'b', 'c', '12', 'd'], getline(2, '$'))
close!
endfunc
func Test_undofile_earlier()
throw 'Skipped: Nvim does not support test_settime()'
if has('win32')
" FIXME: This test is flaky on MS-Windows.
let g:test_is_flaky = 1
endif
" Issue #1254
" create undofile with timestamps older than Vim startup time.
let t0 = localtime() - 43200
call test_settime(t0)
new XfileEarlier
call feedkeys("ione\<Esc>", 'xt')
set ul=100
call test_settime(t0 + 1)
call feedkeys("otwo\<Esc>", 'xt')
set ul=100
call test_settime(t0 + 2)
call feedkeys("othree\<Esc>", 'xt')
set ul=100
w
wundo Xundofile
bwipe!
" restore normal timestamps.
call test_settime(0)
new XfileEarlier
rundo Xundofile
earlier 1d
call assert_equal('', getline(1))
bwipe!
call delete('XfileEarlier')
call delete('Xundofile')
endfunc
func Test_wundo_errors()
new
call setline(1, 'hello')
call assert_fails('wundo! Xdoesnotexist/Xundofile', 'E828:')
bwipe!
endfunc
" Check that reading a truncated undo file doesn't hang.
func Test_undofile_truncated()
new
call setline(1, 'hello')
set ul=100
wundo Xundofile
let contents = readfile('Xundofile', 'B')
" try several sizes
for size in range(20, 500, 33)
call writefile(contents[0:size], 'Xundofile')
call assert_fails('rundo Xundofile', 'E825:')
endfor
bwipe!
call delete('Xundofile')
endfunc
func Test_rundo_errors()
call assert_fails('rundo XfileDoesNotExist', 'E822:')
call writefile(['abc'], 'Xundofile')
call assert_fails('rundo Xundofile', 'E823:')
call delete('Xundofile')
endfunc
func Test_undofile_next()
set undofile
new Xfoo.txt
execute "norm ix\<c-g>uy\<c-g>uz\<Esc>"
write
bwipe
next Xfoo.txt
call assert_equal('xyz', getline(1))
silent undo
call assert_equal('xy', getline(1))
silent undo
call assert_equal('x', getline(1))
bwipe!
call delete('Xfoo.txt')
call delete('.Xfoo.txt.un~')
set undofile&
endfunc
" Test for undo working properly when executing commands from a register.
" Also test this in an empty buffer.
func Test_cmd_in_reg_undo()
enew!
let @a = "Ox\<Esc>jAy\<Esc>kdd"
edit +/^$ test_undo.vim
normal @au
call assert_equal(0, &modified)
return
new
normal @au
call assert_equal(0, &modified)
only!
let @a = ''
endfunc
" This used to cause an illegal memory access
func Test_undo_append()
new
call feedkeys("axx\<Esc>v", 'xt')
undo
norm o
quit
endfunc
func Test_undo_0()
new
set ul=100
normal i1
undo
normal i2
undo
normal i3
undo 0
let d = undotree()
call assert_equal('', getline(1))
call assert_equal(0, d.seq_cur)
redo
let d = undotree()
call assert_equal('3', getline(1))
call assert_equal(3, d.seq_cur)
undo 2
undo 0
let d = undotree()
call assert_equal('', getline(1))
call assert_equal(0, d.seq_cur)
redo
let d = undotree()
call assert_equal('2', getline(1))
call assert_equal(2, d.seq_cur)
undo 1
undo 0
let d = undotree()
call assert_equal('', getline(1))
call assert_equal(0, d.seq_cur)
redo
let d = undotree()
call assert_equal('1', getline(1))
call assert_equal(1, d.seq_cur)
bwipe!
endfunc
" undo or redo are noop if there is nothing to undo or redo
func Test_undo_redo_noop()
new
call assert_fails('undo 2', 'E830:')
message clear
undo
let messages = split(execute('message'), "\n")
call assert_equal('Already at oldest change', messages[-1])
message clear
redo
let messages = split(execute('message'), "\n")
call assert_equal('Already at newest change', messages[-1])
bwipe!
endfunc
func Test_redo_empty_line()
new
exe "norm\x16r\x160"
exe "norm."
bwipe!
endfunc
funct Test_undofile()
" Test undofile() without setting 'undodir'.
if has('persistent_undo')
call assert_equal(fnamemodify('.Xundofoo.un~', ':p'), undofile('Xundofoo'))
else
call assert_equal('', undofile('Xundofoo'))
endif
call assert_equal('', undofile(''))
" Test undofile() with 'undodir' set to an existing directory.
call mkdir('Xundodir')
set undodir=Xundodir
let cwd = getcwd()
if has('win32')
" Replace windows drive such as C:... into C%...
let cwd = substitute(cwd, '^\([a-zA-Z]\):', '\1%', 'g')
endif
let cwd = substitute(cwd . '/Xundofoo', '/', '%', 'g')
if has('persistent_undo')
call assert_equal('Xundodir/' . cwd, undofile('Xundofoo'))
else
call assert_equal('', undofile('Xundofoo'))
endif
call assert_equal('', undofile(''))
call delete('Xundodir', 'd')
" Test undofile() with 'undodir' set to a non-existing directory.
" call assert_equal('', 'Xundofoo'->undofile())
if isdirectory('/tmp')
set undodir=/tmp
if has('osx')
call assert_equal('/tmp/%private%tmp%file', undofile('///tmp/file'))
else
call assert_equal('/tmp/%tmp%file', undofile('///tmp/file'))
endif
endif
set undodir&
endfunc
" Tests for the undo file
" Explicitly break changes up in undo-able pieces by setting 'undolevels'.
func Test_undofile_2()
set undolevels=100 undofile
edit Xtestfile
call append(0, 'this is one line')
call cursor(1, 1)
" first a simple one-line change.
set undolevels=100
s/one/ONE/
set undolevels=100
write
bwipe!
edit Xtestfile
undo
call assert_equal('this is one line', getline(1))
" change in original file fails check
set noundofile
edit! Xtestfile
s/line/Line/
write
set undofile
bwipe!
edit Xtestfile
undo
call assert_equal('this is ONE Line', getline(1))
" add 10 lines, delete 6 lines, undo 3
set undofile
call setbufline('%', 1, ['one', 'two', 'three', 'four', 'five', 'six',
\ 'seven', 'eight', 'nine', 'ten'])
set undolevels=100
normal 3Gdd
set undolevels=100
normal dd
set undolevels=100
normal dd
set undolevels=100
normal dd
set undolevels=100
normal dd
set undolevels=100
normal dd
set undolevels=100
write
bwipe!
edit Xtestfile
normal uuu
call assert_equal(['one', 'two', 'six', 'seven', 'eight', 'nine', 'ten'],
\ getline(1, '$'))
" Test that reading the undofiles when setting undofile works
set noundofile undolevels=0
exe "normal i\n"
undo
edit! Xtestfile
set undofile undolevels=100
normal uuuuuu
call assert_equal(['one', 'two', 'three', 'four', 'five', 'six', 'seven',
\ 'eight', 'nine', 'ten'], getline(1, '$'))
bwipe!
call delete('Xtestfile')
let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
call delete(ufile)
set undofile& undolevels&
endfunc
" Test 'undofile' using a file encrypted with 'zip' crypt method
func Test_undofile_cryptmethod_zip()
throw 'skipped: Nvim does not support cryptmethod'
edit Xtestfile
set undofile cryptmethod=zip
call append(0, ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
call cursor(5, 1)
set undolevels=100
normal kkkdd
set undolevels=100
normal dd
set undolevels=100
normal dd
set undolevels=100
" encrypt the file using key 'foobar'
call feedkeys("foobar\nfoobar\n")
X
write!
bwipe!
call feedkeys("foobar\n")
edit Xtestfile
set key=
normal uu
call assert_equal(['monday', 'wednesday', 'thursday', 'friday', ''],
\ getline(1, '$'))
bwipe!
call delete('Xtestfile')
let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
call delete(ufile)
set undofile& undolevels& cryptmethod&
endfunc
" Test 'undofile' using a file encrypted with 'blowfish' crypt method
func Test_undofile_cryptmethod_blowfish()
throw 'skipped: Nvim does not support cryptmethod'
edit Xtestfile
set undofile cryptmethod=blowfish
call append(0, ['jan', 'feb', 'mar', 'apr', 'jun'])
call cursor(5, 1)
set undolevels=100
exe 'normal kk0ifoo '
set undolevels=100
normal dd
set undolevels=100
exe 'normal ibar '
set undolevels=100
" encrypt the file using key 'foobar'
call feedkeys("foobar\nfoobar\n")
X
write!
bwipe!
call feedkeys("foobar\n")
edit Xtestfile
set key=
call search('bar')
call assert_equal('bar apr', getline('.'))
undo
call assert_equal('apr', getline('.'))
undo
call assert_equal('foo mar', getline('.'))
undo
call assert_equal('mar', getline('.'))
bwipe!
call delete('Xtestfile')
let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
call delete(ufile)
set undofile& undolevels& cryptmethod&
endfunc
" Test 'undofile' using a file encrypted with 'blowfish2' crypt method
func Test_undofile_cryptmethod_blowfish2()
throw 'skipped: Nvim does not support cryptmethod'
edit Xtestfile
set undofile cryptmethod=blowfish2
call append(0, ['jan', 'feb', 'mar', 'apr', 'jun'])
call cursor(5, 1)
set undolevels=100
exe 'normal kk0ifoo '
set undolevels=100
normal dd
set undolevels=100
exe 'normal ibar '
set undolevels=100
" encrypt the file using key 'foo2bar'
call feedkeys("foo2bar\nfoo2bar\n")
X
write!
bwipe!
call feedkeys("foo2bar\n")
edit Xtestfile
set key=
call search('bar')
call assert_equal('bar apr', getline('.'))
normal u
call assert_equal('apr', getline('.'))
normal u
call assert_equal('foo mar', getline('.'))
normal u
call assert_equal('mar', getline('.'))
bwipe!
call delete('Xtestfile')
let ufile = has('vms') ? '_un_Xtestfile' : '.Xtestfile.un~'
call delete(ufile)
set undofile& undolevels& cryptmethod&
endfunc
" Test for redoing with incrementing numbered registers
func Test_redo_repeat_numbered_register()
new
for [i, v] in [[1, 'one'], [2, 'two'], [3, 'three'],
\ [4, 'four'], [5, 'five'], [6, 'six'],
\ [7, 'seven'], [8, 'eight'], [9, 'nine']]
exe 'let @' .. i .. '="' .. v .. '\n"'
endfor
call feedkeys('"1p.........', 'xt')
call assert_equal(['', 'one', 'two', 'three', 'four', 'five', 'six',
\ 'seven', 'eight', 'nine', 'nine'], getline(1, '$'))
bwipe!
endfunc
" Test for redo in insert mode using CTRL-O with multibyte characters
func Test_redo_multibyte_in_insert_mode()
new
call feedkeys("a\<C-K>ft", 'xt')
call feedkeys("uiHe\<C-O>.llo", 'xt')
call assert_equal("He\ufb05llo", getline(1))
bwipe!
endfunc
func Test_undo_mark()
new
" The undo is applied to the only line.
call setline(1, 'hello')
call feedkeys("ggyiw$p", 'xt')
undo
call assert_equal([0, 1, 1, 0], getpos("'["))
call assert_equal([0, 1, 1, 0], getpos("']"))
" The undo removes the last line.
call feedkeys("Goaaaa\<Esc>", 'xt')
call feedkeys("obbbb\<Esc>", 'xt')
undo
call assert_equal([0, 2, 1, 0], getpos("'["))
call assert_equal([0, 2, 1, 0], getpos("']"))
bwipe!
endfunc
func Test_undo_after_write()
CheckScreendump
" use a terminal to make undo work like when text is typed
CheckRunVimInTerminal
let lines =<< trim END
edit Xtestfile.txt
set undolevels=100 undofile
imap . <Cmd>write<CR>
write
END
call writefile(lines, 'Xtest_undo_after_write', 'D')
let buf = RunVimInTerminal('-S Xtest_undo_after_write', #{rows: 6})
call term_sendkeys(buf, "Otest.\<CR>boo!!!\<Esc>")
sleep 100m
call term_sendkeys(buf, "u")
call VerifyScreenDump(buf, 'Test_undo_after_write_1', {})
call term_sendkeys(buf, "u")
call VerifyScreenDump(buf, 'Test_undo_after_write_2', {})
call StopVimInTerminal(buf)
call delete('Xtestfile.txt')
call delete('.Xtestfile.txt.un~')
endfunc
func Test_undo_range_normal()
new
call setline(1, ['asa', 'bsb'])
let &l:undolevels = &l:undolevels
%normal dfs
call assert_equal(['a', 'b'], getline(1, '$'))
undo
call assert_equal(['asa', 'bsb'], getline(1, '$'))
bwipe!
endfunc
func Test_load_existing_undofile()
CheckFeature persistent_undo
sp samples/test_undo.txt
let mess=execute(':verbose rundo samples/test_undo.txt.undo')
call assert_match('Finished reading undo file', mess)
call assert_equal(['one', 'two', 'three'], getline(1, '$'))
norm! u
call assert_equal(['one', 'two'], getline(1, '$'))
norm! u
call assert_equal(['one'], getline(1, '$'))
norm! u
call assert_equal([''], getline(1, '$'))
let mess = execute(':norm! u')
call assert_equal([''], getline(1, '$'))
call assert_match('Already at oldest change', mess)
exe ":norm! \<c-r>"
call assert_equal(['one'], getline(1, '$'))
exe ":norm! \<c-r>"
call assert_equal(['one', 'two'], getline(1, '$'))
exe ":norm! \<c-r>"
call assert_equal(['one', 'two', 'three'], getline(1, '$'))
let mess = execute(":norm! \<c-r>")
call assert_equal(['one', 'two', 'three'], getline(1, '$'))
call assert_match('Already at newest change', mess)
bw!
endfunc
func Test_restore_cursor_position_after_undo()
CheckFeature persistent_undo
sp samples/test_undo.txt
3 | exe "norm! gqk" | undojoin | 1 delete
call assert_equal(1, line('.'))
norm! u
call assert_equal(3, line('.'))
bw!
endfunc
func Test_undo_line_backspace_after_insert_cmd_cursor_movement()
new
setlocal backspace=eol undolevels=100
call setline(1, ['', '', 'abc', 'def'])
call cursor(4, 1)
let v:errmsg = ''
call feedkeys("i\<Cmd>setlocal undolevels=101 | call cursor(3, 1)\<CR>"
\ .. "\<BS>\<BS>\<Esc>u", 'xt')
call assert_equal('', v:errmsg)
call assert_equal(['', '', 'abc', 'def'], getline(1, '$'))
bwipe!
endfunc
func Test_undo_line_backspace_after_insert_func_edit()
new
setlocal backspace=eol undolevels=100
let v:errmsg = ''
call feedkeys("i\<CR>"
\ .. "\<Cmd>call setline(2, 'abc')\<CR>"
\ .. "\<BS>\<Esc>u", 'xt')
call assert_equal('', v:errmsg)
call assert_equal([''], getline(1, '$'))
bwipe!
endfunc
func Test_undo_line_backspace_after_insert_cmd_edit()
new
setlocal backspace=eol undolevels=100
let v:errmsg = ''
call feedkeys("i\<CR>"
\ .. "\<Cmd>s/.*/abc/\<CR>"
\ .. "\<BS>\<Esc>u", 'xt')
call assert_equal('', v:errmsg)
call assert_equal([''], getline(1, '$'))
bwipe!
endfunc
" Corrupted undo file via cyclic cross-references caused
" double free
func Test_corrupted_undofile()
CheckFeature persistent_undo
let _uf = &undofile
set undofile
new
call setline(1, 'hello')
let b=eval('0z56696D9F556E446FE50002F3AEFE62965A91903610' ..
\ 'F0E23CC8A69D5B87CEA6D28E75489B0D2CA02ED7993C' ..
\ '00000001000000000000000000000000000000010000' ..
\ '00010000000100000001000000010000000100000000' ..
\ '3B9ACA00005FD0000000010000000000000000000000' ..
\ '00000000010000000100000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '00000000000000000000000000000000000000000000' ..
\ '000000000000000000000000000000000000003B9ACA' ..
\ '00003581E7AA')
" Nvim: convert undo file format
let b = b->slice(0, -2) + 0z3581 + b->slice(-2) | let b[10] = 0x03
call writefile(b, 'Xundo', 'bD')
rundo Xundo
bw!
let &undofile = _uf
endfunc
" Test that an undo file with alternate branches round-trips: the tree
" structure and the text at every sequence number survive :wundo + :rundo.
func Test_undofile_branches()
CheckFeature persistent_undo
let save_ul = &undolevels
defer execute('let &undolevels = ' .. save_ul)
new Xubranches.txt
setl noswapfile
set ul=100
call setline(1, 'a')
for i in range(5)
let &undolevels = &undolevels
call setline(1, 'main' .. i)
endfor
" create two alternate branches
silent undo 3
let &undolevels = &undolevels
call setline(1, 'branch-a')
silent undo 2
let &undolevels = &undolevels
call setline(1, 'branch-b')
write
defer delete('Xubranches.txt')
wundo! Xubranches.undo
defer delete('Xubranches.undo')
let tree_before = undotree()
" remember the text at every undo state
let texts = {}
for seq in range(1, tree_before.seq_last)
exe 'silent undo ' .. seq
let texts[seq] = getline(1)
endfor
bwipe!
edit Xubranches.txt
setl noswapfile
rundo Xubranches.undo
let tree_after = undotree()
call assert_equal(tree_before.seq_last, tree_after.seq_last)
" every field of the entries, including times and save numbers, must
" round-trip through the undo file exactly
call assert_equal(tree_before.entries, tree_after.entries)
for [seq, text] in items(texts)
exe 'silent undo ' .. seq
call assert_equal(text, getline(1), 'text at undo state ' .. seq)
endfor
bwipe!
endfunc
" Test that a duplicated sequence number in an undo file is detected.
func Test_undofile_duplicate_seq()
CheckFeature persistent_undo
let save_ul = &undolevels
defer execute('let &undolevels = ' .. save_ul)
new Xudupseq.txt
setl noswapfile
set ul=100
call setline(1, 'one')
let &undolevels = &undolevels
call setline(1, 'two')
let &undolevels = &undolevels
call setline(1, 'three')
write
defer delete('Xudupseq.txt')
wundo! Xudupseq.undo
defer delete('Xudupseq.undo')
" Overwrite the uh_seq of the second header with that of the first. A
" header starts with the magic bytes 0x5f 0xd0, followed by four 4-byte
" header references and then the 4-byte uh_seq. Require the references
" and uh_seq to be small numbers, so that a timestamp that happens to
" contain the magic bytes is not mistaken for a header.
let blob = readfile('Xudupseq.undo', 'B')
let headers = []
for i in range(len(blob) - 22)
if blob[i] == 0x5f && blob[i + 1] == 0xd0
let ok = v:true
for field in range(5)
let off = i + 2 + field * 4
if blob[off] != 0 || blob[off + 1] != 0 || blob[off + 2] != 0
\ || blob[off + 3] > 8
let ok = v:false
break
endif
endfor
if ok
call add(headers, i)
endif
endif
endfor
call assert_true(len(headers) >= 2, 'found undo file headers')
let first = headers[0] + 18
let second = headers[1] + 18
" Check that the detected headers are the intended ones before patching
" any bytes: the headers are written oldest first, so the first two carry
" sequence numbers 1 and 2.
call assert_equal(0z00000001, blob[first : first + 3])
call assert_equal(0z00000002, blob[second : second + 3])
let blob[second : second + 3] = blob[first : first + 3]
call writefile(blob, 'Xudupseq.undo')
call assert_fails('rundo Xudupseq.undo', 'E825:')
bwipe!
endfunc
" Test reading an undo file with zero undo headers, which is written when
" only the line for the "U" command is saved, e.g. with 'undolevels' -1.
func Test_undofile_zero_headers()
CheckFeature persistent_undo
let save_ul = &undolevels
defer execute('let &undolevels = ' .. save_ul)
" The buffer must not collect any undo header, so create the file on disk
" directly and only change the buffer with 'undolevels' already negative.
call writefile(['hello', 'world'], 'Xuzero.txt', 'D')
set undolevels=-1
edit Xuzero.txt
normal! x
wundo! Xuzero.undo
defer delete('Xuzero.undo')
bwipe!
" Make the same change so that the buffer text matches the hash stored in
" the undo file, then read the undo file back.
edit Xuzero.txt
normal! x
let v:errmsg = ''
" a silent rejection of the undo file gives a warning, not an error
let v:warningmsg = ''
rundo Xuzero.undo
call assert_equal('', v:errmsg)
call assert_equal('', v:warningmsg)
normal! U
call assert_equal('hello', getline(1))
bwipe!
endfunc
" vim: shiftwidth=2 sts=2 expandtab