neovim

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

swapfile_preserve_recover_spec.lua (20377B)


      1 local t = require('test.testutil')
      2 local n = require('test.functional.testnvim')()
      3 local Screen = require('test.functional.ui.screen')
      4 
      5 local uv = vim.uv
      6 local eq, eval, expect, exec = t.eq, n.eval, n.expect, n.exec
      7 local assert_alive = n.assert_alive
      8 local clear = n.clear
      9 local command = n.command
     10 local feed = n.feed
     11 local fn = n.fn
     12 local neq = t.neq
     13 local nvim_prog = n.nvim_prog
     14 local ok = t.ok
     15 local rmdir = n.rmdir
     16 local new_pipename = n.new_pipename
     17 local pesc = vim.pesc
     18 local set_session = n.set_session
     19 local async_meths = n.async_meths
     20 local expect_msg_seq = n.expect_msg_seq
     21 local pcall_err = t.pcall_err
     22 local mkdir = t.mkdir
     23 local poke_eventloop = n.poke_eventloop
     24 local api = n.api
     25 local retry = t.retry
     26 local write_file = t.write_file
     27 
     28 describe(':recover', function()
     29  before_each(clear)
     30 
     31  it('fails if given a non-existent swapfile', function()
     32    local swapname = 'bogus_swapfile'
     33    local swapname2 = 'bogus_swapfile.swp'
     34    eq(
     35      'Vim(recover):E305: No swap file found for ' .. swapname,
     36      pcall_err(command, 'recover ' .. swapname)
     37    ) -- Should not segfault. #2117
     38    -- Also check filename ending with ".swp". #9504
     39    eq('Vim(recover):E306: Cannot open ' .. swapname2, pcall_err(command, 'recover ' .. swapname2)) -- Should not segfault. #2117
     40    assert_alive()
     41  end)
     42 end)
     43 
     44 describe("preserve and (R)ecover with custom 'directory'", function()
     45  local swapdir = uv.cwd() .. '/Xtest_recover_dir'
     46  local testfile = 'Xtest_recover_file1'
     47  -- Put swapdir at the start of the 'directory' list. #1836
     48  -- Note: `set swapfile` *must* go after `set directory`: otherwise it may
     49  -- attempt to create a swapfile in different directory.
     50  local init = [[
     51    set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//
     52    set swapfile fileformat=unix undolevels=-1
     53  ]]
     54 
     55  local nvim0 --- @type test.Session
     56  before_each(function()
     57    nvim0 = n.new_session(false)
     58    set_session(nvim0)
     59    rmdir(swapdir)
     60    mkdir(swapdir)
     61  end)
     62  after_each(function()
     63    command('%bwipeout!')
     64    rmdir(swapdir)
     65  end)
     66 
     67  --- @return string
     68  local function setup_swapname()
     69    exec(init)
     70    command('edit! ' .. testfile)
     71    feed('isometext<esc>')
     72    exec('redir => g:swapname | silent swapname | redir END')
     73    return eval('g:swapname'):match('[^\n]*$')
     74  end
     75 
     76  local function test_recover(swappath1)
     77    -- Start another Nvim instance.
     78    local nvim2 =
     79      n.new_session(false, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false })
     80    set_session(nvim2)
     81 
     82    exec(init)
     83 
     84    -- Use the "SwapExists" event to choose the (R)ecover choice at the dialog.
     85    command('autocmd SwapExists * let v:swapchoice = "r"')
     86    command('silent edit! ' .. testfile)
     87    exec('redir => g:swapname | silent swapname | redir END')
     88 
     89    local swappath2 = eval('g:swapname')
     90 
     91    expect('sometext')
     92    -- swapfile from session 1 should end in .swp
     93    eq(testfile .. '.swp', string.match(swappath1, '[^%%]+$'))
     94    -- swapfile from session 2 should end in .swo
     95    eq(testfile .. '.swo', string.match(swappath2, '[^%%]+$'))
     96    -- Verify that :swapname was not truncated (:help 'shortmess').
     97    ok(nil == string.find(swappath1, '%.%.%.'))
     98    ok(nil == string.find(swappath2, '%.%.%.'))
     99  end
    100 
    101  it('with :preserve and SIGKILL', function()
    102    local swappath1 = setup_swapname()
    103    command('preserve')
    104    neq(nil, uv.fs_stat(swappath1))
    105    eq(0, vim.uv.kill(eval('getpid()'), 'sigkill'))
    106    test_recover(swappath1)
    107  end)
    108 
    109  it('closing stdio channel without :preserve #22096', function()
    110    local swappath1 = setup_swapname()
    111    nvim0:close()
    112    neq(nil, uv.fs_stat(swappath1))
    113    test_recover(swappath1)
    114  end)
    115 
    116  it('killing TUI process without :preserve #22096', function()
    117    local screen0 = Screen.new()
    118    local child_server = new_pipename()
    119    fn.jobstart({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--listen', child_server }, {
    120      term = true,
    121      env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
    122    })
    123    screen0:expect({ any = pesc('[No Name]') }) -- Wait for the child process to start.
    124    local child_session = n.connect(child_server)
    125    set_session(child_session)
    126    local swappath1 = setup_swapname()
    127    set_session(nvim0)
    128    -- n.exec_lua([[vim.uv.kill(vim.fn.jobpid(vim.bo.channel), 'sigterm')]])
    129    command('call chanclose(&channel)') -- Kill the child process.
    130    screen0:expect({ any = pesc('[Process exited 1]') }) -- Wait for the child process to stop.
    131    neq(nil, uv.fs_stat(swappath1))
    132    test_recover(swappath1)
    133  end)
    134 
    135  it('manual :recover with multiple swapfiles', function()
    136    local swappath1 = setup_swapname()
    137    eq('.swp', swappath1:match('%.[^.]+$'))
    138    nvim0:close()
    139    neq(nil, uv.fs_stat(swappath1))
    140    local swappath2 = swappath1:gsub('%.swp$', '.swo')
    141    eq(true, uv.fs_copyfile(swappath1, swappath2))
    142    clear()
    143    exec(init)
    144    local screen = Screen.new(256, 40)
    145    feed(':recover! ' .. testfile .. '<CR>')
    146    screen:expect({
    147      any = {
    148        '\nSwap files found:',
    149        '\n   In directory ',
    150        vim.pesc('\n1.    '),
    151        vim.pesc('\n2.    '),
    152        vim.pesc('\nEnter number of swap file to use (0 to quit): ^'),
    153      },
    154      none = vim.pesc('{18:^@}'),
    155    })
    156    feed('2<CR>')
    157    screen:expect({
    158      any = {
    159        vim.pesc('\nRecovery completed.'),
    160        vim.pesc('\n{6:Press ENTER or type command to continue}^'),
    161      },
    162    })
    163    feed('<CR>')
    164    expect('sometext')
    165  end)
    166 end)
    167 
    168 describe('swapfile detection', function()
    169  local swapdir = uv.cwd() .. '/Xtest_swapdialog_dir'
    170  local nvim0 --- @type test.Session
    171  -- Put swapdir at the start of the 'directory' list. #1836
    172  -- Note: `set swapfile` *must* go after `set directory`: otherwise it may
    173  -- attempt to create a swapfile in different directory.
    174  local init = [[
    175    set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//
    176    set swapfile fileformat=unix nomodified undolevels=-1 nohidden
    177  ]]
    178  before_each(function()
    179    nvim0 = n.new_session(false)
    180    set_session(nvim0)
    181    rmdir(swapdir)
    182    mkdir(swapdir)
    183  end)
    184  after_each(function()
    185    set_session(nvim0)
    186    command('%bwipeout!')
    187    rmdir(swapdir)
    188  end)
    189 
    190  it('redrawing during prompt does not break treesitter', function()
    191    local testfile = 'Xtest_swapredraw.lua'
    192    finally(function()
    193      os.remove(testfile)
    194    end)
    195    write_file(
    196      testfile,
    197      [[
    198 vim.o.foldmethod = 'expr'
    199 vim.o.foldexpr = 'v:lua.vim.treesitter.foldexpr()'
    200 vim.defer_fn(function()
    201  vim.api.nvim__redraw({ valid = false })
    202 end, 500)
    203 pcall(vim.cmd.edit, 'Xtest_swapredraw.lua')
    204    ]]
    205    )
    206    exec(init)
    207    command('edit! ' .. testfile)
    208    command('preserve')
    209    local args2 = { '--clean', '--embed', '--cmd', n.runtime_set }
    210    local nvim2 = n.new_session(true, { args = args2, merge = false })
    211    set_session(nvim2)
    212    local screen2 = Screen.new(100, 40)
    213    screen2:add_extra_attr_ids({
    214      [100] = { foreground = Screen.colors.NvimLightGrey2 },
    215      [101] = { foreground = Screen.colors.NvimLightGreen },
    216      [102] = {
    217        foreground = Screen.colors.NvimLightGrey4,
    218        background = Screen.colors.NvimDarkGrey1,
    219      },
    220      [104] = { foreground = Screen.colors.NvimLightCyan },
    221      [105] = { foreground = Screen.colors.NvimDarkGrey4 },
    222      [106] = {
    223        foreground = Screen.colors.NvimLightGrey2,
    224        background = Screen.colors.NvimDarkGrey4,
    225      },
    226      [107] = { foreground = Screen.colors.NvimLightGrey2, bold = true },
    227      [108] = { foreground = Screen.colors.NvimLightBlue },
    228    })
    229    exec(init)
    230    command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog).
    231    feed(':edit ' .. testfile .. '<CR>')
    232    eq('r?', api.nvim_get_mode().mode)
    233    feed('E:source<CR>')
    234    eq('r?', api.nvim_get_mode().mode)
    235    screen2:sleep(1000)
    236    feed('E')
    237    screen2:expect([[
    238      {100:^vim.o.foldmethod} {100:=} {101:'expr'}                                                                           |
    239      {100:vim.o.foldexpr} {100:=} {101:'v:lua.vim.treesitter.foldexpr()'}                                                  |
    240      {102:+--  3 lines: vim.defer_fn(function()·······························································}|
    241      {104:pcall}{100:(vim.cmd.edit,} {101:'Xtest_swapredraw.lua'}{100:)}                                                         |
    242      {105:~                                                                                                   }|*34
    243      {106:Xtest_swapredraw.lua                                                              1,1            All}|
    244                                                                                                          |
    245    ]])
    246    nvim2:close()
    247  end)
    248 
    249  it('always show swapfile dialog #8840 #9027', function()
    250    local testfile = 'Xtest_swapdialog_file1'
    251 
    252    local expected_no_dialog = '^' .. (' '):rep(256) .. '|\n'
    253    for _ = 1, 37 do
    254      expected_no_dialog = expected_no_dialog .. '~' .. (' '):rep(255) .. '|\n'
    255    end
    256    expected_no_dialog = expected_no_dialog .. testfile .. (' '):rep(216) .. '0,0-1          All|\n'
    257    expected_no_dialog = expected_no_dialog .. (' '):rep(256) .. '|\n'
    258 
    259    exec(init)
    260    command('edit! ' .. testfile)
    261    feed('isometext<esc>')
    262    command('preserve')
    263 
    264    -- Start another Nvim instance.
    265    local nvim2 =
    266      n.new_session(true, { args = { '-u', 'NONE', '-i', 'NONE', '--embed' }, merge = false })
    267    set_session(nvim2)
    268    local screen2 = Screen.new(256, 40)
    269    screen2._default_attr_ids = nil
    270    exec(init)
    271    command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog).
    272 
    273    -- With shortmess+=F
    274    command('set shortmess+=F')
    275    feed(':edit ' .. testfile .. '<CR>')
    276    screen2:expect {
    277      any = [[E325: ATTENTION.*]]
    278        .. '\n'
    279        .. [[Found a swap file by the name ".*]]
    280        .. [[Xtest_swapdialog_dir[/\].*]]
    281        .. testfile
    282        .. [[%.swp"]],
    283    }
    284    feed('e') -- Chose "Edit" at the swap dialog.
    285    screen2:expect(expected_no_dialog)
    286 
    287    -- With :silent and shortmess+=F
    288    feed(':silent edit %<CR>')
    289    screen2:expect {
    290      any = [[Found a swap file by the name ".*]]
    291        .. [[Xtest_swapdialog_dir[/\].*]]
    292        .. testfile
    293        .. [[%.swp"]],
    294    }
    295    feed('e') -- Chose "Edit" at the swap dialog.
    296    screen2:expect(expected_no_dialog)
    297 
    298    -- With :silent! and shortmess+=F
    299    feed(':silent! edit %<CR>')
    300    screen2:expect {
    301      any = [[Found a swap file by the name ".*]]
    302        .. [[Xtest_swapdialog_dir[/\].*]]
    303        .. testfile
    304        .. [[%.swp"]],
    305    }
    306    feed('e') -- Chose "Edit" at the swap dialog.
    307    screen2:expect(expected_no_dialog)
    308 
    309    -- With API (via eval/Vimscript) call and shortmess+=F
    310    feed(':call nvim_command("edit %")<CR>')
    311    screen2:expect {
    312      any = [[Found a swap file by the name ".*]]
    313        .. [[Xtest_swapdialog_dir[/\].*]]
    314        .. testfile
    315        .. [[%.swp"]],
    316    }
    317    feed('e') -- Chose "Edit" at the swap dialog.
    318    screen2:expect({ any = pesc('E5555: API call: Vim(edit):E325: ATTENTION') })
    319    feed('<c-c>')
    320    screen2:expect(expected_no_dialog)
    321 
    322    -- With API call and shortmess+=F
    323    async_meths.nvim_command('edit %')
    324    screen2:expect {
    325      any = [[Found a swap file by the name ".*]]
    326        .. [[Xtest_swapdialog_dir[/\].*]]
    327        .. testfile
    328        .. [[%.swp"]],
    329    }
    330    feed('e') -- Chose "Edit" at the swap dialog.
    331    expect_msg_seq({
    332      ignore = { 'redraw' },
    333      seqs = {
    334        { { 'notification', 'nvim_error_event', { 0, 'Vim(edit):E325: ATTENTION' } } },
    335      },
    336    })
    337    feed('<cr>')
    338 
    339    nvim2:close()
    340  end)
    341 
    342  it('default SwapExists handler selects "(E)dit" and skips prompt', function()
    343    exec(init)
    344    command('edit Xfile1')
    345    command("put ='some text...'")
    346    command('preserve') -- Make sure the swap file exists.
    347    local nvimpid = fn.getpid()
    348 
    349    local nvim1 = n.new_session(true)
    350    set_session(nvim1)
    351    local screen = Screen.new(75, 18)
    352    exec(init)
    353    feed(':edit Xfile1\n')
    354 
    355    screen:expect({ any = ('W325: Ignoring swapfile from Nvim process %d'):format(nvimpid) })
    356    nvim1:close()
    357  end)
    358 
    359  -- oldtest: Test_swap_prompt_splitwin()
    360  it('selecting "q" in the attention prompt', function()
    361    exec(init)
    362    command('edit Xfile1')
    363    command('preserve') -- Make sure the swap file exists.
    364 
    365    local screen = Screen.new(75, 18)
    366    local nvim1 = n.new_session(true)
    367    set_session(nvim1)
    368    screen:attach()
    369    exec(init)
    370    command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog).
    371    feed(':split Xfile1\n')
    372    -- The default SwapExists handler does _not_ skip this prompt.
    373    screen:expect({
    374      any = pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
    375    })
    376    feed('q')
    377    screen:expect([[
    378      ^                                                                           |
    379      {1:~                                                                          }|*16
    380                                                                                 |
    381    ]])
    382    feed(':<CR>')
    383    screen:expect([[
    384      ^                                                                           |
    385      {1:~                                                                          }|*16
    386      :                                                                          |
    387    ]])
    388    nvim1:close()
    389 
    390    local nvim2 = n.new_session(true)
    391    set_session(nvim2)
    392    screen:attach()
    393    exec(init)
    394    command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog).
    395    command('set more')
    396    command('au bufadd * let foo_w = wincol()')
    397    feed(':e Xfile1<CR>')
    398    screen:expect({ any = pesc('{6:-- More --}^') })
    399    feed('<Space>')
    400    screen:expect({
    401      any = pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
    402    })
    403    feed('q')
    404    command([[echo 'hello']])
    405    screen:expect([[
    406      ^                                                                           |
    407      {1:~                                                                          }|*16
    408      hello                                                                      |
    409    ]])
    410    nvim2:close()
    411  end)
    412 
    413  --- @param swapexists boolean Enable the default SwapExists handler.
    414  --- @param on_swapfile_running fun(screen: any) Called after swapfile ("STILL RUNNING") prompt.
    415  local function test_swapfile_after_reboot(swapexists, on_swapfile_running)
    416    local screen = Screen.new(75, 30)
    417 
    418    exec(init)
    419    if not swapexists then
    420      command('autocmd! nvim.swapfile') -- Delete the default handler (which skips the dialog).
    421    end
    422    command('set nohidden')
    423 
    424    exec([=[
    425      " Make a copy of the current swap file to "Xswap".
    426      " Return the name of the swap file.
    427      func CopySwapfile()
    428        preserve
    429        " get the name of the swap file
    430        let swname = split(execute("swapname"))[0]
    431        let swname = substitute(swname, '[[:blank:][:cntrl:]]*\(.\{-}\)[[:blank:][:cntrl:]]*$', '\1', '')
    432        " make a copy of the swap file in Xswap
    433        set binary
    434        exe 'sp ' . fnameescape(swname)
    435        w! Xswap
    436        set nobinary
    437        return swname
    438      endfunc
    439    ]=])
    440 
    441    -- Edit a file and grab its swapfile.
    442    exec([[
    443      edit Xswaptest
    444      call setline(1, ['a', 'b', 'c'])
    445    ]])
    446    local swname = fn.CopySwapfile()
    447 
    448    -- Forget we edited this file
    449    exec([[
    450      new
    451      only!
    452      bwipe! Xswaptest
    453    ]])
    454 
    455    os.rename('Xswap', swname)
    456 
    457    feed(':edit Xswaptest<CR>')
    458    on_swapfile_running(screen)
    459 
    460    feed('e')
    461 
    462    -- Forget we edited this file
    463    exec([[
    464      new
    465      only!
    466      bwipe! Xswaptest
    467    ]])
    468 
    469    -- pretend that the swapfile was created before boot
    470    local atime = os.time() - uv.uptime() - 10
    471    uv.fs_utime(swname, atime, atime)
    472 
    473    feed(':edit Xswaptest<CR>')
    474    screen:expect({
    475      any = table.concat({
    476        '{9:E325: ATTENTION}',
    477        pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort: }^'),
    478      }, '.*'),
    479    })
    480 
    481    feed('e')
    482  end
    483 
    484  -- oldtest: Test_nocatch_process_still_running()
    485  it('swapfile created before boot vim-patch:8.2.2586', function()
    486    test_swapfile_after_reboot(false, function(screen)
    487      screen:expect({
    488        any = table.concat({
    489          '{9:E325: ATTENTION}',
    490          '{6:        process ID: %d* %(STILL RUNNING%)}',
    491          '{6:While opening file "Xswaptest"}',
    492          pesc('{6:[O]pen Read-Only, (E)dit anyway, (R)ecover, (Q)uit, (A)bort: }^'),
    493        }, '.*'),
    494      })
    495    end)
    496  end)
    497 
    498  it('swapfile created before boot + default SwapExists handler', function()
    499    test_swapfile_after_reboot(true, function(screen)
    500      screen:expect({ any = 'W325: Ignoring swapfile from Nvim process' })
    501    end)
    502  end)
    503 end)
    504 
    505 describe('quitting swapfile dialog on startup stops TUI properly', function()
    506  local swapdir = uv.cwd() .. '/Xtest_swapquit_dir'
    507  local testfile = 'Xtest_swapquit_file1'
    508  local otherfile = 'Xtest_swapquit_file2'
    509  -- Put swapdir at the start of the 'directory' list. #1836
    510  -- Note: `set swapfile` *must* go after `set directory`: otherwise it may
    511  -- attempt to create a swapfile in different directory.
    512  local init_dir = [[set directory^=]] .. swapdir:gsub([[\]], [[\\]]) .. [[//]]
    513  local init_set = [[set swapfile fileformat=unix nomodified undolevels=-1 nohidden]]
    514 
    515  before_each(function()
    516    clear({ args = { '--cmd', init_dir, '--cmd', init_set } })
    517    rmdir(swapdir)
    518    mkdir(swapdir)
    519    write_file(
    520      testfile,
    521      [[
    522      first
    523      second
    524      third
    525 
    526    ]]
    527    )
    528    command('edit! ' .. testfile)
    529    feed('Gisometext<esc>')
    530    poke_eventloop()
    531    clear() -- Leaves a swap file behind
    532    api.nvim_ui_attach(80, 30, {})
    533  end)
    534  after_each(function()
    535    rmdir(swapdir)
    536    os.remove(testfile)
    537    os.remove(otherfile)
    538  end)
    539 
    540  it('(Q)uit at first file argument', function()
    541    local chan = fn.jobstart(
    542      { nvim_prog, '-u', 'NONE', '-i', 'NONE', '--cmd', init_dir, '--cmd', init_set, testfile },
    543      {
    544        term = true,
    545        env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
    546      }
    547    )
    548    retry(nil, nil, function()
    549      eq(
    550        '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:',
    551        eval("getline('$')->trim(' ', 2)")
    552      )
    553    end)
    554    api.nvim_chan_send(chan, 'q')
    555    retry(nil, nil, function()
    556      eq(
    557        { '', '[Process exited 1]', '' },
    558        eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})")
    559      )
    560    end)
    561  end)
    562 
    563  it('(A)bort at second file argument with -p', function()
    564    local chan = fn.jobstart({
    565      nvim_prog,
    566      '-u',
    567      'NONE',
    568      '-i',
    569      'NONE',
    570      '--cmd',
    571      init_dir,
    572      '--cmd',
    573      init_set,
    574      '-p',
    575      otherfile,
    576      testfile,
    577    }, {
    578      term = true,
    579      env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
    580    })
    581    retry(nil, nil, function()
    582      eq(
    583        '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:',
    584        eval("getline('$')->trim(' ', 2)")
    585      )
    586    end)
    587    api.nvim_chan_send(chan, 'a')
    588    retry(nil, nil, function()
    589      eq(
    590        { '', '[Process exited 1]', '' },
    591        eval("[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})")
    592      )
    593    end)
    594  end)
    595 
    596  it('(Q)uit at file opened by -t', function()
    597    write_file(
    598      otherfile,
    599      ([[
    600      !_TAG_FILE_ENCODING	utf-8	//
    601      first	%s	/^  \zsfirst$/
    602      second	%s	/^  \zssecond$/
    603      third	%s	/^  \zsthird$/]]):format(testfile, testfile, testfile)
    604    )
    605    local chan = fn.jobstart({
    606      nvim_prog,
    607      '-u',
    608      'NONE',
    609      '-i',
    610      'NONE',
    611      '--cmd',
    612      init_dir,
    613      '--cmd',
    614      init_set,
    615      '--cmd',
    616      'set tags=' .. otherfile,
    617      '-tsecond',
    618    }, {
    619      term = true,
    620      env = { VIMRUNTIME = os.getenv('VIMRUNTIME') },
    621    })
    622    retry(nil, nil, function()
    623      eq(
    624        '[O]pen Read-Only, (E)dit anyway, (R)ecover, (D)elete it, (Q)uit, (A)bort:',
    625        eval("getline('$')->trim(' ', 2)")
    626      )
    627    end)
    628    api.nvim_chan_send(chan, 'q')
    629    retry(nil, nil, function()
    630      eq(
    631        { '[Process exited 1]' },
    632        eval(
    633          "[1, 2, '$']->map({_, lnum -> getline(lnum)->trim(' ', 2)})->filter({_, s -> !empty(trim(s))})"
    634        )
    635      )
    636    end)
    637  end)
    638 end)