view src/testdir/test_listener.vim @ 33674:021e5bb88513 v9.0.2074

patch 9.0.2074: Completion menu may be wrong Commit: https://github.com/vim/vim/commit/daef8c74375141974d61b85199b383017644978c Author: Christian Brabandt <cb@256bit.org> Date: Fri Oct 27 19:16:26 2023 +0200 patch 9.0.2074: Completion menu may be wrong Problem: Completion menu may be wrong Solution: Check for the original direction of the completion menu, add more tests, make it work with 'noselect' completion: move in right direction when filling completion_info() When moving through the insert completion menu and switching directions, we need to make sure we start at the correct position in the list and move correctly forward/backwards through it, so that we do not skip entries and the selected item points to the correct entry in the list of completion entries generated by the completion_info() function. The general case is this: 1) CTRL-X CTRL-N, we will traverse the list starting from compl_first_match and then go forwards (using the cp->next pointer) through the list (skipping the very first entry, which has the CP_ORIGINAL_TEXT flag set (since that is the empty/non-selected entry 2) CTRL-X CTRL-P, we will traverse the list starting from compl_first_match (which now points to the last entry). The previous entry will have the CP_ORIGINAL_TEXT flag set, so we need to start traversing the list from the second prev pointer. There are in fact 2 special cases after starting the completion menu with CTRL-X: 3) CTRL-N and then going backwards by pressing CTRL-P again. compl_first_match will point to the same entry as in step 1 above, but since compl_dir_foward() has been switched by pressing CTRL-P to backwards we need to pretend to be in still in case 1 and still traverse the list in forward direction using the cp_next pointer 4) CTRL-P and then going forwards by pressing CTRL-N again. compl_first_match will point to the same entry as in step 2 above, but since compl_dir_foward() has been switched by pressing CTRL-N to forwards we need to pretend to be in still in case 2 and still traverse the list in backward direction using the cp_prev pointer For the 'noselect' case however, this is slightly different again. When going backwards, we only need to go one cp_prev pointer back. And resting of the direction works again slightly different. So we need to take the noselect option into account when deciding in which direction to iterate through the list of matches. related: #13402 related: #12971 closes: #13408 Signed-off-by: Christian Brabandt <cb@256bit.org>
author Christian Brabandt <cb@256bit.org>
date Fri, 27 Oct 2023 19:30:05 +0200
parents 695b50472e85
children
line wrap: on
line source

" tests for listener_add() and listener_remove()

func s:StoreList(s, e, a, l)
  let s:start = a:s
  let s:end = a:e
  let s:added = a:a
  let s:text = getline(a:s)
  let s:list = a:l
endfunc

func s:AnotherStoreList(l)
  let s:list2 = a:l
endfunc

func s:EvilStoreList(l)
  let s:list3 = a:l
  call assert_fails("call add(a:l, 'myitem')", "E742:")
endfunc

func Test_listening()
  new
  call setline(1, ['one', 'two'])
  let s:list = []
  let id = listener_add({b, s, e, a, l -> s:StoreList(s, e, a, l)})
  call setline(1, 'one one')
  call listener_flush()
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list)

  " Undo is also a change
  set undolevels&  " start new undo block
  call append(2, 'two two')
  undo
  call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}], s:list)
  redraw
  " the two changes are not merged
  call assert_equal([{'lnum': 3, 'end': 4, 'col': 1, 'added': -1}], s:list)
  1

  " Two listeners, both get called.  Also check column.
  call setline(1, ['one one', 'two'])
  call listener_flush()
  let id2 = listener_add({b, s, e, a, l -> s:AnotherStoreList(l)})
  let s:list = []
  let s:list2 = []
  exe "normal $asome\<Esc>"
  redraw
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 8, 'added': 0}], s:list)
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 8, 'added': 0}], s:list2)

  " removing listener works
  call listener_remove(id2)
  call setline(1, ['one one', 'two'])
  call listener_flush()
  let s:list = []
  let s:list2 = []
  call setline(3, 'three')
  redraw
  call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1}], s:list)
  call assert_equal([], s:list2)

  " a change above a previous change without a line number change is reported
  " together
  call setline(1, ['one one', 'two'])
  call listener_flush(bufnr())
  call append(2, 'two two')
  call setline(1, 'something')
  call bufnr()->listener_flush()
  call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1},
	\ {'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list)
  call assert_equal(1, s:start)
  call assert_equal(3, s:end)
  call assert_equal(1, s:added)

  " an insert just above a previous change that was the last one does not get
  " merged
  call setline(1, ['one one', 'two'])
  call listener_flush()
  let s:list = []
  call setline(2, 'something')
  call append(1, 'two two')
  call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list)
  call listener_flush()
  call assert_equal([{'lnum': 2, 'end': 2, 'col': 1, 'added': 1}], s:list)

  " an insert above a previous change causes a flush
  call setline(1, ['one one', 'two'])
  call listener_flush()
  call setline(2, 'something')
  call append(0, 'two two')
  call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list)
  call assert_equal('something', s:text)
  call listener_flush()
  call assert_equal([{'lnum': 1, 'end': 1, 'col': 1, 'added': 1}], s:list)
  call assert_equal('two two', s:text)

  " a delete at a previous change that was the last one does not get merged
  call setline(1, ['one one', 'two'])
  call listener_flush()
  let s:list = []
  call setline(2, 'something')
  2del
  call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list)
  call listener_flush()
  call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': -1}], s:list)

  " a delete above a previous change causes a flush
  call setline(1, ['one one', 'two'])
  call listener_flush()
  call setline(2, 'another')
  1del
  call assert_equal([{'lnum': 2, 'end': 3, 'col': 1, 'added': 0}], s:list)
  call assert_equal(2, s:start)
  call assert_equal('another', s:text)
  call listener_flush()
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': -1}], s:list)
  call assert_equal('another', s:text)

  " the "o" command first adds an empty line and then changes it
  %del
  call setline(1, ['one one', 'two'])
  call listener_flush()
  let s:list = []
  exe "normal Gofour\<Esc>"
  redraw
  call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1},
	\ {'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list)

  " Remove last listener
  let s:list = []
  call listener_remove(id)
  call setline(1, 'asdfasdf')
  redraw
  call assert_equal([], s:list)

  " Trying to change the list fails
  let id = listener_add({b, s, e, a, l -> s:EvilStoreList(l)})
  let s:list3 = []
  call setline(1, 'asdfasdf')
  redraw
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list3)

  eval id->listener_remove()
  bwipe!
endfunc

func s:StoreListArgs(buf, start, end, added, list)
  let s:buf = a:buf
  let s:start = a:start
  let s:end = a:end
  let s:added = a:added
  let s:list = a:list
endfunc

func Test_listener_args()
  new
  call setline(1, ['one', 'two'])
  let s:list = []
  let id = listener_add('s:StoreListArgs')

  " just one change
  call setline(1, 'one one')
  call listener_flush()
  call assert_equal(bufnr(''), s:buf)
  call assert_equal(1, s:start)
  call assert_equal(2, s:end)
  call assert_equal(0, s:added)
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list)

  " two disconnected changes
  call setline(1, ['one', 'two', 'three', 'four'])
  call listener_flush()
  call setline(1, 'one one')
  call setline(3, 'three three')
  call listener_flush()
  call assert_equal(bufnr(''), s:buf)
  call assert_equal(1, s:start)
  call assert_equal(4, s:end)
  call assert_equal(0, s:added)
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0},
	\ {'lnum': 3, 'end': 4, 'col': 1, 'added': 0}], s:list)

  " add and remove lines
  call setline(1, ['one', 'two', 'three', 'four', 'five', 'six'])
  call listener_flush()
  call append(2, 'two two')
  4del
  call append(5, 'five five')
  call listener_flush()
  call assert_equal(bufnr(''), s:buf)
  call assert_equal(3, s:start)
  call assert_equal(6, s:end)
  call assert_equal(1, s:added)
  call assert_equal([{'lnum': 3, 'end': 3, 'col': 1, 'added': 1},
	\ {'lnum': 4, 'end': 5, 'col': 1, 'added': -1},
	\ {'lnum': 6, 'end': 6, 'col': 1, 'added': 1}], s:list)

  " split a line then insert one, should get two disconnected change lists
  call setline(1, 'split here')
  call listener_flush()
  let s:list = []
  exe "normal 1ggwi\<CR>\<Esc>"
  1
  normal o
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 7, 'added': 1}], s:list)
  call listener_flush()
  call assert_equal([{'lnum': 2, 'end': 2, 'col': 1, 'added': 1}], s:list)

  call listener_remove(id)
  bwipe!

  " Invalid arguments
  call assert_fails('call listener_add([])', 'E921:')
  call assert_fails('call listener_add("s:StoreListArgs", [])', 'E730:')
  call assert_fails('call listener_flush([])', 'E730:')

  call assert_fails('eval ""->listener_add()', 'E119:')
endfunc

func s:StoreBufList(buf, start, end, added, list)
  let s:bufnr = a:buf
  let s:list = a:list
endfunc

func Test_listening_other_buf()
  new
  call setline(1, ['one', 'two'])
  let bufnr = bufnr('')
  normal ww
  let id = bufnr->listener_add(function('s:StoreBufList'))
  let s:list = []
  call setbufline(bufnr, 1, 'hello')
  redraw
  call assert_equal(bufnr, s:bufnr)
  call assert_equal([{'lnum': 1, 'end': 2, 'col': 1, 'added': 0}], s:list)

  call listener_remove(id)
  exe "buf " .. bufnr
  bwipe!
endfunc

func Test_listener_garbage_collect()
  func MyListener(x, bufnr, start, end, added, changes)
    " NOP
  endfunc

  new
  let id = listener_add(function('MyListener', [{}]), bufnr(''))
  call test_garbagecollect_now()
  " must not crash caused by invalid memory access
  normal ia
  call assert_true(v:true)

  call listener_remove(id)
  delfunc MyListener
  bwipe!
endfunc

" This verifies the fix for issue #4455
func Test_listener_caches_buffer_line()
  new
  inoremap <silent> <CR> <CR><Esc>O

  function EchoChanges(bufnr, start, end, added, changes)
    for l:change in a:changes
      let text = getbufline(a:bufnr, l:change.lnum, l:change.end-1+l:change.added)
    endfor
  endfunction
  let lid = listener_add("EchoChanges")
  set autoindent
  set cindent

  call setline(1, ["{", "\tif true {}", "}"])
  exe "normal /{}\nl"
  call feedkeys("i\r\e", 'xt')
  call assert_equal(["{", "\tif true {", "", "\t}", "}"], getline(1, 5))

  bwipe!
  delfunc EchoChanges
  call listener_remove(lid)
  iunmap <CR>
  set nocindent
endfunc

" Verify the fix for issue #4908
func Test_listener_undo_line_number()
  function DoIt()
    " NOP
  endfunction
  function EchoChanges(bufnr, start, end, added, changes)
    call DoIt()
  endfunction

  new
  let lid = listener_add("EchoChanges")
  call setline(1, ['a', 'b', 'c'])
  set undolevels&  " start new undo block
  call feedkeys("ggcG\<Esc>", 'xt')
  undo

  bwipe!
  delfunc DoIt
  delfunc EchoChanges
  call listener_remove(lid)
endfunc

func Test_listener_undo_delete_all()
  new
  call setline(1, [1, 2, 3, 4])
  let s:changes = []
  func s:ExtendList(bufnr, start, end, added, changes)
    call extend(s:changes, a:changes)
  endfunc
  let id = listener_add('s:ExtendList')

  set undolevels&  " start new undo block
  normal! ggdG
  undo
  call listener_flush()
  call assert_equal(2, s:changes->len())
  " delete removes four lines, empty line remains
  call assert_equal({'lnum': 1, 'end': 5, 'col': 1, 'added': -4}, s:changes[0])
  " undo replaces empty line and adds 3 lines
  call assert_equal({'lnum': 1, 'end': 2, 'col': 1, 'added': 3}, s:changes[1])

  call listener_remove(id)
  delfunc s:ExtendList
  unlet s:changes
  bwipe!
endfunc

func Test_listener_cleared_newbuf()
  func Listener(bufnr, start, end, added, changes)
    let g:gotCalled += 1
  endfunc
  new
  " check that listening works
  let g:gotCalled = 0
  let lid = listener_add("Listener")
  call feedkeys("axxx\<Esc>", 'xt')
  call listener_flush(bufnr())
  call assert_equal(1, g:gotCalled)
  %bwipe!
  let bufnr = bufnr()
  let b:testing = 123
  let lid = listener_add("Listener")
  enew!
  " check buffer is reused
  call assert_equal(bufnr, bufnr())
  call assert_false(exists('b:testing'))

  " check that listening stops when reusing the buffer
  let g:gotCalled = 0
  call feedkeys("axxx\<Esc>", 'xt')
  call listener_flush(bufnr())
  call assert_equal(0, g:gotCalled)
  unlet g:gotCalled

  bwipe!
  delfunc Listener
endfunc

func Test_col_after_deletion_moved_cur()
  func Listener(bufnr, start, end, added, changes)
    call assert_equal([#{lnum: 1, end: 2, added: 0, col: 2}], a:changes)
  endfunc
  new
  call setline(1, ['foo'])
  let lid = listener_add('Listener')
  call feedkeys("lD", 'xt')
  call listener_flush()
  bwipe!
  delfunc Listener
endfunc

func Test_remove_listener_in_callback()
  new
  let s:ID = listener_add('Listener')
  func Listener(...)
    call listener_remove(s:ID)
    let g:listener_called = 'yes'
  endfunc
  call setline(1, ['foo'])
  call feedkeys("lD", 'xt')
  call listener_flush()
  call assert_equal('yes', g:listener_called)

  bwipe!
  delfunc Listener
  unlet g:listener_called
endfunc

" When multiple listeners are registered, remove one listener and verify the
" other listener is still called
func Test_remove_one_listener_in_callback()
  new
  let g:listener1_called = 0
  let g:listener2_called = 0
  let s:ID1 = listener_add('Listener1')
  let s:ID2 = listener_add('Listener2')
  func Listener1(...)
    call listener_remove(s:ID1)
    let g:listener1_called += 1
  endfunc
  func Listener2(...)
    let g:listener2_called += 1
  endfunc
  call setline(1, ['foo'])
  call feedkeys("~", 'xt')
  call listener_flush()
  call feedkeys("~", 'xt')
  call listener_flush()
  call assert_equal(1, g:listener1_called)
  call assert_equal(2, g:listener2_called)

  call listener_remove(s:ID2)
  bwipe!
  delfunc Listener1
  delfunc Listener2
  unlet g:listener1_called
  unlet g:listener2_called
endfunc

func Test_no_change_for_empty_undo()
  new
  let text = ['some word here', 'second line']
  call setline(1, text)
  let g:entries = []
  func Listener(bufnr, start, end, added, changes)
    for change in a:changes
      call add(g:entries, [change.lnum, change.end, change.added])
    endfor
  endfunc
  let s:ID = listener_add('Listener')
  let @a = "one line\ntwo line\nthree line"
  set undolevels&  " start new undo block
  call feedkeys('fwviw"ap', 'xt')
  call listener_flush(bufnr())
  " first change deletes "word", second change inserts the register
  call assert_equal([[1, 2, 0], [1, 2, 2]], g:entries)
  let g:entries = []

  set undolevels&  " start new undo block
  undo
  call listener_flush(bufnr())
  call assert_equal([[1, 4, -2]], g:entries)
  call assert_equal(text, getline(1, 2))

  call listener_remove(s:ID)
  bwipe!
  unlet g:entries
  delfunc Listener
endfunc


" vim: shiftwidth=2 sts=2 expandtab