neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

buffer_updates_spec.lua (57586B)


      1 -- Test suite for testing interactions with API bindings
      2 local t = require('test.testutil')
      3 local n = require('test.functional.testnvim')()
      4 local Screen = require('test.functional.ui.screen')
      5 
      6 local command = n.command
      7 local api = n.api
      8 local fn = n.fn
      9 local clear = n.clear
     10 local eq = t.eq
     11 local fail = t.fail
     12 local exec_lua = n.exec_lua
     13 local feed = n.feed
     14 local expect_events = t.expect_events
     15 local write_file = t.write_file
     16 local dedent = t.dedent
     17 local matches = t.matches
     18 local pcall_err = t.pcall_err
     19 
     20 local origlines = {
     21  'original line 1',
     22  'original line 2',
     23  'original line 3',
     24  'original line 4',
     25  'original line 5',
     26  'original line 6',
     27  '    indented line',
     28 }
     29 
     30 before_each(function()
     31  clear()
     32  exec_lua(function()
     33    local events = {}
     34 
     35    function _G.test_register(bufnr, evname, id, changedtick, utf_sizes, preview)
     36      local function callback(...)
     37        table.insert(events, { id, ... })
     38        if _G.test_unreg == id then
     39          return true
     40        end
     41      end
     42      local opts = {
     43        [evname] = callback,
     44        on_detach = callback,
     45        on_reload = callback,
     46        utf_sizes = utf_sizes,
     47        preview = preview,
     48      }
     49      if changedtick then
     50        opts.on_changedtick = callback
     51      end
     52      vim.api.nvim_buf_attach(bufnr, false, opts)
     53    end
     54 
     55    function _G.get_events()
     56      local ret_events = events
     57      events = {}
     58      return ret_events
     59    end
     60  end)
     61 end)
     62 
     63 describe('lua: nvim_buf_attach on_lines', function()
     64  local function setup_eventcheck(verify, utf_sizes, lines)
     65    local lastsize
     66    api.nvim_buf_set_lines(0, 0, -1, true, lines)
     67    if verify then
     68      lastsize = api.nvim_buf_get_offset(0, api.nvim_buf_line_count(0))
     69    end
     70    exec_lua('return test_register(...)', 0, 'on_lines', 'test1', false, utf_sizes)
     71    local verify_name = 'test1'
     72 
     73    local function check_events(expected)
     74      local events = exec_lua('return get_events(...)')
     75      if utf_sizes then
     76        -- this test case uses ASCII only, so sizes should be the same.
     77        -- Unicode is tested below.
     78        for _, event in ipairs(expected) do
     79          event[9] = event[9] or event[8]
     80          event[10] = event[10] or event[9]
     81        end
     82      end
     83      expect_events(expected, events, 'line updates')
     84      if verify then
     85        for _, event in ipairs(events) do
     86          if event[1] == verify_name and event[2] == 'lines' then
     87            local startline, endline = event[5], event[7]
     88            local newrange = api.nvim_buf_get_offset(0, endline)
     89              - api.nvim_buf_get_offset(0, startline)
     90            local newsize = api.nvim_buf_get_offset(0, api.nvim_buf_line_count(0))
     91            local oldrange = newrange + lastsize - newsize
     92            eq(oldrange, event[8])
     93            lastsize = newsize
     94          end
     95        end
     96      end
     97    end
     98    return check_events, function(new)
     99      verify_name = new
    100    end
    101  end
    102 
    103  -- verifying the sizes with nvim_buf_get_offset is nice (checks we cannot
    104  -- assert the wrong thing), but masks errors with unflushed lines (as
    105  -- nvim_buf_get_offset forces a flush of the memline). To be safe run the
    106  -- test both ways.
    107  local function check(verify, utf_sizes)
    108    local check_events, verify_name = setup_eventcheck(verify, utf_sizes, origlines)
    109 
    110    local tick = api.nvim_buf_get_changedtick(0)
    111    command('set autoindent')
    112    command('normal! GyyggP')
    113    tick = tick + 1
    114    check_events { { 'test1', 'lines', 1, tick, 0, 0, 1, 0 } }
    115 
    116    api.nvim_buf_set_lines(0, 3, 5, true, { 'changed line' })
    117    tick = tick + 1
    118    check_events { { 'test1', 'lines', 1, tick, 3, 5, 4, 32 } }
    119 
    120    exec_lua('return test_register(...)', 0, 'on_lines', 'test2', true, utf_sizes)
    121    tick = tick + 1
    122    command('undo')
    123 
    124    -- plugins can opt in to receive changedtick events, or choose
    125    -- to only receive actual changes.
    126    check_events {
    127      { 'test1', 'lines', 1, tick, 3, 4, 5, 13 },
    128      { 'test2', 'lines', 1, tick, 3, 4, 5, 13 },
    129      { 'test2', 'changedtick', 1, tick + 1 },
    130    }
    131    tick = tick + 1
    132 
    133    tick = tick + 1
    134    command('redo')
    135    check_events {
    136      { 'test1', 'lines', 1, tick, 3, 5, 4, 32 },
    137      { 'test2', 'lines', 1, tick, 3, 5, 4, 32 },
    138      { 'test2', 'changedtick', 1, tick + 1 },
    139    }
    140    tick = tick + 1
    141 
    142    tick = tick + 1
    143    command('undo!')
    144    check_events {
    145      { 'test1', 'lines', 1, tick, 3, 4, 5, 13 },
    146      { 'test2', 'lines', 1, tick, 3, 4, 5, 13 },
    147      { 'test2', 'changedtick', 1, tick + 1 },
    148    }
    149    tick = tick + 1
    150 
    151    -- simulate next callback returning true
    152    exec_lua("test_unreg = 'test1'")
    153 
    154    api.nvim_buf_set_lines(0, 6, 7, true, { 'x1', 'x2', 'x3' })
    155    tick = tick + 1
    156 
    157    -- plugins can opt in to receive changedtick events, or choose
    158    -- to only receive actual changes.
    159    check_events {
    160      { 'test1', 'lines', 1, tick, 6, 7, 9, 16 },
    161      { 'test2', 'lines', 1, tick, 6, 7, 9, 16 },
    162    }
    163 
    164    verify_name 'test2'
    165 
    166    api.nvim_buf_set_lines(0, 1, 1, true, { 'added' })
    167    tick = tick + 1
    168    check_events { { 'test2', 'lines', 1, tick, 1, 1, 2, 0 } }
    169 
    170    feed('wix')
    171    tick = tick + 1
    172    check_events { { 'test2', 'lines', 1, tick, 4, 5, 5, 16 } }
    173 
    174    -- check hot path for multiple insert
    175    feed('yz')
    176    tick = tick + 1
    177    check_events { { 'test2', 'lines', 1, tick, 4, 5, 5, 17 } }
    178 
    179    feed('<bs>')
    180    tick = tick + 1
    181    check_events { { 'test2', 'lines', 1, tick, 4, 5, 5, 19 } }
    182 
    183    feed('<esc>Go')
    184    tick = tick + 1
    185    check_events { { 'test2', 'lines', 1, tick, 11, 11, 12, 0 } }
    186 
    187    feed('x')
    188    tick = tick + 1
    189    check_events { { 'test2', 'lines', 1, tick, 11, 12, 12, 5 } }
    190 
    191    command('bwipe!')
    192    check_events { { 'test2', 'detach', 1 } }
    193  end
    194 
    195  it('works', function()
    196    check(false)
    197  end)
    198 
    199  it('works with verify', function()
    200    check(true)
    201  end)
    202 
    203  it('works with utf_sizes and ASCII text', function()
    204    check(false, true)
    205  end)
    206 
    207  local function check_unicode(verify)
    208    local unicode_text = {
    209      'ascii text',
    210      'latin text åäö',
    211      'BMP text ɧ αλφά',
    212      'BMP text 汉语 ↥↧',
    213      'SMP 🤦 🦄🦃',
    214      'combining å بِيَّة',
    215    }
    216    local check_events, verify_name = setup_eventcheck(verify, true, unicode_text)
    217 
    218    local tick = api.nvim_buf_get_changedtick(0)
    219 
    220    feed('ggdd')
    221    tick = tick + 1
    222    check_events { { 'test1', 'lines', 1, tick, 0, 1, 0, 11, 11, 11 } }
    223 
    224    feed('A<bs>')
    225    tick = tick + 1
    226    check_events { { 'test1', 'lines', 1, tick, 0, 1, 1, 18, 15, 15 } }
    227 
    228    feed('<esc>jylp')
    229    tick = tick + 1
    230    check_events { { 'test1', 'lines', 1, tick, 1, 2, 2, 21, 16, 16 } }
    231 
    232    feed('+eea<cr>')
    233    tick = tick + 1
    234    check_events { { 'test1', 'lines', 1, tick, 2, 3, 4, 23, 15, 15 } }
    235 
    236    feed('<esc>jdw')
    237    tick = tick + 1
    238    -- non-BMP chars count as 2 UTF-2 codeunits
    239    check_events { { 'test1', 'lines', 1, tick, 4, 5, 5, 18, 9, 12 } }
    240 
    241    feed('+rx')
    242    tick = tick + 1
    243    -- count the individual codepoints of a composed character.
    244    check_events { { 'test1', 'lines', 1, tick, 5, 6, 6, 27, 20, 20 } }
    245 
    246    feed('kJ')
    247    tick = tick + 1
    248    -- verification fails with multiple line updates, sorry about that
    249    verify_name ''
    250    -- NB: this is inefficient (but not really wrong).
    251    check_events {
    252      { 'test1', 'lines', 1, tick, 4, 5, 5, 14, 5, 8 },
    253      { 'test1', 'lines', 1, tick + 1, 5, 6, 5, 27, 20, 20 },
    254    }
    255  end
    256 
    257  it('works with utf_sizes and unicode text', function()
    258    check_unicode(false)
    259  end)
    260 
    261  it('works with utf_sizes and unicode text with verify', function()
    262    check_unicode(true)
    263  end)
    264 
    265  it('has valid cursor position while shifting', function()
    266    api.nvim_buf_set_lines(0, 0, -1, true, { 'line1' })
    267    exec_lua(function()
    268      vim.api.nvim_buf_attach(0, false, {
    269        on_lines = function()
    270          vim.api.nvim_set_var('listener_cursor_line', vim.api.nvim_win_get_cursor(0)[1])
    271        end,
    272      })
    273    end)
    274    feed('>>')
    275    eq(1, api.nvim_get_var('listener_cursor_line'))
    276  end)
    277 
    278  it('has valid cursor position while deleting lines', function()
    279    api.nvim_buf_set_lines(0, 0, -1, true, { 'line_1', 'line_2', 'line_3', 'line_4' })
    280    api.nvim_win_set_cursor(0, { 2, 0 })
    281    eq(2, api.nvim_win_get_cursor(0)[1])
    282    api.nvim_buf_set_lines(0, 0, -1, true, { 'line_1', 'line_2', 'line_3' })
    283    eq(2, api.nvim_win_get_cursor(0)[1])
    284  end)
    285 
    286  it('#12718 lnume', function()
    287    api.nvim_buf_set_lines(0, 0, -1, true, { '1', '2', '3' })
    288    exec_lua(function()
    289      vim.api.nvim_buf_attach(0, false, {
    290        on_lines = function(...)
    291          vim.api.nvim_set_var('linesev', { ... })
    292        end,
    293      })
    294    end)
    295    feed('1G0')
    296    feed('y<C-v>2j')
    297    feed('G0')
    298    feed('p')
    299    -- Is the last arg old_byte_size correct? Doesn't matter for this PR
    300    eq({ 'lines', 1, 4, 2, 3, 5, 4 }, api.nvim_get_var('linesev'))
    301 
    302    feed('2G0')
    303    feed('p')
    304    eq({ 'lines', 1, 5, 1, 4, 4, 8 }, api.nvim_get_var('linesev'))
    305 
    306    feed('1G0')
    307    feed('P')
    308    eq({ 'lines', 1, 6, 0, 3, 3, 9 }, api.nvim_get_var('linesev'))
    309  end)
    310 
    311  it('nvim_buf_call() from callback does not cause wrong Normal mode CTRL-A #16729', function()
    312    exec_lua(function()
    313      vim.api.nvim_buf_attach(0, false, {
    314        on_lines = function()
    315          vim.api.nvim_buf_call(0, function() end)
    316        end,
    317      })
    318    end)
    319    feed('itest123<Esc><C-A>')
    320    eq('test124', api.nvim_get_current_line())
    321  end)
    322 
    323  it('setting extmark in on_lines callback works', function()
    324    local screen = Screen.new(40, 6)
    325 
    326    api.nvim_buf_set_lines(0, 0, -1, true, { 'aaa', 'bbb', 'ccc' })
    327    exec_lua(function()
    328      local ns = vim.api.nvim_create_namespace('')
    329      vim.api.nvim_buf_attach(0, false, {
    330        on_lines = function(_, _, _, row, _, end_row)
    331          vim.api.nvim_buf_clear_namespace(0, ns, row, end_row)
    332          for i = row, end_row - 1 do
    333            vim.api.nvim_buf_set_extmark(0, ns, i, 0, {
    334              virt_text = { { 'NEW' .. tostring(i), 'WarningMsg' } },
    335            })
    336          end
    337        end,
    338      })
    339    end)
    340 
    341    feed('o')
    342    screen:expect({
    343      grid = [[
    344        aaa                                     |
    345        ^ {19:NEW1}                                   |
    346        bbb                                     |
    347        ccc                                     |
    348        {1:~                                       }|
    349        {5:-- INSERT --}                            |
    350      ]],
    351    })
    352    feed('<CR>')
    353    screen:expect({
    354      grid = [[
    355        aaa                                     |
    356         {19:NEW1}                                   |
    357        ^ {19:NEW2}                                   |
    358        bbb                                     |
    359        ccc                                     |
    360        {5:-- INSERT --}                            |
    361      ]],
    362    })
    363  end)
    364 
    365  it('line lengths are correct when pressing TAB with folding #29119', function()
    366    api.nvim_buf_set_lines(0, 0, -1, true, { 'a', 'b' })
    367 
    368    exec_lua(function()
    369      _G.res = {}
    370      vim.o.foldmethod = 'indent'
    371      vim.o.softtabstop = -1
    372      vim.api.nvim_buf_attach(0, false, {
    373        on_lines = function(_, bufnr, _, row, _, end_row)
    374          local lines = vim.api.nvim_buf_get_lines(bufnr, row, end_row, true)
    375          table.insert(_G.res, lines)
    376        end,
    377      })
    378    end)
    379 
    380    feed('i<Tab>')
    381    eq({ '\ta' }, exec_lua('return _G.res[#_G.res]'))
    382  end)
    383 
    384  it('quickfix buffer send change', function()
    385    command('copen')
    386    exec_lua(function()
    387      vim.api.nvim_buf_attach(vim.api.nvim_get_current_buf(), false, {
    388        on_lines = function(...)
    389          vim.g.qf_on_lines = { ... }
    390        end,
    391        on_bytes = function(...)
    392          vim.g.qf_on_bytes = { ... }
    393        end,
    394      })
    395    end)
    396    command('caddexpr "foo"')
    397    eq({ 'bytes', 2, 2, 0, 0, 0, 0, 0, 0, 0, 6, 6 }, api.nvim_get_var('qf_on_bytes'))
    398    eq({ 'lines', 2, 3, 0, 1, 1, 1 }, api.nvim_get_var('qf_on_lines'))
    399 
    400    command('caddexpr "bar"')
    401    eq({ 'bytes', 2, 3, 0, 6, 6, 0, 0, 0, 1, 6, 6 }, api.nvim_get_var('qf_on_bytes'))
    402    eq({ 'lines', 2, 4, 1, 1, 2, 0 }, api.nvim_get_var('qf_on_lines'))
    403 
    404    command('caddexpr ["line1", "line2", "line3"]')
    405    eq({ 'bytes', 2, 4, 1, 6, 13, 0, 0, 0, 3, 8, 26 }, api.nvim_get_var('qf_on_bytes'))
    406    eq({ 'lines', 2, 5, 2, 2, 5, 0 }, api.nvim_get_var('qf_on_lines'))
    407 
    408    command('cexpr "replace"')
    409    eq({ 'bytes', 2, 5, 0, 0, 0, 4, 0, 40, 0, 10, 10 }, api.nvim_get_var('qf_on_bytes'))
    410    eq({ 'lines', 2, 6, 0, 5, 1, 42 }, api.nvim_get_var('qf_on_lines'))
    411  end)
    412 
    413  it('single-line visual block insert should not trigger extra on_lines #22009', function()
    414    exec_lua(function()
    415      _G.res = {}
    416      vim.api.nvim_buf_attach(0, false, {
    417        on_lines = function(_, bufnr, _, first, last, last_updated, _, _, _)
    418          _G.res = { bufnr, first, last, last_updated }
    419        end,
    420      })
    421    end)
    422    feed('<C-v>I <ESC>')
    423    eq({ api.nvim_get_current_buf(), 0, 1, 1 }, exec_lua('return _G.res'))
    424  end)
    425 
    426  it('prompt buffer', function()
    427    local check_events = setup_eventcheck(false, nil, {})
    428    api.nvim_set_option_value('buftype', 'prompt', {})
    429    feed('i')
    430    check_events {
    431      { 'test1', 'lines', 1, 4, 0, 1, 1, 1 },
    432    }
    433    fn.prompt_setprompt('', 'foo > ')
    434    check_events {
    435      { 'test1', 'lines', 1, 5, 0, 1, 1, 3 },
    436    }
    437    feed('hello')
    438    check_events {
    439      { 'test1', 'lines', 1, 6, 0, 1, 1, 7 },
    440    }
    441    fn.prompt_setprompt('', 'super-foo > ')
    442    check_events {
    443      { 'test1', 'lines', 1, 7, 0, 1, 1, 12 },
    444    }
    445    eq({ 'super-foo > hello' }, api.nvim_buf_get_lines(0, 0, -1, true))
    446    -- Do this in the same event.
    447    exec_lua(function()
    448      vim.fn.setpos("':", { 0, 1, 999, 0 })
    449      vim.fn.prompt_setprompt('', 'discard > ')
    450    end)
    451    check_events {
    452      { 'test1', 'lines', 1, 8, 0, 1, 1, 18 },
    453    }
    454    eq({ 'discard > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
    455    feed('hello<S-CR>there')
    456    check_events {
    457      { 'test1', 'lines', 1, 9, 0, 1, 1, 11 },
    458      { 'test1', 'lines', 1, 10, 0, 1, 2, 16 },
    459      { 'test1', 'lines', 1, 11, 1, 2, 2, 1 },
    460    }
    461    fn.prompt_setprompt('', 'foo > ')
    462    check_events {
    463      { 'test1', 'lines', 1, 12, 0, 1, 1, 16 },
    464    }
    465    eq({ 'foo > hello', 'there' }, api.nvim_buf_get_lines(0, 0, -1, true))
    466 
    467    -- init_prompt uses appended_lines_mark when appending to fix prompt.
    468    api.nvim_buf_set_lines(0, 0, -1, true, { 'hi' })
    469    eq({ 'hi', 'foo > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
    470    check_events {
    471      { 'test1', 'lines', 1, 13, 0, 2, 1, 18 },
    472      { 'test1', 'lines', 1, 14, 1, 1, 2, 0 },
    473    }
    474  end)
    475 end)
    476 
    477 describe('lua: nvim_buf_attach on_bytes', function()
    478  -- verifying the sizes with nvim_buf_get_offset is nice (checks we cannot
    479  -- assert the wrong thing), but masks errors with unflushed lines (as
    480  -- nvim_buf_get_offset forces a flush of the memline). To be safe run the
    481  -- test both ways.
    482  local function setup_eventcheck(verify, start_txt)
    483    if start_txt then
    484      api.nvim_buf_set_lines(0, 0, -1, true, start_txt)
    485    else
    486      start_txt = api.nvim_buf_get_lines(0, 0, -1, true)
    487    end
    488    local shadowbytes = table.concat(start_txt, '\n') .. '\n'
    489    -- TODO: while we are brewing the real strong coffee,
    490    -- verify should check buf_get_offset after every check_events
    491    if verify then
    492      local len = api.nvim_buf_get_offset(0, api.nvim_buf_line_count(0))
    493      eq(len == -1 and 1 or len, string.len(shadowbytes))
    494    end
    495    exec_lua('return test_register(...)', 0, 'on_bytes', 'test1', false, false, true)
    496    api.nvim_buf_get_changedtick(0)
    497 
    498    local verify_name = 'test1'
    499    local function check_events(expected)
    500      local events = exec_lua('return get_events(...)')
    501      expect_events(expected, events, 'byte updates')
    502 
    503      if not verify then
    504        return
    505      end
    506 
    507      for _, event in ipairs(events) do
    508        for _, elem in ipairs(event) do
    509          if type(elem) == 'number' and elem < 0 then
    510            fail(string.format('Received event has negative values'))
    511          end
    512        end
    513 
    514        if event[1] == verify_name and event[2] == 'bytes' then
    515          local _, _, _, _, _, _, start_byte, _, _, old_byte, _, _, new_byte = unpack(event)
    516          local before = string.sub(shadowbytes, 1, start_byte)
    517          -- no text in the tests will contain 0xff bytes (invalid UTF-8)
    518          -- so we can use it as marker for unknown bytes
    519          local unknown = string.rep('\255', new_byte)
    520          local after = string.sub(shadowbytes, start_byte + old_byte + 1)
    521          shadowbytes = before .. unknown .. after
    522        elseif event[1] == verify_name and event[2] == 'reload' then
    523          shadowbytes = table.concat(api.nvim_buf_get_lines(0, 0, -1, true), '\n') .. '\n'
    524        end
    525      end
    526 
    527      local text = api.nvim_buf_get_lines(0, 0, -1, true)
    528      local bytes = table.concat(text, '\n') .. '\n'
    529 
    530      eq(
    531        string.len(bytes),
    532        string.len(shadowbytes),
    533        '\non_bytes: total bytecount of buffer is wrong'
    534      )
    535      for i = 1, string.len(shadowbytes) do
    536        local shadowbyte = string.sub(shadowbytes, i, i)
    537        if shadowbyte ~= '\255' then
    538          eq(string.sub(bytes, i, i), shadowbyte, i)
    539        end
    540      end
    541    end
    542 
    543    return check_events
    544  end
    545 
    546  -- Yes, we can do both
    547  local function do_both(verify)
    548    it('single and multiple join', function()
    549      local check_events = setup_eventcheck(verify, origlines)
    550      feed 'ggJ'
    551      check_events {
    552        { 'test1', 'bytes', 1, 3, 0, 15, 15, 1, 0, 1, 0, 1, 1 },
    553      }
    554 
    555      feed '3J'
    556      check_events {
    557        { 'test1', 'bytes', 1, 5, 0, 31, 31, 1, 0, 1, 0, 1, 1 },
    558        { 'test1', 'bytes', 1, 5, 0, 47, 47, 1, 0, 1, 0, 1, 1 },
    559      }
    560    end)
    561 
    562    it('opening lines', function()
    563      local check_events = setup_eventcheck(verify, origlines)
    564      api.nvim_set_option_value('autoindent', false, {})
    565      feed 'Go'
    566      check_events {
    567        { 'test1', 'bytes', 1, 3, 7, 0, 114, 0, 0, 0, 1, 0, 1 },
    568      }
    569      feed '<cr>'
    570      check_events {
    571        { 'test1', 'bytes', 1, 4, 7, 0, 114, 0, 0, 0, 1, 0, 1 },
    572      }
    573    end)
    574 
    575    it('opening lines with autoindent', function()
    576      local check_events = setup_eventcheck(verify, origlines)
    577      api.nvim_set_option_value('autoindent', true, {})
    578      feed 'Go'
    579      check_events {
    580        { 'test1', 'bytes', 1, 3, 7, 0, 114, 0, 0, 0, 1, 0, 5 },
    581      }
    582      feed '<cr>'
    583      check_events {
    584        { 'test1', 'bytes', 1, 4, 7, 0, 114, 0, 4, 4, 0, 0, 0 },
    585        { 'test1', 'bytes', 1, 4, 7, 0, 114, 0, 0, 0, 1, 4, 5 },
    586      }
    587    end)
    588 
    589    it('setline(num, line)', function()
    590      local check_events = setup_eventcheck(verify, origlines)
    591      fn.setline(2, 'babla')
    592      check_events {
    593        { 'test1', 'bytes', 1, 3, 1, 0, 16, 0, 15, 15, 0, 5, 5 },
    594      }
    595 
    596      fn.setline(2, { 'foo', 'bar' })
    597      check_events {
    598        { 'test1', 'bytes', 1, 4, 1, 0, 16, 0, 5, 5, 0, 3, 3 },
    599        { 'test1', 'bytes', 1, 5, 2, 0, 20, 0, 15, 15, 0, 3, 3 },
    600      }
    601 
    602      local buf_len = api.nvim_buf_line_count(0)
    603      fn.setline(buf_len + 1, 'baz')
    604      check_events {
    605        { 'test1', 'bytes', 1, 6, 7, 0, 90, 0, 0, 0, 1, 0, 4 },
    606      }
    607    end)
    608 
    609    it('continuing comments with fo=or', function()
    610      local check_events = setup_eventcheck(verify, { '// Comment' })
    611      api.nvim_set_option_value('formatoptions', 'ro', {})
    612      api.nvim_set_option_value('filetype', 'c', {})
    613      feed 'A<CR>'
    614      check_events {
    615        { 'test1', 'bytes', 1, 3, 0, 10, 10, 0, 0, 0, 1, 3, 4 },
    616      }
    617 
    618      feed '<ESC>'
    619      check_events {
    620        { 'test1', 'bytes', 1, 4, 1, 2, 13, 0, 1, 1, 0, 0, 0 },
    621      }
    622 
    623      feed 'ggo' -- goto first line to continue testing
    624      check_events {
    625        { 'test1', 'bytes', 1, 5, 1, 0, 11, 0, 0, 0, 1, 0, 4 },
    626      }
    627 
    628      feed '<CR>'
    629      check_events {
    630        { 'test1', 'bytes', 1, 6, 1, 2, 13, 0, 1, 1, 0, 0, 0 },
    631        { 'test1', 'bytes', 1, 6, 1, 2, 13, 0, 0, 0, 1, 3, 4 },
    632      }
    633    end)
    634 
    635    it('editing empty buffers', function()
    636      local check_events = setup_eventcheck(verify, {})
    637 
    638      feed 'ia'
    639      check_events {
    640        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 1 },
    641      }
    642    end)
    643 
    644    it('deleting lines', function()
    645      local check_events = setup_eventcheck(verify, origlines)
    646 
    647      feed('dd')
    648 
    649      check_events {
    650        { 'test1', 'bytes', 1, 3, 0, 0, 0, 1, 0, 16, 0, 0, 0 },
    651      }
    652 
    653      feed('d2j')
    654 
    655      check_events {
    656        { 'test1', 'bytes', 1, 4, 0, 0, 0, 3, 0, 48, 0, 0, 0 },
    657      }
    658 
    659      feed('ld<c-v>2j')
    660 
    661      check_events {
    662        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
    663        { 'test1', 'bytes', 1, 5, 1, 1, 16, 0, 1, 1, 0, 0, 0 },
    664        { 'test1', 'bytes', 1, 5, 2, 1, 31, 0, 1, 1, 0, 0, 0 },
    665      }
    666 
    667      feed('vjwd')
    668 
    669      check_events {
    670        { 'test1', 'bytes', 1, 10, 0, 1, 1, 1, 9, 23, 0, 0, 0 },
    671      }
    672    end)
    673 
    674    it('changing lines', function()
    675      local check_events = setup_eventcheck(verify, origlines)
    676 
    677      feed 'cc'
    678      check_events {
    679        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 15, 15, 0, 0, 0 },
    680      }
    681 
    682      feed '<ESC>'
    683      check_events {}
    684 
    685      feed 'c3j'
    686      check_events {
    687        { 'test1', 'bytes', 1, 4, 1, 0, 1, 3, 0, 48, 0, 0, 0 },
    688      }
    689    end)
    690 
    691    it('visual charwise paste', function()
    692      local check_events = setup_eventcheck(verify, { '1234567890' })
    693      fn.setreg('a', '___')
    694 
    695      feed '1G1|vll'
    696      check_events {}
    697 
    698      feed '"ap'
    699      check_events {
    700        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 3, 3, 0, 0, 0 },
    701        { 'test1', 'bytes', 1, 5, 0, 0, 0, 0, 0, 0, 0, 3, 3 },
    702      }
    703    end)
    704 
    705    it('blockwise paste', function()
    706      local check_events = setup_eventcheck(verify, { '1', '2', '3' })
    707      feed('1G0')
    708      feed('y<C-v>2j')
    709      feed('G0')
    710      feed('p')
    711      check_events {
    712        { 'test1', 'bytes', 1, 3, 2, 1, 5, 0, 0, 0, 0, 1, 1 },
    713        { 'test1', 'bytes', 1, 3, 3, 0, 7, 0, 0, 0, 0, 3, 3 },
    714        { 'test1', 'bytes', 1, 3, 4, 0, 10, 0, 0, 0, 0, 3, 3 },
    715      }
    716 
    717      feed('2G0')
    718      feed('p')
    719      check_events {
    720        { 'test1', 'bytes', 1, 4, 1, 1, 3, 0, 0, 0, 0, 1, 1 },
    721        { 'test1', 'bytes', 1, 4, 2, 1, 6, 0, 0, 0, 0, 1, 1 },
    722        { 'test1', 'bytes', 1, 4, 3, 1, 10, 0, 0, 0, 0, 1, 1 },
    723      }
    724 
    725      feed('1G0')
    726      feed('P')
    727      check_events {
    728        { 'test1', 'bytes', 1, 5, 0, 0, 0, 0, 0, 0, 0, 1, 1 },
    729        { 'test1', 'bytes', 1, 5, 1, 0, 3, 0, 0, 0, 0, 1, 1 },
    730        { 'test1', 'bytes', 1, 5, 2, 0, 7, 0, 0, 0, 0, 1, 1 },
    731      }
    732    end)
    733 
    734    it('linewise paste', function()
    735      local check_events = setup_eventcheck(verify, origlines)
    736 
    737      feed 'yyp'
    738      check_events {
    739        { 'test1', 'bytes', 1, 3, 1, 0, 16, 0, 0, 0, 1, 0, 16 },
    740      }
    741 
    742      feed 'Gyyp'
    743      check_events {
    744        { 'test1', 'bytes', 1, 4, 8, 0, 130, 0, 0, 0, 1, 0, 18 },
    745      }
    746    end)
    747 
    748    it('inccomand=nosplit and substitute', function()
    749      local check_events = setup_eventcheck(verify, { 'abcde', '12345' })
    750      api.nvim_set_option_value('inccommand', 'nosplit', {})
    751 
    752      -- linewise substitute
    753      feed(':%s/bcd/')
    754      check_events {
    755        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 3, 3, 0, 0, 0 },
    756        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 0, 0, 0, 3, 3 },
    757      }
    758 
    759      feed('a')
    760      check_events {
    761        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 3, 3, 0, 1, 1 },
    762        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 1, 1, 0, 3, 3 },
    763      }
    764 
    765      feed('<esc>')
    766 
    767      -- splitting lines
    768      feed([[:%s/abc/\r]])
    769      check_events {
    770        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 3, 3, 1, 0, 1 },
    771        { 'test1', 'bytes', 1, 6, 0, 0, 0, 1, 0, 1, 0, 3, 3 },
    772      }
    773 
    774      feed('<esc>')
    775      -- multi-line regex
    776      feed([[:%s/de\n123/a]])
    777 
    778      check_events {
    779        { 'test1', 'bytes', 1, 3, 0, 3, 3, 1, 3, 6, 0, 1, 1 },
    780        { 'test1', 'bytes', 1, 6, 0, 3, 3, 0, 1, 1, 1, 3, 6 },
    781      }
    782 
    783      feed('<esc>')
    784      -- replacing with unicode
    785      feed(':%s/b/→')
    786 
    787      check_events {
    788        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 1, 1, 0, 3, 3 },
    789        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 3, 3, 0, 1, 1 },
    790      }
    791 
    792      feed('<esc>')
    793      -- replacing with expression register
    794      feed([[:%s/b/\=5+5]])
    795      check_events {
    796        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 1, 1, 0, 2, 2 },
    797        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 2, 2, 0, 1, 1 },
    798      }
    799 
    800      feed('<esc>')
    801      -- replacing with backslash
    802      feed([[:%s/b/\\]])
    803      check_events {
    804        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 1, 1, 0, 1, 1 },
    805        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 1, 1, 0, 1, 1 },
    806      }
    807 
    808      feed('<esc>')
    809      -- replacing with backslash from expression register
    810      feed([[:%s/b/\='\']])
    811      check_events {
    812        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 1, 1, 0, 1, 1 },
    813        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 1, 1, 0, 1, 1 },
    814      }
    815 
    816      feed('<esc>')
    817      -- replacing with backslash followed by another character
    818      feed([[:%s/b/\\!]])
    819      check_events {
    820        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 1, 1, 0, 2, 2 },
    821        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 2, 2, 0, 1, 1 },
    822      }
    823 
    824      feed('<esc>')
    825      -- replacing with backslash followed by another character from expression register
    826      feed([[:%s/b/\='\!']])
    827      check_events {
    828        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 1, 1, 0, 2, 2 },
    829        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 2, 2, 0, 1, 1 },
    830      }
    831    end)
    832 
    833    it('nvim_buf_set_text insert', function()
    834      local check_events = setup_eventcheck(verify, { 'bastext' })
    835      api.nvim_buf_set_text(0, 0, 3, 0, 3, { 'fiol', 'kontra' })
    836      check_events {
    837        { 'test1', 'bytes', 1, 3, 0, 3, 3, 0, 0, 0, 1, 6, 11 },
    838      }
    839 
    840      api.nvim_buf_set_text(0, 1, 6, 1, 6, { 'punkt', 'syntgitarr', 'övnings' })
    841      check_events {
    842        { 'test1', 'bytes', 1, 4, 1, 6, 14, 0, 0, 0, 2, 8, 25 },
    843      }
    844 
    845      eq(
    846        { 'basfiol', 'kontrapunkt', 'syntgitarr', 'övningstext' },
    847        api.nvim_buf_get_lines(0, 0, -1, true)
    848      )
    849    end)
    850 
    851    it('nvim_buf_set_text replace', function()
    852      local check_events = setup_eventcheck(verify, origlines)
    853 
    854      api.nvim_buf_set_text(0, 2, 3, 2, 8, { 'very text' })
    855      check_events {
    856        { 'test1', 'bytes', 1, 3, 2, 3, 35, 0, 5, 5, 0, 9, 9 },
    857      }
    858 
    859      api.nvim_buf_set_text(0, 3, 5, 3, 7, { ' splitty', 'line ' })
    860      check_events {
    861        { 'test1', 'bytes', 1, 4, 3, 5, 57, 0, 2, 2, 1, 5, 14 },
    862      }
    863 
    864      api.nvim_buf_set_text(0, 0, 8, 1, 2, { 'JOINY' })
    865      check_events {
    866        { 'test1', 'bytes', 1, 5, 0, 8, 8, 1, 2, 10, 0, 5, 5 },
    867      }
    868 
    869      api.nvim_buf_set_text(0, 4, 0, 6, 0, { 'was 5,6', '' })
    870      check_events {
    871        { 'test1', 'bytes', 1, 6, 4, 0, 75, 2, 0, 32, 1, 0, 8 },
    872      }
    873 
    874      eq({
    875        'originalJOINYiginal line 2',
    876        'orivery text line 3',
    877        'origi splitty',
    878        'line l line 4',
    879        'was 5,6',
    880        '    indented line',
    881      }, api.nvim_buf_get_lines(0, 0, -1, true))
    882    end)
    883 
    884    it('nvim_buf_set_text delete', function()
    885      local check_events = setup_eventcheck(verify, origlines)
    886 
    887      -- really {""} but accepts {} as a shorthand
    888      api.nvim_buf_set_text(0, 0, 0, 1, 0, {})
    889      check_events {
    890        { 'test1', 'bytes', 1, 3, 0, 0, 0, 1, 0, 16, 0, 0, 0 },
    891      }
    892 
    893      -- TODO(bfredl): this works but is not as convenient as set_lines
    894      api.nvim_buf_set_text(0, 4, 15, 5, 17, { '' })
    895      check_events {
    896        { 'test1', 'bytes', 1, 4, 4, 15, 79, 1, 17, 18, 0, 0, 0 },
    897      }
    898      eq({
    899        'original line 2',
    900        'original line 3',
    901        'original line 4',
    902        'original line 5',
    903        'original line 6',
    904      }, api.nvim_buf_get_lines(0, 0, -1, true))
    905    end)
    906 
    907    it('checktime autoread', function()
    908      write_file(
    909        'Xtest-reload',
    910        dedent [[
    911        old line 1
    912        old line 2]]
    913      )
    914      local atime = os.time() - 10
    915      vim.uv.fs_utime('Xtest-reload', atime, atime)
    916      command 'e Xtest-reload'
    917      command 'set autoread'
    918 
    919      local check_events = setup_eventcheck(verify, nil)
    920 
    921      write_file(
    922        'Xtest-reload',
    923        dedent [[
    924        new line 1
    925        new line 2
    926        new line 3]]
    927      )
    928 
    929      command 'checktime'
    930      check_events {
    931        { 'test1', 'reload', 1 },
    932      }
    933 
    934      feed 'ggJ'
    935      check_events {
    936        { 'test1', 'bytes', 1, 5, 0, 10, 10, 1, 0, 1, 0, 1, 1 },
    937      }
    938 
    939      eq({ 'new line 1 new line 2', 'new line 3' }, api.nvim_buf_get_lines(0, 0, -1, true))
    940 
    941      -- check we can undo and redo a reload event.
    942      feed 'u'
    943      check_events {
    944        { 'test1', 'bytes', 1, 8, 0, 10, 10, 0, 1, 1, 1, 0, 1 },
    945      }
    946 
    947      feed 'u'
    948      check_events {
    949        { 'test1', 'reload', 1 },
    950      }
    951 
    952      feed '<c-r>'
    953      check_events {
    954        { 'test1', 'reload', 1 },
    955      }
    956 
    957      feed '<c-r>'
    958      check_events {
    959        { 'test1', 'bytes', 1, 14, 0, 10, 10, 1, 0, 1, 0, 1, 1 },
    960      }
    961    end)
    962 
    963    it('tab with noexpandtab and softtabstop', function()
    964      command('set noet')
    965      command('set ts=4')
    966      command('set sw=2')
    967      command('set sts=4')
    968 
    969      local check_events = setup_eventcheck(verify, { 'asdfasdf' })
    970 
    971      feed('gg0i<tab>')
    972 
    973      check_events {
    974        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 0, 0, 0, 1, 1 },
    975        { 'test1', 'bytes', 1, 4, 0, 1, 1, 0, 0, 0, 0, 1, 1 },
    976      }
    977      feed('<tab>')
    978 
    979      -- when spaces are merged into a tabstop
    980      check_events {
    981        { 'test1', 'bytes', 1, 5, 0, 2, 2, 0, 0, 0, 0, 1, 1 },
    982        { 'test1', 'bytes', 1, 6, 0, 3, 3, 0, 0, 0, 0, 1, 1 },
    983        { 'test1', 'bytes', 1, 7, 0, 0, 0, 0, 4, 4, 0, 1, 1 },
    984      }
    985 
    986      feed('<esc>u')
    987      check_events {
    988        { 'test1', 'bytes', 1, 9, 0, 0, 0, 0, 1, 1, 0, 4, 4 },
    989        { 'test1', 'bytes', 1, 9, 0, 0, 0, 0, 4, 4, 0, 0, 0 },
    990      }
    991 
    992      -- in REPLACE mode
    993      feed('R<tab><tab>')
    994      check_events {
    995        { 'test1', 'bytes', 1, 10, 0, 0, 0, 0, 1, 1, 0, 1, 1 },
    996        { 'test1', 'bytes', 1, 11, 0, 1, 1, 0, 0, 0, 0, 1, 1 },
    997        { 'test1', 'bytes', 1, 12, 0, 2, 2, 0, 1, 1, 0, 1, 1 },
    998        { 'test1', 'bytes', 1, 13, 0, 3, 3, 0, 0, 0, 0, 1, 1 },
    999        { 'test1', 'bytes', 1, 14, 0, 0, 0, 0, 4, 4, 0, 1, 1 },
   1000      }
   1001      feed('<esc>u')
   1002      check_events {
   1003        { 'test1', 'bytes', 1, 16, 0, 0, 0, 0, 1, 1, 0, 4, 4 },
   1004        { 'test1', 'bytes', 1, 16, 0, 2, 2, 0, 2, 2, 0, 1, 1 },
   1005        { 'test1', 'bytes', 1, 16, 0, 0, 0, 0, 2, 2, 0, 1, 1 },
   1006      }
   1007 
   1008      -- in VISUALREPLACE mode
   1009      feed('gR<tab><tab>')
   1010      check_events {
   1011        { 'test1', 'bytes', 1, 17, 0, 0, 0, 0, 1, 1, 0, 1, 1 },
   1012        { 'test1', 'bytes', 1, 18, 0, 1, 1, 0, 1, 1, 0, 1, 1 },
   1013        { 'test1', 'bytes', 1, 19, 0, 2, 2, 0, 1, 1, 0, 1, 1 },
   1014        { 'test1', 'bytes', 1, 20, 0, 3, 3, 0, 1, 1, 0, 1, 1 },
   1015        { 'test1', 'bytes', 1, 21, 0, 3, 3, 0, 1, 1, 0, 0, 0 },
   1016        { 'test1', 'bytes', 1, 22, 0, 3, 3, 0, 0, 0, 0, 1, 1 },
   1017        { 'test1', 'bytes', 1, 24, 0, 2, 2, 0, 1, 1, 0, 0, 0 },
   1018        { 'test1', 'bytes', 1, 25, 0, 2, 2, 0, 0, 0, 0, 1, 1 },
   1019        { 'test1', 'bytes', 1, 27, 0, 1, 1, 0, 1, 1, 0, 0, 0 },
   1020        { 'test1', 'bytes', 1, 28, 0, 1, 1, 0, 0, 0, 0, 1, 1 },
   1021        { 'test1', 'bytes', 1, 30, 0, 0, 0, 0, 1, 1, 0, 0, 0 },
   1022        { 'test1', 'bytes', 1, 31, 0, 0, 0, 0, 0, 0, 0, 1, 1 },
   1023        { 'test1', 'bytes', 1, 33, 0, 0, 0, 0, 4, 4, 0, 1, 1 },
   1024      }
   1025 
   1026      -- inserting tab after other tabs
   1027      command('set sw=4')
   1028      feed('<esc>0a<tab>')
   1029      check_events {
   1030        { 'test1', 'bytes', 1, 34, 0, 1, 1, 0, 0, 0, 0, 1, 1 },
   1031        { 'test1', 'bytes', 1, 35, 0, 2, 2, 0, 0, 0, 0, 1, 1 },
   1032        { 'test1', 'bytes', 1, 36, 0, 3, 3, 0, 0, 0, 0, 1, 1 },
   1033        { 'test1', 'bytes', 1, 37, 0, 4, 4, 0, 0, 0, 0, 1, 1 },
   1034        { 'test1', 'bytes', 1, 38, 0, 1, 1, 0, 4, 4, 0, 1, 1 },
   1035      }
   1036    end)
   1037 
   1038    it('retab', function()
   1039      command('set noet')
   1040      command('set ts=4')
   1041 
   1042      local check_events = setup_eventcheck(verify, { '			asdf' })
   1043      command('retab 8')
   1044 
   1045      check_events {
   1046        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 7, 7, 0, 9, 9 },
   1047      }
   1048    end)
   1049 
   1050    it('sends events when undoing with undofile', function()
   1051      write_file(
   1052        'Xtest-undofile',
   1053        dedent([[
   1054      12345
   1055      hello world
   1056      ]])
   1057      )
   1058 
   1059      command('e! Xtest-undofile')
   1060      command('set undodir=. | set undofile')
   1061 
   1062      local ns = n.request('nvim_create_namespace', 'ns1')
   1063      api.nvim_buf_set_extmark(0, ns, 0, 0, {})
   1064 
   1065      eq({ '12345', 'hello world' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1066 
   1067      -- splice
   1068      feed('gg0d2l')
   1069 
   1070      eq({ '345', 'hello world' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1071 
   1072      -- move
   1073      command('.m+1')
   1074 
   1075      eq({ 'hello world', '345' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1076 
   1077      -- reload undofile and undo changes
   1078      command('w')
   1079      command('set noundofile')
   1080      command('bw!')
   1081      command('e! Xtest-undofile')
   1082 
   1083      command('set undofile')
   1084 
   1085      local check_events = setup_eventcheck(verify, nil)
   1086 
   1087      feed('u')
   1088      eq({ '345', 'hello world' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1089 
   1090      check_events {
   1091        { 'test1', 'bytes', 2, 6, 1, 0, 12, 1, 0, 4, 0, 0, 0 },
   1092        { 'test1', 'bytes', 2, 6, 0, 0, 0, 0, 0, 0, 1, 0, 4 },
   1093      }
   1094 
   1095      feed('u')
   1096      eq({ '12345', 'hello world' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1097 
   1098      check_events {
   1099        { 'test1', 'bytes', 2, 8, 0, 0, 0, 0, 0, 0, 0, 2, 2 },
   1100      }
   1101      command('bw!')
   1102    end)
   1103 
   1104    it('blockwise paste with uneven line lengths', function()
   1105      local check_events = setup_eventcheck(verify, { 'aaaa', 'aaa', 'aaa' })
   1106 
   1107      -- eq({}, api.nvim_buf_get_lines(0, 0, -1, true))
   1108      feed('gg0<c-v>jj$d')
   1109 
   1110      check_events {
   1111        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 4, 4, 0, 0, 0 },
   1112        { 'test1', 'bytes', 1, 3, 1, 0, 1, 0, 3, 3, 0, 0, 0 },
   1113        { 'test1', 'bytes', 1, 3, 2, 0, 2, 0, 3, 3, 0, 0, 0 },
   1114      }
   1115 
   1116      feed('p')
   1117      check_events {
   1118        { 'test1', 'bytes', 1, 4, 0, 0, 0, 0, 0, 0, 0, 4, 4 },
   1119        { 'test1', 'bytes', 1, 4, 1, 0, 5, 0, 0, 0, 0, 3, 3 },
   1120        { 'test1', 'bytes', 1, 4, 2, 0, 9, 0, 0, 0, 0, 3, 3 },
   1121      }
   1122    end)
   1123 
   1124    it(':luado', function()
   1125      local check_events = setup_eventcheck(verify, { 'abc', '12345' })
   1126 
   1127      command(".luado return 'a'")
   1128 
   1129      check_events {
   1130        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 3, 3, 0, 1, 1 },
   1131      }
   1132 
   1133      command('luado return 10')
   1134 
   1135      check_events {
   1136        { 'test1', 'bytes', 1, 4, 0, 0, 0, 0, 1, 1, 0, 2, 2 },
   1137        { 'test1', 'bytes', 1, 5, 1, 0, 3, 0, 5, 5, 0, 2, 2 },
   1138      }
   1139    end)
   1140 
   1141    it('flushes deleted bytes on move', function()
   1142      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC', 'DDD' })
   1143 
   1144      feed(':.move+1<cr>')
   1145 
   1146      check_events {
   1147        { 'test1', 'bytes', 1, 5, 0, 0, 0, 1, 0, 4, 0, 0, 0 },
   1148        { 'test1', 'bytes', 1, 5, 1, 0, 4, 0, 0, 0, 1, 0, 4 },
   1149      }
   1150 
   1151      feed('jd2j')
   1152 
   1153      check_events {
   1154        { 'test1', 'bytes', 1, 6, 2, 0, 8, 2, 0, 8, 0, 0, 0 },
   1155      }
   1156    end)
   1157 
   1158    it('virtual edit', function()
   1159      local check_events = setup_eventcheck(verify, { '', '	' })
   1160 
   1161      api.nvim_set_option_value('virtualedit', 'all', {})
   1162 
   1163      feed [[<Right><Right>iab<ESC>]]
   1164 
   1165      check_events {
   1166        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2 },
   1167        { 'test1', 'bytes', 1, 4, 0, 2, 2, 0, 0, 0, 0, 2, 2 },
   1168      }
   1169 
   1170      feed [[j<Right><Right>iab<ESC>]]
   1171 
   1172      check_events {
   1173        { 'test1', 'bytes', 1, 5, 1, 0, 5, 0, 1, 1, 0, 8, 8 },
   1174        { 'test1', 'bytes', 1, 6, 1, 5, 10, 0, 0, 0, 0, 2, 2 },
   1175      }
   1176    end)
   1177 
   1178    it('block visual paste', function()
   1179      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC', 'DDD', 'EEE', 'FFF' })
   1180      fn.setreg('a', '___')
   1181      feed([[gg0l<c-v>3jl"ap]])
   1182 
   1183      check_events {
   1184        { 'test1', 'bytes', 1, 3, 0, 1, 1, 0, 2, 2, 0, 0, 0 },
   1185        { 'test1', 'bytes', 1, 3, 1, 1, 3, 0, 2, 2, 0, 0, 0 },
   1186        { 'test1', 'bytes', 1, 3, 2, 1, 5, 0, 2, 2, 0, 0, 0 },
   1187        { 'test1', 'bytes', 1, 3, 3, 1, 7, 0, 2, 2, 0, 0, 0 },
   1188        { 'test1', 'bytes', 1, 5, 0, 1, 1, 0, 0, 0, 0, 3, 3 },
   1189        { 'test1', 'bytes', 1, 6, 1, 1, 6, 0, 0, 0, 0, 3, 3 },
   1190        { 'test1', 'bytes', 1, 7, 2, 1, 11, 0, 0, 0, 0, 3, 3 },
   1191        { 'test1', 'bytes', 1, 8, 3, 1, 16, 0, 0, 0, 0, 3, 3 },
   1192      }
   1193    end)
   1194 
   1195    it('visual paste', function()
   1196      local check_events = setup_eventcheck(verify, { 'aaa {', 'b', '}' })
   1197      -- Setting up
   1198      feed [[jdd]]
   1199      check_events {
   1200        { 'test1', 'bytes', 1, 3, 1, 0, 6, 1, 0, 2, 0, 0, 0 },
   1201      }
   1202 
   1203      -- Actually testing
   1204      feed [[v%p]]
   1205      check_events {
   1206        { 'test1', 'bytes', 1, 8, 0, 4, 4, 1, 1, 3, 0, 0, 0 },
   1207        { 'test1', 'bytes', 1, 8, 0, 4, 4, 0, 0, 0, 2, 0, 3 },
   1208      }
   1209    end)
   1210 
   1211    it('visual paste 2: splitting an empty line', function()
   1212      local check_events = setup_eventcheck(verify, { 'abc', '{', 'def', '}' })
   1213      feed('ggyyjjvi{p')
   1214      check_events {
   1215        { 'test1', 'bytes', 1, 6, 2, 0, 6, 1, 0, 4, 0, 0, 0 },
   1216        { 'test1', 'bytes', 1, 6, 2, 0, 6, 0, 0, 0, 2, 0, 5 },
   1217      }
   1218    end)
   1219 
   1220    it('nvim_buf_set_lines', function()
   1221      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB' })
   1222 
   1223      -- delete
   1224      api.nvim_buf_set_lines(0, 0, 1, true, {})
   1225 
   1226      check_events {
   1227        { 'test1', 'bytes', 1, 3, 0, 0, 0, 1, 0, 4, 0, 0, 0 },
   1228      }
   1229 
   1230      -- add
   1231      api.nvim_buf_set_lines(0, 0, 0, true, { 'asdf' })
   1232      check_events {
   1233        { 'test1', 'bytes', 1, 4, 0, 0, 0, 0, 0, 0, 1, 0, 5 },
   1234      }
   1235 
   1236      -- replace
   1237      api.nvim_buf_set_lines(0, 0, 1, true, { 'asdf', 'fdsa' })
   1238      check_events {
   1239        { 'test1', 'bytes', 1, 5, 0, 0, 0, 1, 0, 5, 2, 0, 10 },
   1240      }
   1241    end)
   1242 
   1243    it('flushes delbytes on substitute', function()
   1244      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC' })
   1245 
   1246      feed('gg0')
   1247      command('s/AAA/GGG/')
   1248 
   1249      check_events {
   1250        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 3, 3, 0, 3, 3 },
   1251      }
   1252 
   1253      -- check that byte updates for :delete (which uses curbuf->deleted_bytes2)
   1254      -- are correct
   1255      command('delete')
   1256      check_events {
   1257        { 'test1', 'bytes', 1, 4, 0, 0, 0, 1, 0, 4, 0, 0, 0 },
   1258      }
   1259    end)
   1260 
   1261    it('on_bytes sees modified buffer after substitute', function()
   1262      api.nvim_buf_set_lines(0, 0, -1, true, { 'Hello' })
   1263 
   1264      local buffer_lines = exec_lua(function()
   1265        local lines
   1266        vim.api.nvim_buf_attach(0, false, {
   1267          on_bytes = function()
   1268            lines = vim.api.nvim_buf_get_lines(0, 0, -1, true)
   1269          end,
   1270        })
   1271        vim.cmd('s/llo/y/')
   1272        return lines
   1273      end)
   1274 
   1275      -- Make sure on_bytes is called after the buffer is modified.
   1276      eq({ 'Hey' }, buffer_lines)
   1277    end)
   1278 
   1279    it('on_bytes called multiple times for multiple substitutions on same line', function()
   1280      api.nvim_buf_set_lines(0, 0, -1, true, { 'Hello Hello' })
   1281 
   1282      local call_count, args = exec_lua(function()
   1283        local count = 0
   1284        local args = {}
   1285        vim.api.nvim_buf_attach(0, false, {
   1286          on_bytes = function(
   1287            _,
   1288            _,
   1289            _,
   1290            start_row,
   1291            start_col,
   1292            start_byte,
   1293            old_row,
   1294            old_col,
   1295            old_byte,
   1296            new_row,
   1297            new_col,
   1298            new_byte
   1299          )
   1300            count = count + 1
   1301            table.insert(args, {
   1302              start_row = start_row,
   1303              start_col = start_col,
   1304              start_byte = start_byte,
   1305              old_row = old_row,
   1306              old_col = old_col,
   1307              old_byte = old_byte,
   1308              new_row = new_row,
   1309              new_col = new_col,
   1310              new_byte = new_byte,
   1311              buffer_lines = vim.api.nvim_buf_get_lines(0, 0, -1, true),
   1312            })
   1313          end,
   1314        })
   1315        vim.cmd('s/llo/y/g')
   1316        return count, args
   1317      end)
   1318 
   1319      -- Should be called twice, once for each match.
   1320      eq(2, call_count)
   1321 
   1322      -- First match: "llo" at column 2 -> "y".
   1323      eq({
   1324        start_row = 0,
   1325        start_col = 2,
   1326        start_byte = 2,
   1327        old_row = 0,
   1328        old_col = 3,
   1329        old_byte = 3,
   1330        new_row = 0,
   1331        new_col = 1,
   1332        new_byte = 1,
   1333        buffer_lines = { 'Hey Hey' },
   1334      }, args[1])
   1335 
   1336      -- Second match: "llo" at column 8 (in original) -> column 6 (after first substitution).
   1337      eq({
   1338        start_row = 0,
   1339        start_col = 6, -- Adjusted position after first substitution.
   1340        start_byte = 6,
   1341        old_row = 0,
   1342        old_col = 3,
   1343        old_byte = 3,
   1344        new_row = 0,
   1345        new_col = 1,
   1346        new_byte = 1,
   1347        buffer_lines = { 'Hey Hey' },
   1348      }, args[2])
   1349    end)
   1350 
   1351    it('on_bytes called correctly for multi-line substitutions', function()
   1352      api.nvim_buf_set_lines(0, 0, -1, true, { 'foo bar', 'baz qux' })
   1353 
   1354      local call_count, args = exec_lua(function()
   1355        local count = 0
   1356        local args = {}
   1357        vim.api.nvim_buf_attach(0, false, {
   1358          on_bytes = function(
   1359            _,
   1360            _,
   1361            _,
   1362            start_row,
   1363            start_col,
   1364            start_byte,
   1365            old_row,
   1366            old_col,
   1367            old_byte,
   1368            new_row,
   1369            new_col,
   1370            new_byte
   1371          )
   1372            count = count + 1
   1373            table.insert(args, {
   1374              start_row = start_row,
   1375              start_col = start_col,
   1376              start_byte = start_byte,
   1377              old_row = old_row,
   1378              old_col = old_col,
   1379              old_byte = old_byte,
   1380              new_row = new_row,
   1381              new_col = new_col,
   1382              new_byte = new_byte,
   1383              buffer_lines = vim.api.nvim_buf_get_lines(0, 0, -1, true),
   1384            })
   1385          end,
   1386        })
   1387        vim.cmd('s/bar/X\\rY/')
   1388        return count, args
   1389      end)
   1390 
   1391      -- Should be called once for the substitution.
   1392      eq(1, call_count)
   1393 
   1394      eq({
   1395        start_row = 0,
   1396        start_col = 4,
   1397        start_byte = 4,
   1398        old_row = 0,
   1399        old_col = 3,
   1400        old_byte = 3,
   1401        new_row = 1,
   1402        new_col = 1,
   1403        new_byte = 3,
   1404        buffer_lines = { 'foo X', 'Y', 'baz qux' },
   1405      }, args[1])
   1406    end)
   1407 
   1408    it('on_bytes called multiple times for global substitution creating multiple lines', function()
   1409      api.nvim_buf_set_lines(0, 0, -1, true, { 'foo bar baz' })
   1410 
   1411      local call_count, args = exec_lua(function()
   1412        local count = 0
   1413        local args = {}
   1414        vim.api.nvim_buf_attach(0, false, {
   1415          on_bytes = function(
   1416            _,
   1417            _,
   1418            _,
   1419            start_row,
   1420            start_col,
   1421            start_byte,
   1422            old_row,
   1423            old_col,
   1424            old_byte,
   1425            new_row,
   1426            new_col,
   1427            new_byte
   1428          )
   1429            count = count + 1
   1430            table.insert(args, {
   1431              start_row = start_row,
   1432              start_col = start_col,
   1433              start_byte = start_byte,
   1434              old_row = old_row,
   1435              old_col = old_col,
   1436              old_byte = old_byte,
   1437              new_row = new_row,
   1438              new_col = new_col,
   1439              new_byte = new_byte,
   1440              buffer_lines = vim.api.nvim_buf_get_lines(0, 0, -1, true),
   1441            })
   1442          end,
   1443        })
   1444        -- Global substitution with newlines in replacement.
   1445        vim.cmd([[s/ /\r/g]])
   1446        return count, args
   1447      end)
   1448 
   1449      -- Should be called once per space replacement.
   1450      eq(2, call_count)
   1451 
   1452      eq({
   1453        start_row = 0,
   1454        start_col = 3,
   1455        start_byte = 3,
   1456        old_row = 0,
   1457        old_col = 1,
   1458        old_byte = 1,
   1459        new_row = 1,
   1460        new_col = 0,
   1461        new_byte = 1,
   1462        buffer_lines = { 'foo', 'bar', 'baz' },
   1463      }, args[1])
   1464 
   1465      eq({
   1466        start_row = 1,
   1467        start_col = 3,
   1468        start_byte = 7,
   1469        old_row = 0,
   1470        old_col = 1,
   1471        old_byte = 1,
   1472        new_row = 1,
   1473        new_col = 0,
   1474        new_byte = 1,
   1475        buffer_lines = { 'foo', 'bar', 'baz' },
   1476      }, args[2])
   1477    end)
   1478 
   1479    it(
   1480      'no buffer update event is emitted while editing substitute command, only after confirmation',
   1481      function()
   1482        api.nvim_buf_set_lines(0, 0, -1, true, { 'Hello world', 'Hello Neovim' })
   1483 
   1484        exec_lua(function()
   1485          _G.num_buffer_updates = 0
   1486          vim.api.nvim_buf_attach(0, false, {
   1487            on_bytes = function()
   1488              _G.num_buffer_updates = _G.num_buffer_updates + 1
   1489            end,
   1490          })
   1491        end)
   1492 
   1493        -- Start typing the substitute command - no events should be emitted yet.
   1494        feed(':%s/Hello/Hi')
   1495        eq(0, exec_lua('return _G.num_buffer_updates'))
   1496 
   1497        -- Continue editing the command - still no events.
   1498        feed('<BS><BS>Hey')
   1499        eq(0, exec_lua('return _G.num_buffer_updates'))
   1500 
   1501        -- After confirming the substitution, two events should be emitted (one per line).
   1502        feed('<CR>')
   1503        eq(2, exec_lua('return _G.num_buffer_updates'))
   1504 
   1505        -- Verify the buffer was actually modified.
   1506        eq({ 'Hey world', 'Hey Neovim' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1507      end
   1508    )
   1509 
   1510    it('flushes delbytes on join', function()
   1511      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC' })
   1512 
   1513      feed('gg0J')
   1514 
   1515      check_events {
   1516        { 'test1', 'bytes', 1, 3, 0, 3, 3, 1, 0, 1, 0, 1, 1 },
   1517      }
   1518 
   1519      command('delete')
   1520      check_events {
   1521        { 'test1', 'bytes', 1, 5, 0, 0, 0, 1, 0, 8, 0, 0, 0 },
   1522      }
   1523    end)
   1524 
   1525    it('sends updates on U', function()
   1526      feed('ggiAAA<cr>BBB')
   1527      feed('<esc>gg$a CCC')
   1528 
   1529      local check_events = setup_eventcheck(verify, nil)
   1530 
   1531      feed('ggU')
   1532 
   1533      check_events {
   1534        { 'test1', 'bytes', 1, 6, 0, 7, 7, 0, 0, 0, 0, 3, 3 },
   1535      }
   1536    end)
   1537 
   1538    it('delete in completely empty buffer', function()
   1539      local check_events = setup_eventcheck(verify, nil)
   1540 
   1541      command 'delete'
   1542      check_events {}
   1543    end)
   1544 
   1545    it('delete the only line of a buffer', function()
   1546      local check_events = setup_eventcheck(verify, { 'AAA' })
   1547 
   1548      command 'delete'
   1549      check_events {
   1550        { 'test1', 'bytes', 1, 3, 0, 0, 0, 1, 0, 4, 1, 0, 1 },
   1551      }
   1552    end)
   1553 
   1554    it('delete the last line of a buffer with two lines', function()
   1555      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB' })
   1556 
   1557      command '2delete'
   1558      check_events {
   1559        { 'test1', 'bytes', 1, 3, 1, 0, 4, 1, 0, 4, 0, 0, 0 },
   1560      }
   1561    end)
   1562 
   1563    it(':sort lines', function()
   1564      local check_events = setup_eventcheck(verify, { 'CCC', 'BBB', 'AAA' })
   1565 
   1566      command '%sort'
   1567      check_events {
   1568        { 'test1', 'bytes', 1, 3, 0, 0, 0, 3, 0, 12, 3, 0, 12 },
   1569      }
   1570    end)
   1571 
   1572    it('handles already sorted lines', function()
   1573      local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC' })
   1574 
   1575      command '%sort'
   1576      check_events {}
   1577    end)
   1578 
   1579    it('works with accepting spell suggestions', function()
   1580      local check_events = setup_eventcheck(verify, { 'hallo world', 'hallo world' })
   1581 
   1582      feed('gg0z=4<cr><cr>') -- accepts 'Hello'
   1583      check_events {
   1584        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 2, 2, 0, 2, 2 },
   1585      }
   1586 
   1587      command('spellrepall') -- replaces whole words
   1588      check_events {
   1589        { 'test1', 'bytes', 1, 4, 1, 0, 12, 0, 5, 5, 0, 5, 5 },
   1590      }
   1591    end)
   1592 
   1593    it('works with :diffput and :diffget', function()
   1594      local check_events = setup_eventcheck(verify, { 'AAA' })
   1595      command('diffthis')
   1596      command('new')
   1597      command('diffthis')
   1598      api.nvim_buf_set_lines(0, 0, -1, true, { 'AAA', 'BBB' })
   1599      feed('G')
   1600      command('diffput')
   1601      check_events {
   1602        { 'test1', 'bytes', 1, 3, 1, 0, 4, 0, 0, 0, 1, 0, 4 },
   1603      }
   1604      api.nvim_buf_set_lines(0, 0, -1, true, { 'AAA', 'CCC' })
   1605      feed('<C-w>pG')
   1606      command('diffget')
   1607      check_events {
   1608        { 'test1', 'bytes', 1, 4, 1, 0, 4, 1, 0, 4, 1, 0, 4 },
   1609      }
   1610    end)
   1611 
   1612    it('prompt buffer', function()
   1613      local check_events = setup_eventcheck(verify, {})
   1614      api.nvim_set_option_value('buftype', 'prompt', {})
   1615      feed('i')
   1616      check_events {
   1617        { 'test1', 'bytes', 1, 3, 0, 0, 0, 0, 0, 0, 0, 2, 2 },
   1618      }
   1619      feed('<CR>')
   1620      check_events {
   1621        { 'test1', 'bytes', 1, 4, 1, 0, 3, 0, 0, 0, 1, 0, 1 },
   1622        { 'test1', 'bytes', 1, 5, 1, 0, 3, 0, 0, 0, 0, 2, 2 },
   1623      }
   1624      feed('<CR>')
   1625      check_events {
   1626        { 'test1', 'bytes', 1, 6, 2, 0, 6, 0, 0, 0, 1, 0, 1 },
   1627        { 'test1', 'bytes', 1, 7, 2, 0, 6, 0, 0, 0, 0, 2, 2 },
   1628      }
   1629      fn.prompt_setprompt('', 'foo > ')
   1630      check_events {
   1631        { 'test1', 'bytes', 1, 8, 2, 0, 6, 0, 2, 2, 0, 6, 6 },
   1632      }
   1633      feed('hello')
   1634      check_events {
   1635        { 'test1', 'bytes', 1, 9, 2, 6, 12, 0, 0, 0, 0, 5, 5 },
   1636      }
   1637      fn.prompt_setprompt('', 'uber-foo > ')
   1638      check_events {
   1639        { 'test1', 'bytes', 1, 10, 2, 0, 6, 0, 6, 6, 0, 11, 11 },
   1640      }
   1641      eq({ '% ', '% ', 'uber-foo > hello' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1642      -- Do this in the same event.
   1643      exec_lua(function()
   1644        vim.fn.setpos("':", { 0, vim.fn.line('.'), 999, 0 })
   1645        vim.fn.prompt_setprompt('', 'discard > ')
   1646      end)
   1647      check_events {
   1648        { 'test1', 'bytes', 1, 11, 2, 0, 6, 0, 16, 16, 0, 10, 10 },
   1649      }
   1650      eq({ '% ', '% ', 'discard > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1651      feed('sup<S-CR>dood')
   1652      check_events {
   1653        { 'test1', 'bytes', 1, 12, 2, 10, 16, 0, 0, 0, 0, 3, 3 },
   1654        { 'test1', 'bytes', 1, 13, 2, 13, 19, 0, 0, 0, 1, 0, 1 },
   1655        { 'test1', 'bytes', 1, 14, 3, 0, 20, 0, 0, 0, 0, 4, 4 },
   1656      }
   1657      eq({ '% ', '% ', 'discard > sup', 'dood' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1658      fn.prompt_setprompt('', 'cool > ')
   1659      check_events {
   1660        { 'test1', 'bytes', 1, 15, 2, 0, 6, 0, 10, 10, 0, 7, 7 },
   1661      }
   1662      eq({ '% ', '% ', 'cool > sup', 'dood' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1663 
   1664      -- init_prompt uses appended_lines_mark when appending to fix prompt.
   1665      api.nvim_buf_set_lines(0, 0, -1, true, { 'hi' })
   1666      eq({ 'hi', 'cool > ' }, api.nvim_buf_get_lines(0, 0, -1, true))
   1667      check_events {
   1668        { 'test1', 'bytes', 1, 16, 0, 0, 0, 4, 0, 22, 1, 0, 3 },
   1669        { 'test1', 'bytes', 1, 17, 1, 0, 3, 0, 0, 0, 1, 0, 8 },
   1670      }
   1671    end)
   1672 
   1673    local function test_lockmarks(mode)
   1674      local description = (mode ~= '') and mode or '(baseline)'
   1675      it('test_lockmarks ' .. description .. ' %delete _', function()
   1676        local check_events = setup_eventcheck(verify, { 'AAA', 'BBB', 'CCC' })
   1677 
   1678        command(mode .. ' %delete _')
   1679        check_events {
   1680          { 'test1', 'bytes', 1, 3, 0, 0, 0, 3, 0, 12, 1, 0, 1 },
   1681        }
   1682      end)
   1683 
   1684      it('test_lockmarks ' .. description .. ' append()', function()
   1685        local check_events = setup_eventcheck(verify)
   1686 
   1687        command(mode .. " call append(0, 'CCC')")
   1688        check_events {
   1689          { 'test1', 'bytes', 1, 2, 0, 0, 0, 0, 0, 0, 1, 0, 4 },
   1690        }
   1691 
   1692        command(mode .. " call append(1, 'BBBB')")
   1693        check_events {
   1694          { 'test1', 'bytes', 1, 3, 1, 0, 4, 0, 0, 0, 1, 0, 5 },
   1695        }
   1696 
   1697        command(mode .. " call append(2, '')")
   1698        check_events {
   1699          { 'test1', 'bytes', 1, 4, 2, 0, 9, 0, 0, 0, 1, 0, 1 },
   1700        }
   1701 
   1702        command(mode .. ' $delete _')
   1703        check_events {
   1704          { 'test1', 'bytes', 1, 5, 3, 0, 10, 1, 0, 1, 0, 0, 0 },
   1705        }
   1706 
   1707        eq('CCC|BBBB|', table.concat(api.nvim_buf_get_lines(0, 0, -1, true), '|'))
   1708      end)
   1709    end
   1710 
   1711    -- check that behavior is identical with and without "lockmarks"
   1712    test_lockmarks ''
   1713    test_lockmarks 'lockmarks'
   1714 
   1715    teardown(function()
   1716      os.remove 'Xtest-reload'
   1717      os.remove 'Xtest-undofile'
   1718      os.remove '.Xtest-undofile.un~'
   1719    end)
   1720  end
   1721 
   1722  describe('(with verify) handles', function()
   1723    do_both(true)
   1724  end)
   1725 
   1726  describe('(without verify) handles', function()
   1727    do_both(false)
   1728  end)
   1729 end)
   1730 
   1731 describe('nvim_buf_attach on_detach', function()
   1732  it('does not SEGFAULT when accessing window buffer info #14998', function()
   1733    local code = function()
   1734      local buf = vim.api.nvim_create_buf(false, false)
   1735 
   1736      vim.cmd 'split'
   1737      vim.api.nvim_win_set_buf(0, buf)
   1738 
   1739      vim.api.nvim_buf_attach(buf, false, {
   1740        on_detach = function(_, buf0)
   1741          vim.fn.tabpagebuflist()
   1742          vim.fn.win_findbuf(buf0)
   1743        end,
   1744      })
   1745    end
   1746 
   1747    exec_lua(code)
   1748    command('q!')
   1749    n.assert_alive()
   1750 
   1751    exec_lua(code)
   1752    command('bd!')
   1753    n.assert_alive()
   1754  end)
   1755 
   1756  it('no invalid lnum error for closed memline #31251', function()
   1757    eq(vim.NIL, exec_lua('return _G.did_detach'))
   1758    exec_lua(function()
   1759      vim.api.nvim_buf_set_lines(0, 0, -1, false, { '' })
   1760      local bufname = 'buf2'
   1761      local buf = vim.api.nvim_create_buf(false, true)
   1762      vim.api.nvim_buf_set_name(buf, bufname)
   1763      vim.bo[buf].bufhidden = 'wipe'
   1764      vim.cmd('vertical diffsplit ' .. bufname)
   1765      vim.api.nvim_buf_attach(0, false, {
   1766        on_detach = function()
   1767          vim.cmd('redraw')
   1768          _G.did_detach = true
   1769        end,
   1770      })
   1771      vim.cmd.bdelete()
   1772    end)
   1773    eq(true, exec_lua('return _G.did_detach'))
   1774  end)
   1775 
   1776  it('called before buf_freeall autocommands', function()
   1777    exec_lua(function()
   1778      vim.api.nvim_create_autocmd({ 'BufUnload', 'BufDelete', 'BufWipeout' }, {
   1779        callback = function(args)
   1780          table.insert(
   1781            _G.events,
   1782            ('%s: %d %s'):format(
   1783              args.event,
   1784              args.buf,
   1785              tostring(vim.api.nvim_buf_is_loaded(args.buf))
   1786            )
   1787          )
   1788        end,
   1789      })
   1790      function _G.on_detach(_, b)
   1791        table.insert(
   1792          _G.events,
   1793          ('on_detach: %d %s'):format(b, tostring(vim.api.nvim_buf_is_loaded(b)))
   1794        )
   1795      end
   1796      _G.events = {}
   1797      vim.cmd 'new'
   1798      vim.bo.bufhidden = 'wipe'
   1799      vim.api.nvim_buf_attach(0, false, { on_detach = _G.on_detach })
   1800      vim.cmd 'quit!'
   1801    end)
   1802 
   1803    eq(
   1804      { 'on_detach: 2 true', 'BufUnload: 2 true', 'BufDelete: 2 true', 'BufWipeout: 2 true' },
   1805      exec_lua('return _G.events')
   1806    )
   1807    eq(false, api.nvim_buf_is_valid(2))
   1808 
   1809    exec_lua(function()
   1810      _G.events = {}
   1811      local buf = vim.api.nvim_create_buf(false, true)
   1812      vim.api.nvim_buf_attach(buf, false, { on_detach = _G.on_detach })
   1813      vim.api.nvim_buf_delete(buf, { force = true })
   1814    end)
   1815 
   1816    -- Was unlisted, so no BufDelete.
   1817    eq(
   1818      { 'on_detach: 3 true', 'BufUnload: 3 true', 'BufWipeout: 3 true' },
   1819      exec_lua('return _G.events')
   1820    )
   1821    eq(false, api.nvim_buf_is_valid(3))
   1822 
   1823    exec_lua(function()
   1824      _G.events = {}
   1825      vim.api.nvim_buf_attach(1, false, { on_detach = _G.on_detach })
   1826      vim.api.nvim_create_autocmd('BufUnload', {
   1827        buffer = 1,
   1828        once = true,
   1829        callback = function()
   1830          vim.api.nvim_buf_attach(1, false, {
   1831            on_detach = function(...)
   1832              vim.fn.bufload(1) -- Leaks the memfile it were to run inside free_buffer_stuff.
   1833              return _G.on_detach(...)
   1834            end,
   1835          })
   1836          table.insert(_G.events, 'local BufUnload')
   1837        end,
   1838      })
   1839      vim.cmd 'edit asdf' -- Reuses buffer 1.
   1840    end)
   1841 
   1842    -- on_detach shouldn't run after autocommands when reusing a buffer (in free_buffer_stuff), even
   1843    -- if those autocommands registered it, as curbuf may be in a semi-unloaded state at that point.
   1844    eq({
   1845      'on_detach: 1 true',
   1846      'BufUnload: 1 true',
   1847      'local BufUnload',
   1848      'BufDelete: 1 true',
   1849      'BufWipeout: 1 true',
   1850    }, exec_lua('return _G.events'))
   1851 
   1852    exec_lua(function()
   1853      _G.events = {}
   1854      vim.api.nvim_buf_attach(0, false, { on_detach = _G.on_detach })
   1855      vim.cmd 'edit'
   1856    end)
   1857 
   1858    -- Re-edit buffer; on_detach is called.
   1859    eq({ 'on_detach: 1 true', 'BufUnload: 1 true' }, exec_lua('return _G.events'))
   1860    eq(true, api.nvim_buf_is_valid(1))
   1861 
   1862    exec_lua(function()
   1863      vim.cmd '%bwipeout!'
   1864      vim.bo.modified = true
   1865      _G.events = {}
   1866      vim.api.nvim_buf_attach(0, false, { on_detach = _G.on_detach })
   1867      vim.api.nvim_buf_delete(0, { force = true })
   1868    end)
   1869 
   1870    -- on_detach must still be first when wiping the last buffer if it's listed and non-reusable.
   1871    -- Previously: BufUnload → BufDelete → on_detach → BufWipeout.
   1872    eq(
   1873      { 'on_detach: 4 true', 'BufUnload: 4 true', 'BufDelete: 4 true', 'BufWipeout: 4 false' },
   1874      exec_lua('return _G.events')
   1875    )
   1876  end)
   1877 
   1878  it('disallows splitting', function()
   1879    command('new | setlocal bufhidden=wipe')
   1880    local buf = api.nvim_get_current_buf()
   1881    exec_lua(function()
   1882      vim.api.nvim_buf_attach(0, false, {
   1883        on_detach = function()
   1884          -- Used to allow opening more views into a closing buffer, resulting in open windows to an
   1885          -- unloaded buffer.
   1886          vim.cmd [=[execute "normal! \<C-W>s"]=]
   1887        end,
   1888      })
   1889    end)
   1890    matches('E1159: Cannot split a window when closing the buffer$', pcall_err(command, 'quit!'))
   1891    eq({}, fn.win_findbuf(buf))
   1892    eq(false, api.nvim_buf_is_valid(buf))
   1893  end)
   1894 end)
   1895 
   1896 it('nvim_buf_attach from buf_freeall autocommands does not leak', function()
   1897  exec_lua(function()
   1898    local b = vim.api.nvim_create_buf(true, true)
   1899    vim.api.nvim_create_autocmd('BufWipeout', {
   1900      buffer = b,
   1901      once = true,
   1902      callback = function()
   1903        vim.api.nvim_buf_attach(b, false, {})
   1904        _G.autocmd_fired = true
   1905      end,
   1906    })
   1907    vim.api.nvim_buf_delete(b, { force = true })
   1908  end)
   1909  eq(true, exec_lua('return _G.autocmd_fired'))
   1910 end)